在学习锁优化时,对象头(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,具体分配时又会有不同的情况,如下所示
当关闭了偏向锁的设置,那么就会走左边的流程;反之则走右边的流程。
偏向锁
由于大多数情况下,锁大多都不处于多线程竞争状态,而且总是由同一个线程获取,所以JVM在1.6之后加入了偏向锁 和 轻量锁 ,如今总共由4种锁状态:无状态锁、偏向锁、轻量锁、重量锁。随着线程竞争的提升,锁会逐渐升级(无法降级)。
偏向锁在没有竞争的情况下可以提高同步的性能,这方面主要体现在偏向锁只需要进行一次CAS而轻量锁需要两次。它是一个需要权衡利弊的选择,它不是在任何情况下都对程序有利的。如果竞争很多,那么撤销偏向锁的过程就会成为性能瓶颈。
当偏向锁可用时,初始化的对象头分配如下所示
锁状态 | 23bit | 2bit | 4bit | 1bit 是否是偏向锁 | 2bit 锁标志位 |
---|---|---|---|---|---|
偏向锁 | 线程ID | epoch | 对象分代年龄 | 1 | 01 |
加锁过程
- 当对象头的isBiased 为1时且锁状态为01时,偏向锁可用,继续后面的流程
- 判断目标对象头是否包含本线程ID,如果没有,则直接CAS往对象头里写入本线程ID。到这一步加锁就结束了
锁撤销
由于偏向锁使用了一种直到竞争发生时才会释放的机制,所以当其他线程竞争偏向锁时,持有偏向锁的线程才会去释放锁。
- 等待原持有偏向锁的线程(后文简称原线程)运行至全局安全点(safe point)
- 暂停原线程
- 检查原线程 的线程状态,如果退出了同步代码块,则重偏向;反之升级为轻量锁
- 恢复原线程
轻量锁
加锁过程
注意:轻量锁会一直保持,唤醒总是发生在轻量锁解锁的时候,因为加锁的时候已经成功CAS操作;而CAS失败的线程,会立即锁膨胀,并阻塞等待唤醒。
- 第一次进入同步块,开辟一个叫做Lock Record 的空间用于存储锁记录
- 将对象头中的Mark Word 复制到 当前线程栈中
- 尝试用CAS将Mark Word 替换为 指向Lock Record的指针
- 第三步操作成功,则将Mark Word 设置为00状态,标识轻量锁
- 然后执行同步体
- 第三部操作失败,进入自旋获取锁
- 自旋获取锁的失败次数到达阈值,膨胀锁,修改为重量级锁(状态改为10)
- 线程阻塞
锁释放过程
- 尝试CAS将Lock Record的Owner 复制回 Mark Word
- 如果CAS操作成功,则表示没有竞争发生;否则看步骤3
- 释放锁并唤醒等待的线程
总结
本章是对synchronized 在JVM里的各种等级及升级的流程进行了讲解,其中主要是通过控制对象头的一些状态来控制锁的等级。偏向锁通过标记Thread ID 来表示,当前对象已经被对应线程占用;轻量锁则替换Mark Word 为 Lock Record 地址 来表示当前对象被对应线程占用。无论是哪种锁,在不同的场景下有不同的需求,可以参考以下表格做出选择
偏向锁:
- 优点:加锁和解锁不需要额外消耗,和执行非同步方法相比,仅存在纳秒级的差距
- 缺点:如果线程间存在竞争,会带来额外开销(偏向锁的撤销)
- 适用场景: 适用于只有一个线程访问同步块的场景
轻量锁:
- 优点: 竞争的线程不会造成阻塞,提高了程序的响应速度
- 缺点: 如果始终得不到锁,使用自旋会消耗CPU
- 适用场景: 追求相应实践,同步块执行速度非常快
重量锁:
- 优点: 线程竞争不使用自选,不会消耗CPU
- 缺点: 线程阻塞,响应时间缓慢
- 适用场景: 追求吞吐量,同步块执行速度较慢
这个是网上找到的关于锁撤销、膨胀等操作的总流程
关于偏向锁的相关JVM设置:-XXBiasedLockingStartupDelay=0表示启动程序几秒钟后激活偏向锁-XXUseBiasedLocking=false表示关闭偏向锁(确定会发生竞争时可以这么设置)↩