在学习锁优化时,对象头(Mark Word) 是必不可缺的一环,因为synchronized 用的锁是存在对象头里的。32位的虚拟机上对象头占64位(8字节),64位的虚拟机上对象头占128位(16字节)[^objectHead];而不同的类型,对象头的布局不太一样:
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 |
由于偏向锁使用了一种直到竞争发生时才会释放的机制,所以当其他线程竞争偏向锁时,持有偏向锁的线程才会去释放锁。
注意:轻量锁会一直保持,唤醒总是发生在轻量锁解锁的时候,因为加锁的时候已经成功CAS操作;而CAS失败的线程,会立即锁膨胀,并阻塞等待唤醒。
本章是对synchronized 在JVM里的各种等级及升级的流程进行了讲解,其中主要是通过控制对象头的一些状态来控制锁的等级。偏向锁通过标记Thread ID 来表示,当前对象已经被对应线程占用;轻量锁则替换Mark Word 为 Lock Record 地址 来表示当前对象被对应线程占用。无论是哪种锁,在不同的场景下有不同的需求,可以参考以下表格做出选择
偏向锁:
轻量锁:
重量锁:
这个是网上找到的关于锁撤销、膨胀等操作的总流程
关于偏向锁的相关JVM设置:-XXBiasedLockingStartupDelay=0表示启动程序几秒钟后激活偏向锁-XXUseBiasedLocking=false表示关闭偏向锁(确定会发生竞争时可以这么设置)?
《Java并发编程的艺术》之synchronized的底层实现原理
原文:https://www.cnblogs.com/codeleven/p/10963092.html