首页 > 其他分享 >浅析 Kotlin 中的 synchronized

浅析 Kotlin 中的 synchronized

时间:2022-11-27 12:31:39浏览次数:75  
标签:synchronized 对象 Kotlin public 线程 方法 浅析 loop


首先,在 Java 中 synchronized 是一个关键字,在Kotlin 中是一个函数。这个函数如下:

/*
* Copyright 2010-2018 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/

@file:kotlin.jvm.JvmMultifileClass
@file:kotlin.jvm.JvmName("StandardKt")
package kotlin

import kotlin.contracts.*
import kotlin.jvm.internal.unsafe.*

/**
* Executes the given function [block] while holding the monitor of the given object [lock].
*/
@kotlin.internal.InlineOnly
public actual inline fun <R> synchronized(lock: Any, block: () -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}

@Suppress("NON_PUBLIC_CALL_FROM_PUBLIC_INLINE", "INVISIBLE_MEMBER")
monitorEnter(lock)
try {
return block()
}
finally {
@Suppress("NON_PUBLIC_CALL_FROM_PUBLIC_INLINE", "INVISIBLE_MEMBER")
monitorExit(lock)
}
}

Decompile成字节码:




浅析 Kotlin 中的 synchronized_java


可以看出:这里边也是有monitorenter和monitorexit的,所以做出推测,不管synchronized是java中的关键字还是kotlin中的函数,最终被编译成的字节码是一样的。

关于:contract{ ... } Kotlin 的契约编程,

Java synchronized 实现原理

在《深入理解Java虚拟机》一书中,介绍了HotSpot虚拟机中,对象的内存布局分为三个区域:对象头(Header)、实例数据(Instance Data)和对齐数据(Padding)。而对象头又分为两个部分“Mark Word”和类型指针,其中“Mark Word”包含了线程持有的锁。
  因此,synchronized锁,也是保存在对象头中。JVM基于进入和退出Monitor对象来实现synchronized方法和代码块的同步,对于方法和代码块的实现细节又有不同:

代码块,使用monitorenter和monitorexit指令来实现;monitorenter指令编译后,插入到同步代码块开始的位置,monitorexit指令插入到方法同步代码块结束位置和异常处,JVM保证每个monitorenter必须有一个monitorexit指令与之对应。线程执行到monitorenter指令处时,会尝试获取对象对应的Monitor对象的所有权 (任何一个对象都有一个Monitor对象预制对应,当一个Monitor被持有后,它将处于锁定状态) 。

方法:在《深入理解Java虚拟机》同步指令一节中,关于方法级的同步描述如下:

方法级的同步是隐式的,即无需通过字节码指令来控制,它实现在方法调用和返回操作之中。JVM可以从方法常量池中的方法表结构(method_info Structure) 中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会 检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有管程,然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放管程。在方法执行期间,执行线程获取了管程,其他线程就无法获取管程。

synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性。

Java中每一个对象都可以作为锁,这是synchronized实现同步的基础:

  • 普通同步方法,锁是当前实例对象
  • 静态同步方法,锁是当前类的class对象
  • 同步方法块,锁是括号里面的对象

在​​HotSpot​​虚拟机中,对象的内存布局分为三个区域:

  • 对象头(​​Header​​)
  • 实例数据(​​Instance Data​​)
  • 对齐填充(​​Padding​​)

其中,对象头(​​Header​​)又分为两部分:

  • ​Mark Word​
  • 类型指针

​synchronized​​​用的锁是存储在​​Java​​​对象头的​​Mark Word​​中的。

下面是​​Mark Word​​​的存储结构(​​32位JVM​​):

锁状态

25bit

4bit

1bit,是否是偏向锁

2bit,锁标志位

无锁状态

对象的hashCode

对象分代年龄

0

01

在运行期,​​Mark Word​​里存储的数据会随着标志位的变化而变化。

存储内容

标志位

状态

指向栈中锁记录的指针

00

轻量级锁

指向互斥量(重量级锁)的指针

10

重量级锁

