Synchronized 锁

升级过程

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

锁标记

偏向锁

偏向锁的思想是偏向于让第一个获取锁对象的线程,这个线程之后重新获取该锁不再需要同步操作

  • 当锁对象第一次被线程获得的时候进入偏向状态,标记为 101,同时使用 CAS 操作将线程 ID 记录到 Mark Word。如果 CAS 操作成功,这个线程以后进入这个锁相关的同步块,查看这个线程 ID 是自己的就表示没有竞争,就不需要再进行任何同步操作

  • 当有另外一个线程去尝试获取这个锁对象时,偏向状态就宣告结束,此时撤销偏向后恢复到未锁定或轻量级锁状态

加锁

  1. 线程第一次进入 synchronized (lock)
    1. 读取对象 Mark Word,判断是无偏向(偏向位 = 0);
    2. 执行 CAS,将当前线程 ID + Epoch 写入 Mark Word;
    3. CAS 成功:对象变成偏向锁状态,绑定当前线程;
    4. 同步代码执行完毕,偏向锁不会主动释放线程 ID(不会清空)。
  2. 同一线程再次进入同步块(核心优势)
    无需 CAS,只做两步判断:
    1. Mark Word 里存储的线程 ID == 当前线程 ID;
    2. Epoch 匹配;
      校验通过,直接进入同步代码,无任何原子操作,性能极高。

撤销锁

  • 场景 1:原偏向线程已退出同步块(无活动)

    JVM 到达安全点(STW);

    暂停所有线程,遍历持有偏向锁的线程栈,确认原线程无同步代码;

    清空 Mark Word 中的线程 ID,恢复为无偏向状态;

    第二个线程通过 CAS 抢占,升级为轻量级锁,走自适应自旋逻辑。

  • 场景 2:原偏向线程还持有锁(两个线程同时竞争)

    STW 安全点暂停所有线程;

    检查原线程栈帧,找到锁记录,撤销偏向;

    将锁直接升级为轻量级锁;

    两个线程通过 CAS 竞争轻量级锁,失败线程进入自旋。

批量重偏向

频繁撤销锁非常耗性能,所以设计了批量重偏向

在单次撤销次数 < 20 时,每次锁竞争都会伴随一次撤销锁的操作(无论是串行竞争还是并行竞争)

当同一个类的对象发生超过 20 次偏向竞争(无论串行还是并行)后,会触发批量重偏向,重偏向会重置对象的 Thread ID,当新线程进行串行竞争时,只校验对象头,不用 STW 撤销,直接把对象偏向新线程。

阈值参数:-XX:BiasedLockingBulkRebiasThreshold=20

批量撤销

同一个类在触发了批量重偏向后,25s 内发生了超过 40 次偏向撤销(批量重定向后,串行竞争已经不会触发撤销操作,所以这里是发生了 40 次并发竞争),JVM 判断该类锁竞争激烈

直接永久关闭这个类所有对象的偏向锁,后续新建对象默认无偏向,直接走轻量级锁

阈值参数:-XX:BiasedLockingBulkRevokeThreshold=40

轻量级锁

两个线程交替用锁,无长时间阻塞

线程在自己的栈帧创建 Lock Record,把对象头复制到 Lock Record 中,通过 CAS 把对象头换成指向自己 Lock Record 的指针

成功:拿到锁

失败:将锁升级为重量级锁,然后进入 cxq 链表进行自旋,自旋成功则获取锁,自旋多次失败则会进入阻塞状态,等待唤醒

过程

  1. 创建锁记录(Lock Record)对象,每个线程的栈帧都会包含一个锁记录的结构,存储锁定对象的 Mark Word
  1. 让锁记录中 Object reference 指向锁住的对象,并尝试用 CAS 替换 Object 的 Mark Word,将 Mark Word 的值存入锁记录

  2. 如果 CAS 替换成功,对象头中存储了锁记录地址和状态 00(轻量级锁) ,表示由该线程给对象加锁

  3. 如果 CAS 失败,有两种情况:

    • 如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程
    • 如果是线程自己执行了 synchronized 锁重入,就添加一条 Lock Record 作为重入的计数
  4. 当退出 synchronized 代码块(解锁时)

    • 如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减 1
    • 如果锁记录的值不为 null,这时使用 CAS 将 Mark Word 的值恢复给对象头
      成功,则解锁成功
      失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程

锁膨胀

在尝试加轻量级锁的过程中,CAS 操作无法成功,可能是其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁

  1. 当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁

  2. Thread-1 加轻量级锁失败,进入锁膨胀流程:为 Object 对象申请 Monitor 锁,通过 Object 对象头获取到持锁线程,将 Monitor 的 Owner 置为 Thread-0,将 Object 的对象头指向重量级锁地址,然后自己进入 Monitor 的 EntryList BLOCKED

  3. 当 Thread-0 退出同步块解锁时,使用 CAS 将 Mark Word 的值恢复给对象头失败,这时进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程

重量级锁

monitor

每个 Java 对象都可以关联一个 Monitor 对象,Monitor 也是 class,其实例存储在堆中,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的 Mark Word 中就被设置指向 Monitor 对象的指针,这就是重量级锁

结构

过程

  1. 开始时 Monitor 中 Owner 为 null
  2. 当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor 中只能有一个 Owner,obj 对象的 MarkWord 指向 Monitor
  3. 在 Thread-2 上锁的过程,Thread-3、Thread-4、Thread-5 也执行 synchronized(obj),就会进入 EntryList BLOCKED(双向链表)
  4. Thread-2 执行完同步代码块的内容,根据 obj 对象头中 Monitor 地址寻找,设置 Owner 为空,把线程栈的锁记录中的对象头的值设置回 MarkWord
  5. 唤醒 EntryList 中等待的线程来竞争锁,竞争是非公平的,如果这时有新的线程想要获取锁,可能直接就抢占到了,阻塞队列的线程就会继续阻塞
  6. WaitSet 中的 Thread-0,是以前获得过锁,但条件不满足进入 WAITING 状态的线程

自旋优化

重量级锁竞争时,尝试获取锁的线程不会立即阻塞,可以使用自旋来进行优化,采用循环的方式去尝试获取锁

优点:不会进入阻塞状态,减少线程上下文切换的消耗

缺点:当自旋的线程越来越多时,会不断的消耗 CPU 资源

抢锁成功:

抢锁失败:

注意:

  • 自旋占用 CPU 时间,单核 CPU 自旋就是浪费时间,因为同一时刻只能运行一个线程,多核 CPU 自旋才能发挥优势
  • 自旋失败的线程会进入阻塞状态
  • 在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋
  • Java 7 之后不能控制是否开启自旋功能,由 JVM 控制