轻量级锁

轻量级锁是 JDK 6 时加入的新型锁机制,它名字中的“轻量级”是相对于使用操作系统互斥量来实现的传统锁而言的,因此传统的锁机制就被称为“重量级”锁。

轻量级锁并不是用来代替重量级锁的,它设计的初衷是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。

对象头

HotSpot 虚拟机的对象头(Object Header)分为两部分:

  • 第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC 分代年龄(Generational GC Age)等。这部分数据的长度在 32 位和 64 位的 Java 虚拟机中分别会占用 32 个或 64 个比特,官方称它为“Mark Word”。这部分是实现轻量级锁和偏向锁的关键。

  • 另外一部分用于存储指向方法区对象类型数据的指针,如果是数组对象,还会有一个额外的部分用于存储数组长度。

由于对象头信息是与对象自身定义的数据无关的额外存储成本,考虑到 Java 虚拟机的空间使用效率,Mark Word 被设计成一个非固定的动态数据结构,以便在极小的空间内存储尽量多的信息。它会根据对象的状态复用自己的存储空间。例如在 32 位的HotSpot虚拟机中,对象未被锁定的状态下,Mark Word 的 32 个比特空间里的 25 个比特将用于存储对象哈希码,4 个比特用于存储对象分代年龄,2 个比特用于存储锁标志位,还有 1 个比特固定为 0(这表示未进入偏向模式)。

对象除了未被锁定的正常状态外,还有轻量级锁定、重量级锁定、GC 标记、可偏向等几种不同状态,这些状态下对象头的存储内容如下图所示:

轻量级锁的加锁过程

在代码即将进入同步块的时候,如果此同步对象没有被锁定(锁标志位为“01”状态),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的 Mark Word 的拷贝(官方为这份拷贝加了一个 Displaced 前缀,即 Displaced Mark Word)。

然后,虚拟机将使用 CAS 操作尝试把对象的 Mark Word 更新为指向 Lock Record 的指针。

  • 如果这个更新动作成功了,即代表该线程拥有了这个对象的锁,并且对象 Mark Word 的锁标志位(Mark Word 的最后两个比特)将转变为“00”,表示此对象处于轻量级锁定状态。

  • 如果这个更新操作失败了,那就意味着至少存在一条线程与当前线程竞争获取该对象的锁。虚拟机首先会检查对象的 Mark Word 是否指向当前线程的栈帧,如果是,说明当前线程已经拥有了这个对象的锁,那直接进入同步块继续执行就可以了,否则就说明这个锁对象已经被其他线程抢占了。

如果出现两条以上的线程争用同一个锁的情况,那轻量级锁就不再有效,必须要膨胀为重量级锁,锁标志的状态值变为“10”,此时 Mark Word 中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也必须进入阻塞状态。

轻量级锁的解锁过程

轻量级锁的解锁过程也同样是通过 CAS 操作来进行的,如果对象的 Mark Word 仍然指向线程的锁记录,那就用 CAS 操作把对象当前的 Mark Word 和线程中复制的 Displaced Mark Word 替换回来:

  • 假如能够成功替换,那整个同步过程就顺利完成了;

  • 如果替换失败,则说明有其他线程尝试过获取该锁,就要在释放锁的同时,唤醒被挂起的线程。

轻量级锁能提升程序同步性能的依据是“对于绝大部分的锁,在整个同步周期内都是不存在竞争的”这一经验法则。

如果没有竞争,轻量级锁便通过 CAS 操作成功避免了使用互斥量的开销;但如果确实存在锁竞争,除了互斥量的本身开销外,还额外发生了 CAS 操作的开销。因此在有竞争的情况下,轻量级锁反而会比传统的重量级锁更慢。

最后更新于