空,不需要记录信息

11

GC标记

偏向线程ID、偏向时间戳、对象分代年龄

01

偏向锁

可以看到,​​Mark Word​​包含了线程持有的锁。

​JVM​​​基于进入和退出​​Monitor​​​对象来实现​​sunchronized​​方法和代码块的同步,两者细节上有差异。

1.1 synchronized代码块

使用​​monitorenter​​​和​​monitorexit​​指令来实现。

​minitorenter​​​指令编译后,插入到同步代码块开始的位置,​​monitorexit​​​指令编译后,插入到同步代码块结束的位置和异常处。​​JVM​​​保证每个​​monitorenter​​​必须有一个​​monitorexit​​指令与之对应。

每个对象都有一个​​Monitor​​对象(监视器锁)与之对应。

  • monitorenter

当线程执行到​​monitorenter​​​指令的时候,将会尝试获取​​Monitor​​对象的所有权,过程如下:

  1. 如果​​Monitor​​​对象的进入计数器为​​0​​​,则该线程成功获取​​Monitor​​​对象的所有权,然后将计数器设置为​​1​​。
  2. 如果该线程已经拥有了​​Monitor​​​的所有权,那这次算作是重入,重入也会将计数器的值加​​1​​。
  3. 如果其他线程已经占有了​​Monitor​​​对象,那么该线程进入阻塞状态,直到​​Monitor​​​的计数器的值为​​0​​​,再重新尝试获取​​Monitor​​对象的所有权。
  • monitorexit

当已经获取​​Monitor​​​对象所有权的线程执行到​​monitorexit​​​指令的时候,将会释放​​Monitor​​对象的所有权。过程如下:

  1. 执行​​monitorexit​​​指令时,​​Monitor​​​对象的进入计数器的值减​​1​​​,如果减​​1​​​后的值为​​0​​​,那么这个线程将会释放​​Monitor​​​对象的所有权,其他被这个​​Monitor​​​阻塞的线程可以开始尝试去获取这个​​Monitor​​对象的所有权。
public class com.fufu.concurrent.SyncCodeBlock {
public int i;

public com.fufu.concurrent.SyncCodeBlock();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return

public void syncTask();
Code:
0: aload_0
1: dup
2: astore_1
3: monitorenter //注意此处,进入同步方法
4: aload_0
5: dup
6: getfield #2 // Field i:I
9: iconst_1
10: iadd
11: putfield #2 // Field i:I
14: aload_1
15: monitorexit //注意此处,退出同步方法
16: goto 24
19: astore_2
20: aload_1
21: monitorexit //注意此处,退出同步方法
22: aload_2
23: athrow
24: return
Exception table:
from to target type
4 16 19 any
19 22 19 any
}

1.2 synchronized方法

方法级的同步是隐式的,即无需通过字节码指令来控制,它实现在方法调用和返回操作之中。

​JVM​​可以从 方法常量池 中的 方法表结构(method_info Structure) 中的 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法。

当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有管程,然后再执行方法,最后在方法完成(无论是正常完成还是非正常完成)时释放管程。在方法执行期间,执行线程获取了管程,其他线程就无法获取管程。

//省略没必要的字节码
//==================syncTask方法======================
public synchronized void syncTask();
descriptor: ()V
//方法标识ACC_PUBLIC代表public修饰,ACC_SYNCHRONIZED指明该方法为同步方法
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: dup
2: getfield #2 // Field i:I
5: iconst_1
6: iadd
7: putfield #2 // Field i:I
10: return
LineNumberTable:
line 12: 0
line 13: 10

2 synchronized使用规则

下面总结了对象的​​synchronized​​基本规则。

  • 规则一:当一个线程访问 “某对象” 的 “synchronized方法” 或者 “synchronized代码块” 时,其他线程对“该对象” 的这个 “synchronized方法” 或者这个 “synchronized代码块” 的访问将被阻塞。
  • 规则二:当一个线程访问 “某对象” 的 “synchronized方法” 或者 “synchronized代码块” 时,其他线程对“该对象” 的其他的 “synchronized方法” 或者其他的 “synchronized代码块” 的访问将被阻塞。
  • 规则三:当一个线程访问 “某对象” 的 “synchronized方法” 或者 “synchronized代码块” 时,其他线程仍然可以访问 “该对象” 的非同步代码块

