JUC-轻量级锁

介绍

轻量级锁是JDK1.6之中加入的新型锁机制,它名字中的“轻量级”是相对于使用操作系统互斥量来实现的传统锁而言的,因此传统的锁机制就称为“重量级”锁。首先需要强调一点的是,轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。

轻量级锁能够提升程序性能的依据是“对绝大部分的锁,在整个同步周期内都不存在竞争”,注意这是经验数据。需要了解的是,轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁。

引入轻量级锁的目的

轻量级锁的目标是,减少无实际竞争情况下,使用重量级锁产生的性能消耗,包括系统调用引起的内核态与用户态切换、线程阻塞造成的线程切换等。

顾名思义,轻量级锁是相对于重量级锁而言的。使用轻量级锁时,不需要申请互斥量,仅仅将Mark Word中的部分字节CAS更新指向线程栈中的Lock Record,如果更新成功,则轻量级锁获取成功,记录锁状态为轻量级锁;否则,说明已经有线程获得了轻量级锁,目前发生了锁竞争(不适合继续使用轻量级锁),接下来膨胀为重量级锁

优点

竞争的线程不会阻塞,使用自选,提高程序响应速度。

缺点

如果一直不能获取到锁,长时间的自旋会造成CPU消耗。

适用场景

适用于少量线程竞争锁对象,且线程持有锁的时间不长,追求响应速度的场景。

工作流程

入口

轻量级锁的进入方式有三种

  • 对象处于未锁定不可偏状态。此状态下对象不能进入偏向锁模式,当有线程尝试获取锁时,会通过轻量级锁的方式获取锁。

  • 对象锁已经偏向于线程(不考虑重偏向情况)。当锁已经偏向于线程,且线程处于锁定状态或处于未锁定但不允许重偏向的情况下,其它的线程尝试获取锁时,会触发偏向锁撤销,然后升级为轻量级或重量级锁定。

  • 对象已被轻量级锁定。

当对象已经被轻量级锁定的时候,会判断是否是锁重入,如果是重入的话,会记录一条Displaced Mark Word为空的Lock Record。如果不是重入,会膨胀为重量级锁。需要注意的是,即使膨胀为重量级锁,没有获取到锁的线程也不会马上阻塞,而是通过适应性自旋尝试获取锁,当自旋次数达到临界值后,才会阻塞未获取到的线程。JVM认为获取到锁的线程大概率会很快的释放锁,这样做是为了尽可能的避免用户态到内核态的切换。

加锁过程

  1. 判断对象是否是无锁状态(低三位 = 001),如果是,执行 2,如果不是,执行 4。

  2. 在栈中建立一个Lock Record,将无锁状态的Mark Word拷贝到锁记录的Displaced Mark Word中,将owner指向当前对象。

  3. 尝试通过CAS 将锁对象的 Mark Word 更新为指向Lock Record的指针,如果更新成功,该线程获取到轻量级锁,并且需要把对象头的Mark Word的低两位改成10(注意这里修改的是对象头的Mark Word,Lock Record中记录的还是无锁状态的Mark Word);如果更新失败,执行 4。

  4. 对象是轻量级锁定状态,判断对象头的 Mark Word是否指向当前线程的栈帧。如果是,则这次为锁重入,将刚刚建立的Lock Record中的Displaced Mark Word设置为null,记录重入,该线程重入轻量级锁。如果不是,执行 5。

  5. 线程获取轻量级锁失败,锁膨胀为重量级锁,对象头的Mark Word改为指向重量级锁monitor的指针。获取失败的线程不会立即阻塞,先适应性自旋,尝试获取锁。到达临界值后,阻塞该线程,直到被唤醒。

关于适应性自旋

自适应意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。

图解

无锁状态时获取锁的流程

线程执行到同步块时,同步对象处于无锁状态,锁标志位为01,偏向标志位为0,偏向锁被禁用,对象处于无锁态。

在加锁前,虚拟机需要在当前线程的栈帧中建立锁记录(Lock Record)的空间。Lock Record 中包含一个 _displaced_header 属性,用于存储锁对象的 Mark Word 的拷贝。

