锁的状态
锁的状态一共有四种:无锁、偏向锁、轻量级锁、重量级锁
锁的状态保存在对象的头文件中,以32位JDK为例:
锁状态 | 25 bit | 4bit | 1bit | 2bit | ||
23bit | 2bit | 是否是偏向锁 | 锁标志位 | |||
轻量级锁 | 指向栈中锁记录的指针 | 00 | ||||
重量级锁 | 指向互斥量(重量级锁)的指针 | 10 | ||||
GC标记 | 空 | 11 | ||||
偏向锁 | 线程ID | Epoch | 对象分代年龄 | 1 | 01 | |
无锁 | 对象的hashCode | 对象分代年龄 | 0 | 01 |
锁的初始为无所状态,随着锁的竞争,锁的升级过程为: 偏向锁 ---> 轻量级锁--->重量级锁,锁的升级是单向的,不会出现锁的降级
锁升级过程
(初始:无锁状态)
场景一、线程#1 获取偏向锁
1. 线程#1 访问同步块
2. 当前对象头,锁标志位:01,是否是偏向锁:0
3. CAS更新Mark Word,设置偏向锁标志位1,设置线程ID为线程#1的ID
线程#1成功获取偏向锁,偏向锁获取成功后,线程不会主动释放,即使线程执行完毕
场景二、线程#1拥有偏向锁
1. 线程#1 又要访问同步块
2. 判断锁标志位为01,是偏向锁,判断线程ID是线程#1的ID
3. 执行同步代码
场景三、线程#2 竞争偏向锁
1. 发现当前对象头是偏向锁
2. 判断线程ID是否为线程#2的ID:否
3. 判断线程#1是否还存在,不存在--->场景四,存在--->场景五
场景四、线程#2 竞争偏向锁,线程#1 不存在,线程#2 获取偏向锁
1. 设置偏向锁标志位为0
2. 类似场景一,线程#2 获取偏向锁
场景五、线程#2 竞争偏向锁,线程#1 存在,偏向锁升级为轻量级锁
1. 到达全局安全点(在这个时间点上没有字节码正在执行),线程#1挂起
2. 设置锁标志位为00,将线程#1 的mark word复制到线程栈的lock record中,更新mark word,将mark word指向线程#1中monitor record的指针 (升级为轻量级锁)
3. 继续执行线程#1 的代码
4. 线程#2 自旋获取锁对象
场景六、线程#3竞争轻量级锁
1. 拷贝对象头的mark word到线程栈的lock record中
2. CAS操作将mark word更新为指向lock record的指针,并将Lock record里的owner指针指向object mark word,成功--->3,失败--->4
3. 更新成功,线程#3则获取到了锁
4. 更新失败,检查对象的mark word是否指向当前线程的栈帧,如果是则说明已经拥有该对象锁,直接进入同步快执行。否则轻量级锁膨胀为重量级锁,mark word中存储的是指向重量级锁(互斥量)的指针
轻量级锁CAS操作之前堆栈与对象的状态
轻量级锁CAS操作之后堆栈与对象的状态
重量级锁、轻量级锁和偏向锁之间的转换
总结
锁 | 优点 | 缺点 | 适用场景 |
偏向锁 | 加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距。 | 如果线程间存在锁竞争,会带来额外的锁撤销的消耗。 | 只有一个线程执行同步块 |
轻量级锁 | 竞争的线程不会阻塞,提高了程序的响应速度。 | 如果始终得不到锁竞争的线程使用自旋会消耗CPU。 | 线程交替执行同步块 追求响应时间。 同步块执行速度非常快。 |
重量级锁 | 线程竞争不使用自旋,不会消耗CPU。 | 线程阻塞,响应时间缓慢。 | 追求吞吐量。 同步块执行速度较长。 |