《Java并发编程的艺术》之synchronized的底层实现原理

时间:2021-09-09 15:56:41

在学习锁优化时,对象头(Mark Word) 是必不可缺的一环,因为synchronized 用的锁是存在对象头里的。32位的虚拟机上对象头占64位(8字节),64位的虚拟机上对象头占128位(16字节)[^objectHead];而不同的类型,对象头的布局不太一样:

  • 数组类型:Mark Word、Class Metadata Address、Array Length
  • 普通类型:Mark Word、Class Metadata Address

Mark Word 表示对象的HashCode锁信息
Class Metadata Address 表示对象的数据类型在方法区对应的地址
Array Length 表示数组的长度(只在对象是数组的情况下才会存在)

对象头的默认表示应该如下所示

锁状态 25bit 4bit 1bit是否是偏向锁 2bit 锁标志位
无状态锁 对象的hashcode 对象分代年龄 0 01

具体的对象内存布局看这篇文章

而根据JVM的设置1,具体分配时又会有不同的情况,如下所示

《Java并发编程的艺术》之synchronized的底层实现原理

当关闭了偏向锁的设置,那么就会走左边的流程;反之则走右边的流程。

偏向锁

由于大多数情况下,锁大多都不处于多线程竞争状态,而且总是由同一个线程获取,所以JVM在1.6之后加入了偏向锁轻量锁 ,如今总共由4种锁状态:无状态锁偏向锁轻量锁重量锁。随着线程竞争的提升,锁会逐渐升级(无法降级)。
偏向锁在没有竞争的情况下可以提高同步的性能,这方面主要体现在偏向锁只需要进行一次CAS而轻量锁需要两次。它是一个需要权衡利弊的选择,它不是在任何情况下都对程序有利的。如果竞争很多,那么撤销偏向锁的过程就会成为性能瓶颈。

当偏向锁可用时,初始化的对象头分配如下所示

锁状态 23bit 2bit 4bit 1bit 是否是偏向锁 2bit 锁标志位
偏向锁 线程ID epoch 对象分代年龄 1 01

加锁过程

  1. 当对象头的isBiased 为1时且锁状态为01时,偏向锁可用,继续后面的流程
  2. 判断目标对象头是否包含本线程ID,如果没有,则直接CAS往对象头里写入本线程ID。到这一步加锁就结束了

锁撤销

由于偏向锁使用了一种直到竞争发生时才会释放的机制,所以当其他线程竞争偏向锁时,持有偏向锁的线程才会去释放锁。

  1. 等待原持有偏向锁的线程(后文简称原线程)运行至全局安全点(safe point)
  2. 暂停原线程
  3. 检查原线程 的线程状态,如果退出了同步代码块,则重偏向;反之升级为轻量锁
  4. 恢复原线程

轻量锁

加锁过程

注意:轻量锁会一直保持,唤醒总是发生在轻量锁解锁的时候,因为加锁的时候已经成功CAS操作;而CAS失败的线程,会立即锁膨胀,并阻塞等待唤醒。
《Java并发编程的艺术》之synchronized的底层实现原理

  1. 第一次进入同步块,开辟一个叫做Lock Record 的空间用于存储锁记录
    《Java并发编程的艺术》之synchronized的底层实现原理
  2. 将对象头中的Mark Word 复制到 当前线程栈中
  3. 尝试用CAS将Mark Word 替换指向Lock Record的指针 《Java并发编程的艺术》之synchronized的底层实现原理
  4. 第三步操作成功,则将Mark Word 设置为00状态,标识轻量锁
  5. 然后执行同步体
  6. 第三部操作失败,进入自旋获取锁
  7. 自旋获取锁的失败次数到达阈值,膨胀锁,修改为重量级锁(状态改为10
  8. 线程阻塞

锁释放过程

  1. 尝试CAS将Lock Record的Owner 复制回 Mark Word
  2. 如果CAS操作成功,则表示没有竞争发生;否则看步骤3
  3. 释放锁并唤醒等待的线程

总结

本章是对synchronized 在JVM里的各种等级及升级的流程进行了讲解,其中主要是通过控制对象头的一些状态来控制锁的等级。偏向锁通过标记Thread ID 来表示,当前对象已经被对应线程占用;轻量锁则替换Mark WordLock Record 地址 来表示当前对象被对应线程占用。无论是哪种锁,在不同的场景下有不同的需求,可以参考以下表格做出选择

偏向锁:

  • 优点:加锁和解锁不需要额外消耗,和执行非同步方法相比,仅存在纳秒级的差距
  • 缺点:如果线程间存在竞争,会带来额外开销(偏向锁的撤销)
  • 适用场景: 适用于只有一个线程访问同步块的场景

轻量锁:

  • 优点: 竞争的线程不会造成阻塞,提高了程序的响应速度
  • 缺点: 如果始终得不到锁,使用自旋会消耗CPU
  • 适用场景: 追求相应实践,同步块执行速度非常快

重量锁:

  • 优点: 线程竞争不使用自选,不会消耗CPU
  • 缺点: 线程阻塞,响应时间缓慢
  • 适用场景: 追求吞吐量,同步块执行速度较慢

这个是网上找到的关于锁撤销、膨胀等操作的总流程
《Java并发编程的艺术》之synchronized的底层实现原理


  1. 关于偏向锁的相关JVM设置:-XXBiasedLockingStartupDelay=0表示启动程序几秒钟后激活偏向锁-XXUseBiasedLocking=false表示关闭偏向锁(确定会发生竞争时可以这么设置)