将锁对象的 Mark Word 复制到锁记录中,这个复制过来的记录叫做 Displaced Mark Word。具体来讲,是将 mark word 放到锁记录的 _displaced_header 属性中,将Owner指向当前对象。

虚拟机使用 CAS 操作尝试将锁对象的 Mark Word 更新为指向锁记录的指针。如果更新成功,这个线程就获得了该对象的锁。

更新成功后,需要修改原对象头Mark Word中的锁状态标志位为00,目的是告诉其它线程此对象已经被轻量级锁定。

重入锁流程

当对象处于加锁状态时,会去检验Mark Word是否指向当前线程的栈帧,如果是则将刚刚建立的Lock Record中的Displaced Mark Word设置为null,记录线程重入锁。

非重入锁

如果指向的不是当前线程的栈帧则会触发锁膨胀,膨胀为重量级锁。


解锁过程

轻量级锁加锁时有锁重入的可能,同样的,在解锁时也需要判断是否是锁重入解锁。

  1. 检索当前线程栈中的锁记录空间,从低位往高位找到第一条和此对象有关的Lock Record。加锁时,如果是锁重入,会将 Displaced Mark Word 设置为 null,相应的,在解锁时需要判断Displaced Mark Word是否为 null,如果是,则说明是锁重入解锁,移除onwer的指向,不做替换操作;如果不是,执行 2。

  2. 通过CAS把当前线程栈帧Lock Record中的Displaced Mark Word替换到对象头的Mark Word中去,如果替换成功,则轻量级解锁成功;如果替换失败,则说明发生了锁膨胀,对象现在是重量级锁定状态,执行 3。

  3. 执行重量级锁释放流程,释放重量级锁,同时唤醒被阻塞的线程去获取锁。

图解

非重入释放锁

解锁的思路是使用 CAS 操作把当前线程的栈帧中的 Displaced Mark Word 替换回锁对象中去,如果替换成功,则解锁成功。

CAS成功

CAS失败


轻量级锁未释放前被其它线程尝试获取,此时Mark Word指针已经被替换为指向Monitor,释放锁时CAS会失败,此时需要走重量级解锁流程。

重入锁释放

加锁时,如果是锁重入,会将 Displaced Mark Word 设置为 null。相对应地,解锁时,如果判断 Displaced Mark Word 为 null 则说明是锁重入,不做替换操作。

归纳总结

轻量级锁的适用场景

少量的线程竞争锁,且所有者线程占用锁的事件补偿,追求响应速度的场景。

什么时候会升级为轻量级锁

当对象的偏向模式被关闭、对象处于已偏向已锁定、已偏向未锁定但不支持重偏向的场景下,就会升级为轻量级锁。

什么时候会升级为重量级锁

当竞争产生时就会升级为重量级锁,比如,两个线程同时获取锁,成功的线程会获取到轻量级锁,失败的线程会执行锁膨胀,升级为重量级锁。

轻量级锁怎样实现锁重入

当轻量级锁已经被线程持有,且对象头的Mark Word指向的是当前线程的栈帧时,会把本条Lock Record的Displaced Mark Word 设置为 null,实现锁重入。当重入解锁时,只需要修改所有者onwer的指向。

轻量级锁是否会自旋

轻量级锁流程不会自旋,自旋发生在产生竞争后,获取失败的线程将锁膨胀为重量级锁。失败的线程不会立刻阻塞,而是先尝试适应性自旋,等待所有者释放锁,当到达临界值后再阻塞。

End

轻量级锁可以看作是偏向锁重偏向的升级版,加入了有锁到无锁的状态转换,即使当竞争产生时升级到重量级锁,也不会马上阻塞线程,而是通过适应性自旋来决定是否阻塞,提高了性能。轻量级锁相较于偏向锁来说,简单一些,下一篇会着重介绍重量级锁,以及自旋的线程是怎样进入重量级锁的等待队列的。

文章作者: GeYu
文章链接: https://nuistgy.github.io/2023/04/13/JUC-轻量级锁/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Yu's Blog