介绍
轻量级锁是JDK1.6之中加入的新型锁机制,它名字中的“轻量级”是相对于使用操作系统互斥量来实现的传统锁而言的,因此传统的锁机制就称为“重量级”锁。首先需要强调一点的是,轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。
轻量级锁能够提升程序性能的依据是“对绝大部分的锁,在整个同步周期内都不存在竞争”,注意这是经验数据。需要了解的是,轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁。
引入轻量级锁的目的
轻量级锁的目标是,减少无实际竞争情况下,使用重量级锁产生的性能消耗,包括系统调用引起的内核态与用户态切换、线程阻塞造成的线程切换等。
顾名思义,轻量级锁是相对于重量级锁而言的。使用轻量级锁时,不需要申请互斥量,仅仅将Mark Word中的部分字节CAS更新指向线程栈中的Lock Record,如果更新成功,则轻量级锁获取成功,记录锁状态为轻量级锁;否则,说明已经有线程获得了轻量级锁,目前发生了锁竞争(不适合继续使用轻量级锁),接下来膨胀为重量级锁。
优点
竞争的线程不会阻塞,使用自选,提高程序响应速度。
缺点
如果一直不能获取到锁,长时间的自旋会造成CPU消耗。
适用场景
适用于少量线程竞争锁对象,且线程持有锁的时间不长,追求响应速度的场景。
工作流程

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

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

- 对象已被轻量级锁定。
当对象已经被轻量级锁定的时候,会判断是否是锁重入,如果是重入的话,会记录一条Displaced Mark Word为空的Lock Record。如果不是重入,会膨胀为重量级锁。需要注意的是,即使膨胀为重量级锁,没有获取到锁的线程也不会马上阻塞,而是通过适应性自旋尝试获取锁,当自旋次数达到临界值后,才会阻塞未获取到的线程。JVM认为获取到锁的线程大概率会很快的释放锁,这样做是为了尽可能的避免用户态到内核态的切换。
加锁过程
判断对象是否是无锁状态(低三位 = 001),如果是,执行 2,如果不是,执行 4。
在栈中建立一个Lock Record,将无锁状态的Mark Word拷贝到锁记录的Displaced Mark Word中,将owner指向当前对象。
尝试通过CAS 将锁对象的 Mark Word 更新为指向Lock Record的指针,如果更新成功,该线程获取到轻量级锁,并且需要把对象头的Mark Word的低两位改成10(注意这里修改的是对象头的Mark Word,Lock Record中记录的还是无锁状态的Mark Word);如果更新失败,执行 4。
对象是轻量级锁定状态,判断对象头的 Mark Word是否指向当前线程的栈帧。如果是,则这次为锁重入,将刚刚建立的Lock Record中的Displaced Mark Word设置为null,记录重入,该线程重入轻量级锁。如果不是,执行 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,记录线程重入锁。

非重入锁
如果指向的不是当前线程的栈帧则会触发锁膨胀,膨胀为重量级锁。
解锁过程
轻量级锁加锁时有锁重入的可能,同样的,在解锁时也需要判断是否是锁重入解锁。
检索当前线程栈中的锁记录空间,从低位往高位找到第一条和此对象有关的Lock Record。加锁时,如果是锁重入,会将 Displaced Mark Word 设置为 null,相应的,在解锁时需要判断Displaced Mark Word是否为 null,如果是,则说明是锁重入解锁,移除onwer的指向,不做替换操作;如果不是,执行 2。
通过CAS把当前线程栈帧Lock Record中的Displaced Mark Word替换到对象头的Mark Word中去,如果替换成功,则轻量级解锁成功;如果替换失败,则说明发生了锁膨胀,对象现在是重量级锁定状态,执行 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
轻量级锁可以看作是偏向锁重偏向的升级版,加入了有锁到无锁的状态转换,即使当竞争产生时升级到重量级锁,也不会马上阻塞线程,而是通过适应性自旋来决定是否阻塞,提高了性能。轻量级锁相较于偏向锁来说,简单一些,下一篇会着重介绍重量级锁,以及自旋的线程是怎样进入重量级锁的等待队列的。











