Synchronize 底层原理总结
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 命令
转载自:https://juejin.cn/post/7244174817678147639