Java偏向锁、轻量级锁、重量级锁 膨胀时的情况
作者:Adsf
链接:https://www.zhihu.com/question/53826114/answer/160222185
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
首先简单说下先偏向锁、轻量级锁、重量级锁三者各自的应用场景:
- 偏向锁:只有一个线程进入临界区;
- 轻量级锁:多个线程交替进入临界区**;**
- 重量级锁:多个线程同时进入临界区。
还要明确的是,偏向锁、轻量级锁都是JVM引入的锁优化手段,目的是降低线程同步的开销。比如以下的同步代码块:
synchronized (lockObject) {
// do something
}
上述同步代码块中存在一个临界区,假设当前存在Thread#1和Thread#2这两个用户线程,分三种情况来讨论:
- 情况一:只有Thread#1会进入临界区;
- 情况二:Thread#1和Thread#2交替进入临界区;
- 情况三:Thread#1和Thread#2同时进入临界区。
上述的情况一是偏向锁的适用场景,此时当Thread#1进入临界区时,JVM会将lockObject的对象头Mark Word的锁标志位设为“01”,同时会用CAS操作把Thread#1的线程ID记录到Mark Word中,此时进入偏向模式。所谓“偏向”,指的是这个锁会偏向于Thread#1,若接下来没有其他线程进入临界区,则Thread#1再出入临界区无需再执行任何同步操作。也就是说,若只有Thread#1会进入临界区,实际上只有Thread#1初次进入临界区时需要执行CAS操作,以后再出入临界区都不会有同步操作带来的开销。
然而情况一是一个比较理想的情况,更多时候Thread#2也会尝试进入临界区。若Thread#2尝试进入时Thread#1已退出临界区,即此时lockObject处于未锁定状态,这时说明偏向锁上发生了竞争(对应情况二),此时会撤销偏向,Mark Word中不再存放偏向线程ID,而是存放hashCode和GC分代年龄,同时锁标识位变为“01”(表示未锁定),这时Thread#2会获取lockObject的轻量级锁。因为此时Thread#1和Thread#2交替进入临界区,所以偏向锁无法满足需求,需要膨胀到轻量级锁。
再说轻量级锁什么时候会膨胀到重量级锁。若一直是Thread#1和Thread#2交替进入临界区,那么没有问题,轻量锁hold住。一旦在轻量级锁上发生竞争,即出现“Thread#1和Thread#2同时进入临界区”的情况,轻量级锁就hold不住了。 (根本原因是轻量级锁没有足够的空间存储额外状态,此时若不膨胀为重量级锁,则所有等待轻量锁的线程只能自旋,可能会损失很多CPU时间)
首先,我们要知道,线程在内核态与用户态之间切换是比较耗资源的。因此,尽可能要减少线程在阻塞、唤醒之间的切换
如果线程A持有线程B需要的锁,线程B觉得我好不容易来一趟,又要让我进入阻塞等待,不知下次几时才能分配到 cpu 资源,我能不能再等等,说不定线程A很快就完事了
cpu 说:可以,但有一个条件,我们不养闲线程,你必须做点事证明自己的用途。这样吧,你就来个自旋舞,给大伙助兴吧
于是,线程B就一直在自旋,直到线程A释放资源,它马上拿到锁资源,开始干活
自旋锁的优点与缺点
优点
自旋锁可以减少CPU上下文的切换,对于占用锁的时间非常短或锁竞争不激烈的代码块来说性能大幅度提升,因为自旋的CPU 耗时明显少于线程阻塞、挂起、再唤醒时两次CPU上下文切换所用的时间
缺点
在持有锁的线程占用锁时间过长或锁的竞争过于激烈时,线程在自旋过程中会长时间获取不到锁资源,将引起CPU的浪费。所以在系统中有复杂锁依赖的情况下不适合采用自旋锁