首先,在 Java 中 synchronized 是一个关键字,在Kotlin 中是一个函数。这个函数如下:
Decompile成字节码:
可以看出:这里边也是有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
对象的所有权,过程如下:
- 如果
Monitor
对象的进入计数器为0
,则该线程成功获取Monitor
对象的所有权,然后将计数器设置为1
。- 如果该线程已经拥有了
Monitor
的所有权,那这次算作是重入,重入也会将计数器的值加1
。- 如果其他线程已经占有了
Monitor
对象,那么该线程进入阻塞状态,直到Monitor
的计数器的值为0
,再重新尝试获取Monitor
对象的所有权。
- monitorexit
当已经获取Monitor
对象所有权的线程执行到monitorexit
指令的时候,将会释放Monitor
对象的所有权。过程如下:
- 执行
monitorexit
指令时,Monitor
对象的进入计数器的值减1
,如果减1
后的值为0
,那么这个线程将会释放Monitor
对象的所有权,其他被这个Monitor
阻塞的线程可以开始尝试去获取这个Monitor
对象的所有权。
1.2 synchronized方法
方法级的同步是隐式的,即无需通过字节码指令来控制,它实现在方法调用和返回操作之中。
JVM
可以从 方法常量池 中的 方法表结构(method_info Structure
) 中的 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法。
当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有管程,然后再执行方法,最后在方法完成(无论是正常完成还是非正常完成)时释放管程。在方法执行期间,执行线程获取了管程,其他线程就无法获取管程。
2 synchronized使用规则
下面总结了对象的synchronized
基本规则。
- 规则一:当一个线程访问 “某对象” 的 “synchronized方法” 或者 “synchronized代码块” 时,其他线程对“该对象” 的这个 “synchronized方法” 或者这个 “synchronized代码块” 的访问将被阻塞。
- 规则二:当一个线程访问 “某对象” 的 “synchronized方法” 或者 “synchronized代码块” 时,其他线程对“该对象” 的其他的 “synchronized方法” 或者其他的 “synchronized代码块” 的访问将被阻塞。
- 规则三:当一个线程访问 “某对象” 的 “synchronized方法” 或者 “synchronized代码块” 时,其他线程仍然可以访问 “该对象” 的非同步代码块。
2.1 规则一
当一个线程访问 “某对象” 的 “synchronized方法” 或者 “synchronized代码块” 时,其他线程对“该对象” 的这个 “synchronized方法” 或者这个 “synchronized代码块” 的访问将被阻塞。
运行结果:
可以看到,线程thread-1
获得了r
对象的锁,执行同步代码块,线程thread-2
只能等待线程thread-1
执行完了才能开始执行。
2.2 规则二
当一个线程访问 “某对象” 的 “synchronized方法” 或者 “synchronized代码块” 时,其他线程对“该对象” 的其他的 “synchronized方法” 或者其他的 “synchronized代码块” 的访问将被阻塞。
运行结果:
可以看到,Obj
类中的methodA
和methodB
方法都有一个同步代码块。当线程thread-1
调用obj
对象的methodA
方法的时候,线程thread-2
被阻塞了,直到thread-1
释放了obj
对象的锁,thread-2
才开始调用methodB
方法。
2.3 规则三
当一个线程访问 “某对象” 的 “synchronized方法” 或者 “synchronized代码块” 时,其他线程仍然可以访问 “该对象” 的非同步代码块。
运行结果:
可以看到,Obj
类的methodA
方法有同步代码块,而methodB
方法没有。当线程thread-1
访问methodA
方法的时候,线程thread-2
可以访问methodB
方法,不会阻塞。
3 实例锁 和 全局锁
实例锁:
- 锁在某一个实例对象上。如果该类是单例,那么该锁也具有全局锁的概念。
- 实例锁对应的就是
synchronized
关键字。
全局锁:
- 该锁针对的是类,无论实例多少个对象,线程都共享该锁。
- 全局锁对应的就是
static synchronized
关键字(或者是锁在该类的class
或者lassloader
对象上)。
例子:
假设Something
有两个实例x
和y
,结论:
-
x.syncA()
和x.syncB()
不能被同时访问。因为使用了同一个对象的实例锁。 -
x.syncA()
和y.syncB()
可以被同时访问。因为使用了不同实例对象的实例锁。 -
x.cSyncA()
和y.cSyncB()
不能被同时访问。因为他们使用了同一个全局锁,相当于Something
类的锁。 -
x.syncA()
和Something.cSyncA()
可以被同时访问。因为一个是实例x
的锁,一个是类Something
的锁,不是同一个锁,互不干扰。
参考资料
《Java并发编程艺术》【死磕Java并发】—–深入分析synchronized的实现原理JVM源码分析之synchronized实现