2.1 规则一

当一个线程访问 “某对象” 的 “synchronized方法” 或者 “synchronized代码块” 时,其他线程对“该对象” 的这个 “synchronized方法” 或者这个 “synchronized代码块” 的访问将被阻塞。

public class Demo1 {

public static void main(String[] args) {

UserRunnable r = new UserRunnable();
Thread t1 = new Thread(r, "thread-1");
Thread t2 = new Thread(r, "thread-2");
t1.start();
t2.start();
}
}

class UserRunnable implements Runnable {

@Override
public void run() {
synchronized (this) {
try {
for (int i = 1; i <= 3; i++) {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + " loop " + i);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

运行结果:

thread-1 loop 1
thread-1 loop 2
thread-1 loop 3
thread-2 loop 1
thread-2 loop 2
thread-2 loop 3

Process finished with exit code 0

可以看到,线程​​thread-1​​​获得了​​r​​​对象的锁,执行同步代码块,线程​​thread-2​​​只能等待线程​​thread-1​​执行完了才能开始执行。

2.2 规则二

当一个线程访问 “某对象” 的 “synchronized方法” 或者 “synchronized代码块” 时,其他线程对“该对象” 的其他的 “synchronized方法” 或者其他的 “synchronized代码块” 的访问将被阻塞。

public class Demo2 {

public static void main(String[] args) {

Obj obj = new Obj();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
obj.methadA();
}
}, "thread-1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
obj.methadB();
}
}, "thread-2");
t1.start();
t2.start();
}
}

