Java 中的偏向锁、轻量级锁和重量级锁
今天我们来聊聊 Synchronized 里面的各种锁:偏向锁、轻量级锁、重量级锁,以及三个锁之间是如何进行锁膨胀的。
GodsGarden_ZH-CN3317703606_1920x1080众所周知,线程阻塞带来的上下文切换的代价是很大的,Java 为了尽量减少上下文的切换从而引入了更多的锁机制。在了解各种锁机制之前,先要学习一些前置知识。对于各种锁的获取和释放、以及锁升级的流程,在文末总结处有一张图,如果赶时间,直接看图吧。
Mark Word
image-20190719003918974Java 对象头里面有一部分叫做 Mark Word,在32位虚拟机下面占有 32bit,在 64 位虚拟机下面占用 64 bit,本文以 32 位虚拟机为例子。
锁介绍
偏向锁:一个线程反复的去获取/释放一个锁,如果这个锁是轻量级锁或者重量级锁,不断的加解锁显然是没有必要的,造成了资源的浪费。于是引入了偏向锁,偏向锁在获取资源的时候会在资源对象上记录该对象是偏向该线程的,偏向锁并不会主动释放,这样每次偏向锁进入的时候都会判断改资源是否是偏向自己的,如果是偏向自己的则不需要进行额外的操作,直接可以进入同步操作。
轻量级锁:轻量级锁是由偏向所升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁; 在轻量级锁中如果发生多线程竞争,未持有锁的线程会自旋等待。
重量级锁:由操作系统的 mutex 来实现,多线程竞争下,未持有锁的线程将被阻塞。
锁的流转
锁转移对象分配
首先确定该类的偏向锁是否可用(决定了在新建对象实例的时候倒数第三 bit 是 0 还是 1);如果不可用,直接使用轻量锁。如果偏向锁可用,新建实例对象 obj,此时 obj 进入到未锁定、未偏向、可偏向的状态。
偏向锁 初始锁定
此时线程 A 想要获取对象 obj 的偏向锁,由于此时 obj 没有偏向任何线程(有可能是刚刚新建,也有可能是由锁定状态重偏向之后导致的未锁定状态),所以利用 CAS 操作将线程 ID 写入到 Mark Word 里面,此时 线程 A 获取了 obj 的偏向锁。obj 处于已偏向、锁定状态。
偏向锁 锁定/解锁
一旦 obj 第一次偏向了线程 A,A 就可以在没有竞争的情况下,也就是锁不升级的情况下,以极小的代价反复获取 obj 对象的锁。
偏向锁 重偏向(rebias)
如果 obj 先被线程 A 锁定,然后释放。然后线程 B 过来是否能重新获取偏向锁呐?在一定条件下,经过了重偏向可以重新获得偏向锁。
通过上面的 mark word 我们可以看出,在偏向锁的时候有一个字段是 epoch,同时在 obj 的类 O 信息里面也有一个 epoch,每次系统到达安全点会对类的 epoch 加 1,变成 epoch_new,然后扫描所有的类 O 的实例,判断该偏向锁是否还被持有,如果被持有则将 epoch_new 复制给对象头的 epoch 字段。
每次去获取偏向锁的时候回去判断对象实例的 epoch 和 类的 epoch 是否相等,如果不等代表对象是未锁定、可偏向、未偏向状态,可以偏向新的线程。
撤销偏向(revoke)
Once biased, that thread can subsequently lock and unlock the object without resorting to expensive atomic instructions. Obviously, an object can be biased toward at most one thread at any given time. (We refer to that thread as the bias holding thread). If another thread tries to acquire a biased object, however, we need to revoke the bias from the original thread.
线程 B 来竞争,此时偏向锁会膨胀位轻量级锁。当 B 线程想利用 CAS 获取偏向锁失败, A 线程被暂停,然后检查 A 线程;
-
A 线程已经退出了同步代码块,或者是已经不在存活了,如果是上面两种情况之一的,将 obj 的 Mark Word 后 3bit 改为 001(无锁状态),然后再去唤醒 A 线程。
-
A 线程还在同步代码块中,此时将 A 线程的偏向锁升级为轻量级锁。首先会 copy 一份 obj 对象的 mark word 内容,放到线程 A 当前栈帧的一个叫做 Lock Record 空间的 displace mark word 的位置,
然后通过 CAS 将 obj 中 mark word 的内容替换为指向栈帧中 mark word 的位置,最后将 Lock Record 里面的 owner 指向 obj 对象。并且对象Mark Word的锁标志位设置为“00”,到此完成了锁的升级。
20170420102754608Bulk Revoke
Java 会在类信息里面维护一个计数器记录,记录该类发生偏向锁 revoke 的次数,一旦达到某个阈值,则认为这个类不适合偏向锁,将会禁用这个类的偏向锁功能。这个类新建的对象实例的最后 3bit 将是 001(无锁)。
Objects that are explicitly designed to be shared between multiple threads, such as producer/consumer queues, are not suitable for biased locking. Therefore, biased locking is disabled for a class if revocations for its instances happened frequently in the past. This is called bulk revocation. If the locking code is invoked on an instance of a class for which biased locking was disabled, it performs the standard thin locking. Newly allocated instances of the class are marked as non-biasable.
轻量级锁定
刚刚在上文中关于撤销偏向(revoke)中已经描述了锁定的流程。在此再总结一遍
- 复制 mark word 到 displace mark word
- CAS 替换 mark word 中的信息为指针
- 修改 锁 标志为 00
- 将栈帧中的 owner 指向锁定的对象
轻量级锁 递归锁定
image-20190719112951132线程 A 在去获取轻量级锁的时候,会首先使用 CAS 操作,如果操作失败那么会在此时判断是不是该线程已经持有过该对象的锁了,通过判断对象的 mark word 是不是指向当前线程的栈帧,如果是则会在最新的栈帧处新建一个 displaced mark word 为 null 的 lock record。关于为什么要这么做,其实就是为了记录一下锁的重入,发生重入了需要记录,本来是记录到 mark word 里面最方便,可能是 mark word 没有足够的空间。如果是在 lock record里面记录的话,需要遍历 lock record 才可以获取这个数量。所以最终Hotspot选择每次获得锁都添加一个Lock Record
来表示锁的重入。
类似的递归的解锁只需要将栈帧中的 lock records 删除即可。
轻量级锁 膨胀
线程 A 持有对象 obj 的轻量级锁,线程 B 过来 CAS 失败,开始自旋等待,自旋到达一定次数之后如果还没有获取该轻量级锁,则会将该锁膨胀为重量级锁。会将 mark word 的的锁标志为改为10,同时经指针指向互斥量,然后线程 B 挂起。
JDK1.6中 -XX:+UseSpinning开启; -XX:PreBlockSpin=10 为自旋次数。
JDK1.7后,去掉 -XX:PreBlockSpin 参数,由jvm控制。
轻量级锁 解锁
轻量级锁解锁也是理由 CAS 将 mark word 里面的指针替换为无锁的 mark word 信息。需要判断 mark word 里面是指向该线程的 lock record
- 如果不是,说明已经锁膨胀了,CAS 失败,此时需要唤醒在等待重量级锁的线程
- 如果是,说明锁没有膨胀,直接 CAS 操作将 mark word 改为 001(无锁)状态,为下一个线程获取轻量级锁做好准备。
重量级锁 解锁
释放 mutex,唤醒在等待的线程
总结
新标签页打开图片可看大图
v2-9db4211af1be81785f6cc51a58ae6054_r参考
java 中的锁 -- 偏向锁、轻量级锁、自旋锁、重量级锁 - zqz_zqz的博客 - CSDN博客
Java 8 并发篇 - 冷静分析 Synchronized(下) - 知乎
Biased Locking in HotSpot | Oracle David Dice's Blog //rebias
Java中的偏向锁,轻量级锁, 重量级锁解析 - 神评网 //rebias