Synchronize 底层原理
我正在参与「启航计划」
目标内存结构
目标头:MarkWord
存储目标头的信息,Klass Word
描绘目标实例的详细类型
实例数据:成员变量
对齐填充:假如目标头 + 实例变量 不是 8 的整数倍,则经过对齐填充补齐
MarkWord 解析
-
hashcode
:25位的目标标识Hash码 -
age
:目标分代年龄占4位 -
biased_lock
:倾向锁标识,占1位,0表明没有开端倾向锁,1表明敞开了倾向锁 -
thread
:持有倾向锁的线程ID,占23位 -
epoch
:倾向时刻戳,占2位 -
ptr_to_lock_record:
轻量级锁状况下,指向栈中锁记录的指针,占30位 -
ptr_to_heavyweight_monitor
:重量级锁状况下,指向目标监视器Monitor的指针,占30位
LockRecord 锁记录
Markword:记录锁记录的地址
目标引证:引证被加上锁了的目标
重量级锁
Monitor
Monitor 监视器,是由 jvm 供给的,由 C++ 完成的,有三个完成部分
WaitSet
:调用了 wait 方法的线程在这里等候,处于 WAITED 状况
EntryList
:没有抢到目标锁的线程在这里等候,处于 BLOCKED 状况
Owner
:存储已经抢到锁的线程目标
Monitor
的完成属于重量级锁,触及到 内核态和用户态的切换,线程的上下文切换,每个 Java
目标都会关联一个 Monitor
目标,假如运用 Synchronize
给该目标加锁,那么 Java
目标上面的 MarkWord
地址就被设置为指向该 Monitor
目标的指针
轻量级锁
加锁流程:
- 在线程栈中创建一个
Lock Record
目标,它的object reference
字段指向锁目标 - 经过 CAS 指令把
Lock Record
的地址存放到目标头的Markword
中,假如是无锁状况则修正成功,代表该线程获取了轻量级锁 - 假如当时线程已经持有该锁,就代表是一次锁重入,设置
Lock Record
的第一部分为null
,起到一个重入计数器的效果 - 假如
CAS
修正失败,则阐明发生了竞赛,需求膨胀为重量级锁
解锁进程:
- 遍历线程栈,找到一切
object reference
字段等于当时锁目标的Lock record
- 假如
Lock record
的MarkWord
为null
,代表这是一次重入,将obj
设置为null
后continue
即可 - 假如
Lock record
的Markword
不为null
,则运用CAS
指令将目标头的markword
与目标目标头的markword
进行替换,假如成功则康复为无锁状况,假如失败则膨胀为重量级锁
Markword 记录
开端时的状况
替换后的状况
倾向锁
背景:轻量级锁在没有竞赛的时分,每次重入都需求进行 CAS
操作
Java 6
中 引入倾向锁来做进一步的优化:只需第一次 操才运用 CAS
将线程 ID 设置到目标的 markword
头,之后发现这个线程 ID 是自己就不会产生竞赛,不用重新 CAS
,今后只需不发生竞赛,这个目标就归这个线程一切
代码示例:
public class Thread5 {
private static final Object object = new Object();
public static void method1() {
synchronized (object) {
method2();
}
}
public static void method2() {
synchronized (object) {
method3();
}
}
public static void method3() {
synchronized (object) {
}
}
}
总结
Java 中的 Synchronize 有倾向锁、轻量级锁、重量级锁三种方式,分别对应了锁只被一个线程持有、不同线程替换持有锁、多线程竞赛的情况
重量级锁:底层运用 Monitor
完成,里面触及到了用户态和内核态的转换、进程的上下文切换,本钱较高,性能比较低
轻量级锁:线程加锁时刻是错开的(也就是没有竞赛),可以用轻量级锁来优化,轻量级修正了目标头的锁标志,相对重量级锁性能提升了许多,每次修正都是 CAS
操作,保证原子性
倾向锁:一段很长的时刻内都只被一个线程运用锁,可以运用倾向锁,第一次取得锁时,会有一个 CAS
操作,之后该线程再获取锁,只需求判断 mark word
中是否是自己的线程 id 即可,而不是开销相对较大的 CAS 指令