class Obj {

public void methadA() {
synchronized (this) {
try {
for (int i = 1; i <= 3; i++) {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()
+ " call methodA, loop " + i);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

public void methadB() {
synchronized (this) {
try {
for (int i = 1; i <= 3; i++) {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()
+ " call methodB, loop " + i);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

运行结果:

thread-1 call methodA, loop 1
thread-1 call methodA, loop 2
thread-1 call methodA, loop 3
thread-2 call methodB, loop 1
thread-2 call methodB, loop 2
thread-2 call methodB, loop 3

Process finished with exit code 0

可以看到,​​Obj​​​类中的​​methodA​​​和​​methodB​​​方法都有一个同步代码块。当线程​​thread-1​​​调用​​obj​​​对象的​​methodA​​​方法的时候,线程​​thread-2​​​被阻塞了,直到​​thread-1​​​释放了​​obj​​​对象的锁,​​thread-2​​​才开始调用​​methodB​​方法。

2.3 规则三

当一个线程访问 “某对象” 的 “synchronized方法” 或者 “synchronized代码块” 时,其他线程仍然可以访问 “该对象” 的非同步代码块

public class Demo3 {

public static void main(String[] args) {

Obj obj = new Obj();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
obj.methadA();
}
}, "thread-1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
obj.methadB();
}
}, "thread-2");
t1.start();
t2.start();
}
}

class Obj {

public void methadA() {
synchronized (this) {
try {
for (int i = 1; i <= 3; i++) {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()
+ " call methodA, loop " + i);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

public void methadB() {
try {
for (int i = 1; i <= 3; i++) {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()
+ " call methodB, loop " + i);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

运行结果:

thread-1 call methodA, loop 1
thread-2 call methodB, loop 1
thread-1 call methodA, loop 2
thread-2 call methodB, loop 2
thread-1 call methodA, loop 3
thread-2 call methodB, loop 3

Process finished with exit code 0

可以看到,​​Obj​​​类的​​methodA​​​方法有同步代码块,而​​methodB​​​方法没有。当线程​​thread-1​​​访问​​methodA​​​方法的时候,线程​​thread-2​​​可以访问​​methodB​​方法,不会阻塞。

3 实例锁 和 全局锁

实例锁

  • 锁在某一个实例对象上。如果该类是单例,那么该锁也具有全局锁的概念。
  • 实例锁对应的就是 ​​synchronized​​关键字。

全局锁

  • 该锁针对的是类,无论实例多少个对象,线程都共享该锁。
  • 全局锁对应的就是 ​​static synchronized​​​关键字(或者是锁在该类的​​class​​​或者​​lassloader​​对象上)。

例子:

pulbic class Something {
public synchronized void syncA(){}
public synchronized void syncB(){}
public static synchronized void cSyncA(){}
public static synchronized void cSyncB(){}
}

假设​​Something​​​有两个实例​​x​​​和​​y​​,结论:

  1. ​x.syncA()​​​和​​x.syncB()​不能被同时访问。因为使用了同一个对象的实例锁。
  2. ​x.syncA()​​​和​​y.syncB()​可以被同时访问。因为使用了不同实例对象的实例锁。
  3. ​x.cSyncA()​​​和​​y.cSyncB()​不能被同时访问。因为他们使用了同一个全局锁,相当于​​Something​​类的锁。
  4. ​x.syncA()​​​和​​Something.cSyncA()​可以被同时访问。因为一个是实例​​x​​​的锁,一个是类​​Something​​的锁,不是同一个锁,互不干扰。

参考资料


《Java并发编程艺术》​​​【死磕Java并发】—–深入分析synchronized的实现原理​​​​JVM源码分析之synchronized实现​

标签:synchronized,对象,Kotlin,public,线程,方法,浅析,loop
From: https://blog.51cto.com/u_15236724/5889961

相关文章

  • Java SPI 机制浅析
    本文通过探析JDK提供的,在开源项目中比较常用的JavaSPI机制,希望给大家在实际开发实践、学习开源项目提供参考。一、SPI是什么SPI全称ServiceProviderInterface,......
  • 性能测试中TPS上不去的几种原因浅析
    https://www.cnblogs.com/imyalost/p/8309468.html先来解释下什么叫TPS:TPS(TransactionPerSecond):每秒事务数,指服务器在单位时间内(秒)可以处理的事务数量,一般以request/se......
  • 性能测试常见术语浅析
    https://www.cnblogs.com/imyalost/p/7117320.html负载对被测系统不断施加压力,直到性能指标超过预期或某项资源使用达到饱和,以验证系统的处理极限,为系统性能调优提供依据......
  • kotlin可空和不可空理解
    下面方法中,如果有分支返回了null,就必须在返回值后面加上?,funtestStr(ints:Int):String?{returnif(ints>0)"youareright"elsenull;}那么访问返回值时候呢又......
  • kotlin函数的举例
    funtheAnswer()=42实际上等于funtheAnswer():Int{return42;}类似java的switch逻辑分支函数funtransform(color:String):Int=when(color){"Red"->0"G......
  • kotlin类似javalist map所谓c shape 或ios那边的字典的遍历循环和创建以及泛型
    println("testlengthfunc:${getObjectLength("Howlongdoihave,please?")}");//geLength会出现会重写的情况,应该是自动倒入了某些系统的类导致的。varlist=li......
  • 碉堡了的kotlin扩展函数
    //下面的方法存在于一个方法体中//varisContainQSSQ2="ddddd2".isContainerQSSQ()//therenotisContainerExtandMehtoderr//println("striscontainqssq:${i......
  • kotlin更牛逼的转义类似swift php的某些代码块转义
    里面任何字符都不会进行转义valtext="""for(cin"foo")//\nprint(c)"""println("kotlin的语法演示:${text}");那么转义则和java一样​​"hello\nworld"......
  • kotlin 类继承的演示
    openclassPerson(name:String,age:Int){varcurrentName=name;varage=age;init{"initcall${currentName}name:${name}......
  • java单例转kotlin的表现
    java代码classTestStatic{privatestaticfinalStringTAG="TestStatic";privatestaticTestStaticinstance;privatestaticTestStaticstaticField;......