Follow me on GitHub

synchronized 原理(三):偏向锁、轻量级锁

Java 1.6 之前,synchronized 等价于重量级锁,为提升 synchronized 的性能,1.6 新增了偏向锁、轻量级锁,因此目前 synchronized 对应 3 种锁:

  1. 偏向锁
  2. 轻量级锁
  3. 重量级锁

重量级锁与之前相同,基于 Monitor 机制实现;偏向锁、轻量级锁 不涉及 Monitor 和操作系统,不会导致线程在 内核态用户态 的切换,通过 Mark Word 在 JVM 维度实现。

本文将同步代码块、同步方法统称为“同步块”;将 synchronized 用作内置锁的对象称为“锁对象”,包含 thisthis.Class、任意对象 3 种情况。

优化依据 & 适用场景

偏向锁、轻量级锁优化同步性能的依据相同,都是基于经验:

绝大部分锁,在整个同步期间都 不存在竞争,即 只有一个 线程会进入同步块。

它们优化的手段不同:

  • 偏向锁:既然无竞争,消除整个同步;
  • 轻量级锁:既然无竞争,用 CAS 替代互斥量;

偏向锁更加激进,它消除了整个同步,同步块就像没有用 synchronized 修饰一样。

实际上,二者适用的场景也不同:

  • 偏向锁:锁在整个执行期间,仅被同一个线程持有;
  • 轻量级锁:锁在整个执行期间,会被多个线程持有,但它们是在 不同时间段 分别请求锁,不存在竞争;

偏向锁

若 JVM 开启了偏向锁,当锁对象被线程 t 第一次获取时,JVM 将:

  1. 将锁对象的锁标志位设为 01,即偏向模式;
  2. 通过 CAS 操作将线程 t 的 线程 ID 更新到锁对象的 Mark Word 中
    1. 更新成功,则线程 t 再次进入被该锁对象保护的同步块,JVM 不执行任何同步操作,如加锁、解锁等;

若有其他线程尝试获取该锁对象,则偏向模式结束,撤销偏向,并根据锁对象的当前锁定状态,分别:

  • 未偏向锁定:撤销偏向后,进入未锁定状态,锁标志位为 01;
  • 偏向锁定:撤销偏向后,进入轻量级锁定,锁标志位为 00;

轻量级锁

既然不存在竞争,轻量级锁用 CAS 操作 替代操作系统 互斥量 可以避免内核态、用户态切换,效率更高。

若存在竞争,甚至存在剧烈竞争,轻量级锁会膨胀为重量级锁。

加锁

轻量级锁加锁

线程 t 执行到同步块时,若锁对象的 锁标志位 为 01,即处于 未锁定 状态,则 JVM 加锁过程为:

  1. 在线程 t 的 栈帧 中创建一个名为 锁记录(Lock Record)的空间,用于存储锁对象当前 Mark Word 的拷贝;
  2. 使用 CAS 操作尝试将锁对象的 Mark Word 修改为指向栈帧中 Lock Record 的指针
    1. 修改成功,则:
      1. 锁对象的锁标志位变为 00,表示处于轻量级锁定状态;
      2. 线程 t 拥有该锁;
    2. 修改失败,则检查锁对象的 Mark Word 是否指向线程 t 的栈帧:
      1. 若指向,说明 t 已经成功获取锁,直接进入同步快执行;
      2. 不指向,说明该锁对象已经被其他线程抢占,由于有 两个以上 线程竞争该锁,所以:
        1. 轻量级锁 膨胀 为重量级锁,锁标志位变为 10;
        2. 锁对象 Mark Word 存储指向重量级锁,即 Monitor 的指针;
        3. 线程 t 进入阻塞状态;

注意,通过 CAS 修改 Mark Word 时,失败原因只有两种可能:

  • 该线程重复获取轻量级锁:删除刚刚在栈帧中重复创建的 Lock Record
  • 其他线程已经成功获取锁:轻量级锁膨胀为重量级锁

解锁

若 t 离开同步块时,锁对象依旧处于轻量级锁定状态,则 JVM 解锁过程为:

  • 若锁对象的 Mark Word 仍然指向线程 t 栈帧中的 Lock Record,则借助 CAS 操作将锁对象的 Mark Word 替换为栈帧中复制的 Lock Record:
    • 替换成功,则解锁成功,线程 t 离开同步块;
    • 替换失败,说明有其他线程竞争过该锁,锁已经 膨胀 为重量级锁,需:
      • 释放 Monitor;
      • 唤醒 Monitor entry set 中被挂起的线程;

锁状态转换

偏向锁、轻量级锁、重量级锁转换图

根据是否开启偏向锁,synchronized 锁膨胀顺序如下,锁膨胀以后无法“瘦”回来:

  • 偏向锁 -> 轻量级锁 -> 重量级锁
  • 轻量级锁 -> 重量级锁

锁转换的时机:

  • 偏向锁定时,若有其他线程尝试获取锁,即使原偏向线程 目前并未持有锁,偏向也会结束;
  • 有两个线程 同时 竞争轻量级锁时,膨胀为重量级锁;

结合偏向锁、轻量级锁的设计目标场景,这两个时机很容易理解:

  • 偏向锁假设整个运行期间 只有一个 线程会获取锁,当出现第二个线程时,打破了改假设,自然偏向结束;
  • 轻量级锁假设多个线程 分时间段 获取锁,当两个线程 同时 竞争锁时,打破了改假设,自然需要膨胀;

不同锁状态下,Mark Word 存储的数据不同:

  • 未锁定:哈希码、GC 分代年龄
  • 偏向锁:线程 ID + epoch
  • 轻量级锁:指向栈帧 Lock Record 的指针
  • 重量级锁:指向 Monitor 的指针

参考: