未完待续~~
MarkWord 与锁
Mark Word 是对象头中一块重要的数据区域,用来给对象做一些标记以实现某些功能,大小跟 JVM 的字(位数)保持一致,在 32 位 JVM 中,Mark Word 长度为 32 bit,在 64 位 JVM 中,Mark Word 长度为 64 bit
关于对象和对象头,请参考 JVM 对象内存布局
为了最大程度利用内存空间,Mark Word 中的数据“结构”不是固定的,根据最后 2~3bit 的不同,Mark Word 中数据表达的含义也不相同,具体分为 5 种
以 64bit 的 JVM 为例,Mark Word 5 种格式及含义
其中最后 2~3bit 用来表示对象的锁状态或 GC 标志
无锁
无锁即当前对象没有加锁,最后 2bit 为 01并且倒数第3bit为 0 则表示当前对象锁状态为无锁,这也是对象的初始状态
无锁状态下, Mark Word 的其他 bit 还记录了对象的 hash 值和分代年龄
hash
对象的 hash 值,在 64 位 JVM 中大小 31bit ,可表示 2147483648 种 hash 值。
Mark Word 中的 hash 值采用延迟加载,只有在使用时才会计算。如果对象没有重写 hashcode()
方法,那么默认是调用 os::random
产生 hashcode, 可以通过 System.identityHashCode()
获取。
在 64 位操作系统中 os::random
产生 hashcode 的规则为 : next_rand = (16807seed) mod (2*31-1)
。
当对象加锁后(偏向、轻量级、重量级),MarkWord 中就没有足够的空间保存 hashCode 了。
在锁状态为重量级锁时,hashCode 会移动到 Monitor 中,关于 Monitor 在讲重量级锁时会介绍。
age
对象的分代年龄,大小 4bit。
除了一些“体积”比较大的对象之外,大部分对象创建出来时都位于新生代,新生代的对象,对象头中的 age 默认为0,当对象每经过一次 Minor GC 并存活下来时,age 就加 1,当 age 达到阈值 时,JVM 会将该对象移动到老年代。默认情况下,并行 GC 的年龄阈值为 15,并发 GC 的年龄阈值为 6
偏向锁
偏向锁的提出
HotSpot 的开发人员在优化 synchronized 关键字的过程中,调查、统计中发现
- 在对象(锁)的生命周期中,大部分时间,锁的竞争激烈程度都是比较低甚至是没有竞争
- 在无竞争的时候,大多数时间,锁都是被同一个线程连续获取释放
如上,大部分时间锁都是被线程a或线程b连续获取释放,小部分时间是线程a和线程b交替获取。
而锁的获取和释放都是比较消耗性能的。某个线程连续获取释放锁,其实是在浪费性能,最好是这段连续的过程只获取释放一次锁,于是就有了偏向锁
偏向锁要解决的问题
偏向锁要解决是锁无竞争的时候,某个线程连续获取/释放锁造成的不必要的性能浪费
偏向锁在 MarkWord 的体现
最后 2bit 为 01并且倒数第3bit为 1 则表示当前对象,锁状态为偏向锁
而对象锁状态为偏向锁时,MarkWord 中最为核心的数据是 JavaThread* 和 epoch
JavaThread*
线程指针,指向持有当前对象锁的线程,大小54bit
之所以是 54bit 是因为启用偏移锁后(默认是启用的,但可以关闭),在创建线程时,线程地址会进行对齐处理,保证低 10 位为 0,也就是 64-10 = 54bit
未偏向状态
当 JVM 开启偏向锁后,如果一个对象被加锁,那么刚开始对象的锁状态一定是偏向锁,并且处于未偏向状态。
所谓的未偏向状态,即 JavaThread* 指针存储的值是 0
已偏向状态
在未偏向状态时,如果有线程来获取锁,则 JVM会将线程地址使用 CAS 拷贝到 JavaThread* 中,这样就相当于建立了线程和锁的绑定关系。此时偏向锁位于已偏向状态
如果使用 CAS 设置 JavaThread* 失败,则说明发生了锁竞争,偏向锁会升级成轻量级锁
值得注意的是,获得偏向锁的线程在操作完资源之后,不会主动释放锁(不会将 JavaThread* 指针改为 0),这样设计的原因前面也说了,为了提高同一线程连续获取/释放锁的性能。
那这把锁难道一生就只能被最先获取到的线程使用吗?当然不是,当有其他线程尝试获取锁时,会检查 JavaThread* 中的线程还有没有在使用锁,如果线程已经使用完了或者线程已经运行结束,那么JVM 就会进行释放操作
这种等到其他线程要使用锁时,才检查锁状态并进行释放的行为,被称为偏向锁的延迟释放策略
已偏向状态下,有线程来获取锁时,JVM 会比较线程地址与JavaThread* 中的地址是否一致,一致则说明是同一个线程,就不需要重复执行获取锁的操作,这样就可以减少获取锁的性能开销。
但如果不一致,情况就比较复杂了,有两种可能
- JavaThread* 中的线程正在使用锁:这种情况相当于新线程是在竞争锁,有竞争的情况下就不适合在用偏向锁了,JVM 会先对偏向锁进行撤销操作将对象恢复到无锁状态,然后再升级成轻量级锁
- JavaThread* 中的线程没有在使用锁(线程已经使用完了锁或者线程已经运行结束) : 这时,就需要将锁重新偏向到另一个线程
这里面就涉及到一个问题,怎么判断 JavaThread* 中的线程是否正在使用锁?
HotSpot 给的方案是 Epoch
Epoch
Epoch 翻译过来叫纪元,大小 2bit。其实就是一个数字,偏向锁在重新偏向另一个线程时,用于判断之前的线程持有锁的过程是否结束
HotSpot 为所有加载的类型,在 class 元数据 —— InstanceKlass
中保留了一个 MarkWord 原型 —— mark_prototype
。而 mark_prototype 中包含了 epoch 。 class 的新对象创建时,会直接拷贝这个mark_prototype
。
在批量重偏向 (bulk rebias) 的操作中,prototype 的 epoch 位将会被更新;在批量吊销 (bulk revoke) 的操作中,prototype 将会被置成不可偏向的状态 ——bias 位被置 0。
Epoch 与重偏向
重新偏向
批量重偏向/撤销
偏向锁的原理
偏向锁并不会主动释放,这样每次偏向锁进入的时候都会判断改资源是否是偏向自己的,如果是偏向自己的则不需要进行额外的操作,直接可以进入同步操作
只需要用一个 CAS 操作和简单地判断比较,就可以让一个线程拥有锁。
Java 从1.6 开始引入偏向锁并默认保持开启状态
其核心思想在于,可以让同一个线程一直拥有同一个锁,直到出现竞争,才去释放锁。
当有另外一个线程去尝试获取这个锁时,偏向模式就宣告结束。根据锁对象目前是否处于被锁定的状态,撤销偏向(Revoke Bias)后恢复到未锁定(标志位为 “01”)或轻量级锁定(标志位为 “00”)的状态,后续的同步操作就如上面介绍的轻量级锁那样执行
锁撤销
偏向锁的撤销在上述第四步骤中有提到。偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。
偏向锁有一个不好的点就是,一旦出现多线程竞争,需要升级成轻量级锁,是有可能需要先做出销撤销的操作。
而销撤销的操作,相对来说,开销就会比较大,其步骤如下:
- 在一个安全点停止拥有锁的线程,就跟开始做 GC 操作一样。
- 遍历线程栈,如果存在锁记录的话,需要修复锁记录和 Markword,使其变成无锁状态。
- 唤醒当前线程,将当前锁升级成轻量级锁。
偏向锁只有在出现竞争的时候才会释放。在一个线程尝试获取偏向锁时,JVM 会对比该线程 ID 与锁对象头中的线程 ID,相同则说明该线程已经持有该锁,直接执行同步方法;不同则通过 CAS 操作改变对象头中的线程 ID。可见与重量级锁相比,偏向锁降低了用户态与内核态切换的开销
虽然偏向锁起到了优化作用,但是也有弊端
在并发程度相对较大的程序中,CAS 改变对象头线程 ID 有可能失败,这时会导致撤销偏向锁,并尝试将偏向锁升级为轻量级锁。偏向锁的撤销操作会在全局安全点(safepoint)暂停当前持有该锁的线程,如果此时该线程没有执行同步方法,则该线程释放锁,同时其他线程重新竞争对象锁,否则将锁升级。在这里频繁地撤销偏向锁会导致线程长时间暂停,也是很大的开销
撤销偏向锁会让持锁线程暂停,是因为当 CAS 改变对象头线程 ID 失败后,需要根据持锁线程状态判断,是将锁变为无锁状态还是升级为轻量级锁。如果不暂停持锁线程,可能判断线程状态的时候还是运行状态,决定将锁对象升级为偏向锁;但升级之前线程又结束了,需要释放锁,这样会产生冲突锁头状态的冲突
所以当线程暂停的时间大于偏向锁带来的用户态与内核态切换节省下来的时间时,我们需要禁用偏向锁优化
偏向锁的使用
在 JDK1.6 以后默认已经开启了偏向锁这个优化,我们可以通过在启动 JVM 的时候加上 - XX:-UseBiasedLocking 参数来禁用偏向锁(在存在大量锁对象的创建并高度并发的环境下禁用偏向锁能够带来一定的性能优化)。
但 JVM 会延迟去启动偏向锁,延迟四秒,此时对于系统的前几秒来说,等价于没有开启偏向锁,解决方案有 2 种:
加 sleep 5s 再试,放开代码中的语句块 TimeUnit.SECONDS.sleep(5); 1
# 是否延迟开启偏向锁,默认为 1 表示 true
-XX:BiasedLockingStartupDelay=0
偏向锁可以提高带有同步但无竞争的程序性能。它同样是一个带有效益权衡(Trade Off)性质的优化,也就是说它并不一定总是对程序运行有利,如果程序中大多数的锁都总是被多个不同的线程访问,那偏向模式就是多余的。
参考
Evaluating and improving biased locking in the HotSpot virtual machine