虚拟机中的锁膨胀

尽管当下几乎所有的服务器环境都以集群为主,在考虑并发问题的时候通常会使用分布式技术,如 redis 等中间件来维护全局的资源和锁。但是对于一些实例层面上的资源,依旧需要使用传统的锁来维护。所以我觉得,理解 JVM 对锁的处理还是有价值的。

JVM上针对锁的处理(这里只描述内部锁,即 synchronized 的处理情况),除了有自旋锁锁粗化锁消除等简单的自动优化机制(不探讨啦),还可以从锁的维护的角度去看,可以分为偏向锁轻量级锁重量级锁
三者的开销是递增的,演变顺序也是递增的,而且不可逆。
如轻型的锁可以膨胀升级至重型锁,但是不可以从重型的锁降级。

之所以有不同程度的锁的处理方式,可以看一下这一个场景:如果在逻辑上某一时间基本只有一个线程,会访问由某个锁对象控制的同步块,很多时候不存在资源征用的问题。那么在这个时候,很多时候上锁解锁的开销就显得繁琐而低效了。以重量级锁(也是 synchronized 的最终形态)为例,取锁过程需要操作系统的介入,使线程从用户态进入核心态,开销还是挺大的。所以一开始 JVM 会对锁做偏向锁处理,在一些条件下升至轻量级锁乃至重量级锁。

前置概念

在阐述锁膨胀之前,先插入几个内容点,对象头自旋锁

对象头

在 HotSpot 虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。

实例数据内容和对齐填充不在赘述,说说对象头,这就类似一个对象的元信息,其中也分为三部分组成:标记字段 Mark Word,类型指针 Class Pointer,数组长度 Array Length(如果该对象是数组,则记录了数组长度),三组数据分别用一个字长来存储(32位机上为32bit,64位机位64bit)。

  • 类型指针
    在虚拟机加载类的时候,除了会在元空间/方法区存储类信息,也会在堆区生成一个 Class 对象。类型指针,便是为一个实例对象指向 Class 对象的指针。
  • 标记字段
    在这一个字的区域内,描述了很多信息。通常一个对象会有其哈希码,年代标记(经历 GC 次数),锁标识等。但是如果该对象充当了一个上锁对象,情况会有所不同,下文详述。

对象头示意图

自旋锁

通常来说,取锁时候如果未能获得锁,线程会进入阻塞状态(Blocking),同时会让线程进入等待队列/同步块的入口集中,并导致一个上下文切换,出让 CPU 资源。
我们可以实现一个忙轮询的机制来尝试获得锁。比如使用一个状态对象来代替锁,使用一个循环来判断状态是否未可用,如下是代码层面的实现。

1
2
3
4
5
6
7
volatile boolean flag = false;
while(true) {
if(flag) {
// do something
break;
}
}

这可以避免在首次取得锁失败的时候直接线程切出,在同步逻辑处理量较少的时候可以带来明显的效率提升,但是如果如果说“同步块”的处理时间很长,或者在“同步块”内的线程发生异常未能更改状态量,将严重损失性能乃至发生更严重的死锁。

所以自旋锁适合同步逻辑的处理时间很短的场合(几个循环内能拿到锁)

锁膨胀过程中的三个阶段

现在已经了解了对象在堆中的存储形式,以及依靠自旋可以在短时间内减少加锁开销。继续深入 JVM 中不同并发程度下锁的不同的机制。

偏向锁

偏向锁是在 JDK 1.5?1.6 之后对锁进行的优化。先来看一个图:

偏向锁示意图

前文提到过,如果一个线程经常获得锁,且资源争用不严重,那么可以尽量减少取锁的开销。JDK 是这么做的:
在一个线程获得锁的时候,会在栈帧记录中以及锁对象的标记字段中写入线程 id,在该线程进入/离开同步块的时候不需要额外的锁开销,这里甚至不需要 CAS 操作,因为只需要比较标记字段中的线程 id 与自身是否一致。如果在尝试获得锁的时候发现标记字段中的线程 id 与自身不一致,会尝试利用 CAS 操作来争抢这个偏向锁。

补充:偏向锁是默认启用的,但是它在应用程序启动几秒钟之后才激活,如有必要可以使用 JVM 参数来关闭延迟 -XX:BiasedLockingStartupDelay=0。如果你确定自己应用程序里所有的锁通常情况下处于竞争状态,甚至可以通过 JVM 参数关闭偏向锁 -XX:-UseBiasedLocking=false,那么所有的内部锁都会直接进入轻量级锁状态。

轻量级锁

线程在执行同步块之前,JVM 会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的标记字段复制到锁记录中。然后线程尝试使用 CAS 将对象头中的标记字段替换为指向锁记录的指针。如果成功,当前线程获得锁;如果失败,则进行自旋来获取锁,当自旋获取锁仍然失败时,表示竞争严重(两条或两条以上的线程竞争同一个锁),则轻量级锁会膨胀成重量级锁。

轻量级锁膨胀示意图

重量级锁

重量级锁其实才是通常涉及的锁概念,这时候它已经是一个彻底的悲观锁了。在 JVM 中又叫对象监视器,它至少包含一个竞争锁的队列,和一个信号阻塞队列(wait队列),前者负责做互斥,后一个用于做线程同步。

锁关系示意图

参考
  1. 《Java多线程编程实战指南》 黄文海
  2. 《深入理解Java虚拟机》 周志明
  3. https://www.cnblogs.com/wade-luffy/p/5969418.html
  4. https://blog.csdn.net/wolegequdidiao/article/details/45116141
码路加油