sychronized与锁升级

9283 字
19 分钟阅读

在高并发编程领域,线程安全是一个绕不开的话题。Java 通过 synchronized 关键字为我们提供了一种简单易用的内置锁机制,用于保证共享数据在多线程环境下的原子性、可见性和有序性。然而,凡事有利必有弊,锁在带来数据安全的同时,也可能成为性能的瓶颈。

正如开发手册所强调的:

【强制】 高并发时,同步调用应该去考量锁的性能损耗。能用无锁数据结构,就不要用锁;能锁区块,就不要锁整个方法体;能用对象锁,就不要用类锁。
说明: 尽可能使加锁的代码块工作量尽可能的小,避免在锁代码块中调用 RPC 方法。

这背后的核心思想,就是在安全性能之间寻找最佳平衡点。

一、Synchronized 的前世今生:从重量级锁到智能优化

在 Java 的早期版本(JDK 1.5 之前),synchronized 的实现非常“重”。它直接依赖于操作系统底层的互斥量(Mutex Lock)来实现。这意味着,每当线程需要获取或释放锁时,都需要在用户态(User Mode)和内核态(Kernel Mode)之间进行切换。

用户态与内核态切换

这种切换的成本是相当高昂的。操作系统需要保存当前用户线程的上下文信息(如寄存器值、变量等),然后加载内核的上下文来执行线程的阻塞或唤醒操作,完成后再切换回来。如果同步代码块本身执行得非常快,那么线程切换的开销甚至可能超过代码执行本身的开销,导致性能急剧下降。因此,早期的 synchronized​ 被称为重量级锁

为了改善这一状况,从 JDK 1.6 开始,Java 虚拟机(JVM)对 synchronized​ 进行了大刀阔斧的优化,引入了偏向锁(Biased Locking)轻量级锁(Lightweight Locking) 。其核心思想是:锁的竞争程度是动态变化的,我们应该根据实际情况逐步升级锁的级别,而不是一开始就动用最“重”的武器。 这种锁状态的升级过程,就是我们常说的锁膨胀(Lock Inflation)

二、锁的秘密:对象头中的 Mark Word

synchronized​ 锁的实现并不依赖于某个独立的锁对象,而是直接将锁信息记录在了 Java 对象头里。每个 Java 对象都由对象头(Header)和实例数据(Instance Data)组成。对象头中包含一个名为 Mark Word 的区域,它就是实现偏向锁、轻量级锁和重量级锁的关键。

Mark Word 的结构在 64 位 JVM 中通常为 8 字节(64 位),它是一个“多功能”的数据结构,其内容会随着锁状态的改变而改变,实现了空间的复用。

Mark Word 结构与锁升级

从上图可以看出,Mark Word 通过末尾的几位标志位来区分当前对象处于哪种锁状态:

  • 无锁状态(标志位 01): 存储对象的哈希码(HashCode)、GC 分代年龄等信息。
  • 偏向锁状态(标志位 01,但偏向锁位置 1): 存储偏向的线程 ID、Epoch 等信息。
  • 轻量级锁状态(标志位 00): 存储一个指向线程栈中锁记录(Lock Record) 的指针。
  • 重量级锁状态(标志位 10): 存储一个指向监视器(Monitor) 对象的指针。

不同锁状态下的 Mark Word

三、重量级锁的底层基石:Monitor

在深入了解锁升级之前,我们必须先理解重量级锁的底层原理——Monitor,也常被称为“管程”或“监视器锁”。

每个 Java 对象天生就与一个 Monitor 关联。你可以将 Monitor 理解为一个同步工具或机制。当一个对象被 synchronized 锁定时,实际上就是获取了这个对象关联的 Monitor 的所有权。

Monitor 的本质是由 C++ 的 ObjectMonitor​ 类实现的,其核心依赖于操作系统的 Mutex Lock。这就是为什么重量级锁需要进行用户态到内核态切换的根本原因。

// hotspot/src/share/vm/runtime/objectMonitor.hpp
class ObjectMonitor {
  // ...
  volatile markOop _header;       // 锁对象的Mark Word
  void*     _owner;              // 指向持有锁的线程
  WaitSet*  _WaitSet;            // 调用了wait()方法的线程被阻塞在此
  EntryList* _EntryList;         // 等待获取锁的线程被阻塞在此
  // ...
};
‍```

ObjectMonitor 结构

当多个线程同时竞争一个锁时:

  1. 成功获取锁的线程成为 Monitor 的 _owner
  2. 获取失败的线程会被封装成节点,放入 _EntryList 队列中挂起,等待被唤醒。
  3. 这个挂起和唤醒的过程,就需要操作系统介入,因此开销巨大。

总结一下 Monitor、Java 对象和线程的关系:

  • 当一个对象升级为重量级锁时,其 Mark Word 会变成一个指针,指向与之关联的 Monitor 对象。
  • Monitor 的 _owner 字段会记录当前持有该锁的线程 ID。

Monitor 与对象和线程的关联

image

了解了 synchronized 的演进背景、Mark Word 的结构以及重量级锁的底层实现后,我们就为理解锁升级铺平了道路。接下来,我们将由浅入深,详细剖析从无锁到偏向锁、轻量级锁,最终膨胀为重量级锁的全过程。

好的,我们继续。现在我们将深入探讨 synchronized​ 优化的第一级阶梯——偏向锁


四、第一级优化:偏向锁 (Biased Locking) - 为“老顾客”开设的VIP通道

HotSpot 虚拟机的开发者们经过研究发现,在绝大多数情况下,一个锁不仅不存在多线程竞争,而且总是由同一个线程多次获得。为了让这种情况下的性能开销降到最低,偏向锁应运而生。

偏向锁,顾名思义,就是“偏心”的锁,它会偏向于第一个获得它的线程

可以把它想象成一个私人停车位:

第一个开车来的线程(比如线程 A)把车停好,并在车位上挂上自己的名牌(在 Mark Word 中记录线程 ID)。只要没有其他人来抢这个车位,线程 A 之后每次来,看到是自己的名牌,就可以直接停车,无需再办理任何手续(无需 CAS 同步)。

这个“无需办理手续”的过程,就是偏向锁性能提升的关键。在没有竞争的情况下,它甚至消除了 CAS 操作,几乎没有同步开销。

偏向锁的获取流程

  1. 初始状态:一个对象刚被创建时,处于“可偏向”的无锁状态(标志位 01​,但偏向锁位为 0)。

    无锁状态的 Mark Word

  2. 首次加锁:当线程 A 第一次进入该对象的 synchronized​ 代码块时,JVM 会通过 CAS 操作 尝试将该线程的 ID 记录到对象头的 Mark Word 中,并将偏向锁标志位置为 1

    • 成功:线程 A 获得偏向锁。此时 Mark Word 中存储的是偏向线程的 ID。
    • 失败:说明发生了竞争(可能其他线程已经持有了偏向锁,或正在升级),进入后续的锁升级流程。

    偏向锁状态的 Mark Word

  3. 后续访问:当持有偏向锁的线程 A 再次进入同步块时,它会检查 Mark Word 中存储的线程 ID 是否是自己的 ID。

    • :表明它就是这位“老顾客”,可以直接进入同步代码块,无需任何额外的同步操作
    • 不是:说明锁对象偏向了其他线程,发生了竞争。

偏向锁的撤销与升级:当“新顾客”到来时

偏向锁的一个重要特点是:持有偏向锁的线程不会主动释放锁。只有当其他线程尝试获取该锁时,偏向锁才会被撤销(Revoke)

这个撤销过程需要在一个全局安全点(Global Safepoint) 进行,即一个所有线程都暂停执行的时刻。此时,JVM 会根据持有偏向锁的线程(我们称之为线程 A)的状态,决定下一步怎么走:

  1. 情况一:线程 A 还在同步代码块内

    • 其他线程(线程 B)来竞争锁。
    • JVM 发现线程 A 仍然活跃且持有锁。
    • 此时,偏向锁会被撤销,并直接升级为轻量级锁
    • 线程 A 的栈帧中会创建锁记录(Lock Record),对象头的 Mark Word 会指向这个锁记录。线程 A 继续执行。
    • 线程 B 则进入自旋状态,尝试获取这个新的轻量级锁。
  2. 情况二:线程 A 已经退出了同步代码块

    • 其他线程(线程 B)来竞争锁。
    • JVM 发现线程 A 已经不再持有该锁。
    • 这时,锁对象会恢复到无锁状态(或者叫“匿名偏向”状态),其 Mark Word 会被重置。
    • 然后线程 B 可以重新尝试获取锁,并可能将该锁偏向于自己。

偏向锁撤销流程

解惑:偏向锁有竞争后,一定会变成轻量级锁吗?

这正是上面分析的关键点。答案是:不一定

  • 如果竞争发生时,原持有者还在同步块内,那么锁会升级为轻量级锁。
  • 如果竞争发生时,原持有者已经退出了同步块,那么锁会恢复为无锁状态,让新的竞争者去偏向。

偏向锁与 hashCode()的“恩怨”

在无锁状态下,Mark Word 的一部分空间是用来存储对象的 identity hash code 的。而一旦对象进入偏向锁状态,这部分空间就会被用来存储线程 ID。这就导致了一个重要的冲突:

  • 一个已经计算过 identity hash code的对象,无法再进入偏向锁状态。 JVM 会直接跳过偏向锁,在加锁时进入轻量级锁状态。

  • 如果一个对象正处于偏向锁状态,此时收到了计算其 identity hash code​ 的请求(即调用了 System.identityHashCode()​ 或对象的 hashCode() 方法),会发生什么?

    • 偏向锁会立即被撤销。并且,锁会膨胀为重量级锁
    • 为什么直接膨胀为重量级锁,而不是轻量级锁? 因为轻量级锁的 Mark Word 需要指向栈上的锁记录,仍然没有空间存放 hashCode​。而重量级锁的实现 ObjectMonitor​ 中有专门的字段来存放这些信息。为了“安放”这个 hashCode,JVM 选择直接升级到最完备的重量级锁状态。

偏向锁的现状与未来

偏向锁在 JDK 1.6 中被默认开启,但它有一个启动延迟。可以通过 JVM 参数进行配置:

  • XX:+UseBiasedLocking:开启偏向锁(默认开启)。
  • XX:BiasedLockingStartupDelay=0:关闭启动延迟,让程序启动后立即启用偏向锁。
  • XX:-UseBiasedLocking:关闭偏向锁,程序将直接进入轻量级锁状态。

然而,随着技术的发展,偏向锁带来的维护成本(尤其是在全局安全点撤销的复杂性)在某些场景下已经超过了其带来的性能收益。因此,从 JDK 15 开始,偏向锁已被默认禁用并被标记为废弃(JEP 374)

JEP 374: Disable and Deprecate Biased Locking

这意味着,在未来的 Java 版本中,我们将告别偏向锁。但在理解 synchronized 的锁升级历史中,它仍然是不可或缺的一环。

好的,我们继续博客的第三部分,深入探讨 synchronized​ 优化的第二级阶梯——轻量级锁


五、第二级优化:轻量级锁 (Lightweight Locking) - 自旋不息,避免阻塞

当偏向锁的“一人独享”模式被打破,即多个线程开始交替访问同一个同步块时,锁就会升级为轻量级锁。轻量级锁的核心设计思想是:线程之间的竞争和锁占用时间通常非常短暂。与其让线程一上来就进入阻塞状态(这需要昂贵的内核态切换),不如让它“稍等片刻”,进行几次空循环(即自旋),看看持有锁的线程是否会很快释放锁。

可以把轻量级锁想象成一个办公室里的空闲会议室:

大家都遵循一个君子协定。当线程 A 想用会议室时,它会在门口挂上“使用中”的牌子(通过 CAS 修改 Mark Word)。如果线程 B 也想用,它看到牌子后不会立刻走开去排队(阻塞),而是在门口踱步观望一会儿(自旋),因为它相信线程 A 很快就会出来。

这种“自旋”机制,就是轻量级锁避免性能损耗的关键。只要竞争不激烈,线程通过自旋就能成功获取锁,从而避免了重量级锁的系统调用开销。

轻量级锁的加锁过程

当一个线程尝试获取一个已经被撤销了偏向锁的对象时,或者一开始就关闭了偏向锁时,就会进入轻量级锁的加锁流程:

  1. 创建锁记录(Lock Record) :JVM 会在当前线程的栈帧(Stack Frame)中开辟一块名为“锁记录”的空间,用于存放锁对象当前的 Mark Word 的一个拷贝。这被称为 Displaced Mark Word

  2. CAS 替换 Mark Word:线程尝试使用 CAS(Compare-And-Swap) 原子操作,将对象头的 Mark Word 更新为一个指向该锁记录的指针,并将锁标志位改为 00

    • 成功:恭喜!当前线程成功获取了轻量级锁。此时,对象头的 Mark Word 指向了当前线程栈中的锁记录,表明该对象已被锁定。
    • 失败:表示在它操作的瞬间,已经有其他线程抢先一步获取了锁。这时,对象头的 Mark Word 已经指向了其他线程的锁记录。
  3. 自旋等待:获取锁失败的线程并不会立即阻塞,而是进入自旋状态,循环地尝试通过 CAS 获取锁。

轻量级锁状态

自旋锁的优化:从固定到自适应

早期的 JVM 中,自旋的次数是固定的(例如 10 次),或者由一个复杂的公式决定。这种“一刀切”的方式并不智能。

从 JDK 6 开始,引入了自适应自旋锁(Adaptive Spinning) 。它的原理非常人性化:

  • 如果一个线程在某次自旋后成功获取了锁,JVM 就会认为下一次它也很可能成功,因此会允许它在未来进行更长时间的自旋。
  • 反之,如果一个线程很少能通过自旋成功获取锁,JVM 就会认为继续自旋只是在浪费 CPU 资源,因此会减少甚至直接跳过下一次的自旋过程,直接进入锁膨胀阶段。

轻量级锁的释放过程

释放锁的过程同样基于 CAS 操作:

  1. 线程使用 CAS 尝试将之前保存在锁记录(Displaced Mark Word)中的数据恢复到对象头的 Mark Word 中。

    • 成功:说明在持有锁期间没有其他线程发生过激烈的竞争(没有导致锁膨胀),锁被成功释放,状态恢复到无锁。
    • 失败:这通常意味着一个严重的情况——在持有锁的期间,有其他线程在自旋等待,并且因为等待时间过长或竞争线程过多,已经将这个锁升级为了重量级锁。此时,对象头的 Mark Word 已经被修改为指向一个重量级锁的 Monitor。
  2. 如果 CAS 恢复失败,释放锁的线程会进入重量级锁的释放流程,即唤醒在 Monitor 的 _EntryList 中等待的线程。

偏向锁与轻量级锁的核心区别

  • 释放时机:偏向锁只有在遇到竞争时才会被动释放;轻量级锁每次退出同步块时都必须主动释放。
  • 竞争处理:偏向锁遇到竞争直接升级;轻量级锁遇到竞争会先自旋尝试,自旋失败后才会升级。

锁升级:从轻量级到重量级

轻量级锁并非万能。如果自旋的线程长时间无法获取锁,或者自旋的线程数量越来越多,自旋本身就会成为一种 CPU 资源的浪费。此时,就会发生最后一次锁升级:

  • 锁膨胀为重量级锁,锁标志位变为 10
  • 对象头的 Mark Word 不再指向线程栈,而是指向与之关联的 Monitor 对象。
  • 所有等待锁的线程(包括正在自旋的线程)都会被挂起,进入 Monitor 的 _EntryList 中排队等待,彻底告别 CPU 空转。

六、最终形态:重量級锁 (Heavyweight Locking)

当轻量级锁的自旋也无法解决激烈的竞争时,synchronized​ 就会亮出它的最后底牌——重量级锁。此时,锁不再依赖于线程的“自觉”自旋,而是由操作系统来强制执行“排队”规则,确保公平和秩序。

我们回到最初讨论的 Monitor。当锁膨胀为重量级锁后:

  1. 对象的 Mark Word 会被修改为一个指针,指向一个重量级的 Monitor 对象。
  2. 所有未能获取锁的线程,都会被阻塞,并放入 Monitor 内部的 _EntryList 队列中等待。
  3. 当持有锁的线程执行完同步代码块,它会释放锁并唤醒 _EntryList 中的一个或多个等待线程,由操作系统调度其中一个线程来获取锁。

这个过程在字节码层面由 monitorenter​ 和 monitorexit​ 两条指令来控制。monitorenter​ 尝试获取 Monitor 的所有权,而 monitorexit 则释放它。

重量级锁与Monitor

重量级锁虽然性能开销最大(因为涉及线程的阻塞和唤醒),但它能够处理任何激烈的并发场景,是保障线程安全的最后一道防线。

七、锁升级之谜:消失的哈希码和 GC 年龄去哪了?

这是一个非常有趣且深入的技术细节。我们知道,在无锁状态下,Mark Word 存储了对象的 identity hash code 和 GC 分代年龄。但在锁升级后,Mark Word 的空间被线程 ID、锁记录指针或 Monitor 指针占用了,那么这些原始信息被移到哪里去了呢?

  • 偏向锁状态

    • Mark Word 被线程 ID 覆盖。此时没有地方存储 hashCode
    • 这就是为什么一个已经计算过 hashCode的对象无法进入偏向锁状态的原因。
    • 反之,如果一个对象处于偏向锁状态时,收到了计算 hashCode​ 的请求,它会立即撤销偏向锁并膨胀为重量级锁,将 hashCode 存入 Monitor 中。
  • 轻量级锁状态

    • 在当前线程的栈帧中创建的锁记录(Lock Record) 里,有一份对象 Mark Word 的拷贝(Displaced Mark Word)。
    • 对象的 hashCode​ 和 GC 年龄等信息就保存在这份拷贝中。
    • 当锁释放时,会通过 CAS 操作将这些信息写回到对象头。
  • 重量级锁状态

    • Mark Word 指向了 Monitor 对象。
    • Monitor 对象的内部有专门的字段,可以记录和保存对象在非锁定状态下的整个 Mark Word 信息。
    • 当锁最终被释放后,这些信息同样会被恢复到对象头中。

八、三种锁策略的优缺点对比

锁类型优点缺点适用场景
偏向锁加解锁几乎没有额外开销,性能接近非同步代码。如果存在锁竞争,会带来额外的锁撤销开销。只有一个线程会访问同步代码块的场景。
轻量级锁线程不阻塞,响应速度快。如果长时间获取不到锁,自旋会持续消耗 CPU 资源。追求响应时间,且同步代码块执行速度非常快的场景。
重量级锁线程竞争不使用自旋,不空耗 CPU。线程阻塞和唤醒的开销大,响应时间慢。追求吞吐量,且同步代码块执行时间较长的场景。

九、JIT 编译器的魔法:超越锁升级的优化

除了锁升级,现代 JVM 的即时(JIT)编译器还会进行一些更智能的锁优化,进一步提升性能。

1. 锁消除(Lock Elimination)

JIT 编译器在动态编译时,会通过逃逸分析来判断一个锁对象是否真的被多个线程共享。如果它发现某个锁对象(如 new Object())只在当前方法内部使用,从未“逃逸”出去被其他线程访问,那么这个锁就是不必要的。

public void someMethod() {
    Object lock = new Object();
    // JIT会发现lock对象从未被其他线程访问
    // 因此,这个synchronized会被“消除”,编译后的代码中将没有加锁操作
    synchronized (lock) {
        // ... do something
    }
}

从 JIT 的角度看,这等同于无视 synchronized 关键字,直接执行内部代码,从而消除了加锁和解锁的开销。

2. 锁粗化(Lock Coarsening)

如果 JIT 编译器发现一系列连续的操作都对同一个对象反复加锁和解锁,它会认为这种频繁的操作是不必要的。此时,它会将多个相邻的同步块合并成一个更大的同步块,这就是锁粗化

public void anotherMethod() {
    StringBuffer sb = new StringBuffer();
    // 原始代码:频繁加锁解锁
    synchronized (sb) {
        sb.append("Hello");
    }
    synchronized (sb) {
        sb.append(" World");
    }

    // JIT优化后的等效代码:锁粗化
    // synchronized (sb) {
    //     sb.append("Hello");
    //     sb.append(" World");
    // }
}

通过一次加锁和一次解锁来覆盖整个操作序列,避免了多次申请和释放锁的性能损耗,提升了整体性能。

十、结论:Synchronized 的进化之旅

synchronized​ 的锁升级过程,一言以蔽之,就是 JVM 遵循 “先自旋,不行再阻塞” 的智能策略。

从 JDK 1.6 开始,synchronized 早已不是那个曾经被诟病为“性能低下”的重量级锁。它通过引入偏向锁和轻量级锁,将一个悲观锁在特定场景下转变为乐观锁的实现,极大地提升了性能。

  • 单线程场景:使用偏向锁,几乎零开销。
  • 竞争不激烈场景:升级为轻量级锁,通过自旋避免阻塞,保证响应速度。
  • 竞争激烈场景:膨胀为重量级锁,通过操作系统调度保证公平性,充分利用吞吐量。

再加上 JIT 编译器在更高维度的锁消除和锁粗化优化,使得 synchronized 成为一个既强大又智能的同步工具。理解其底层的演进逻辑,不仅能帮助我们写出更高效的并发代码,更能让我们体会到现代虚拟机设计的精妙与智慧。

Synchronized 高频面试题

第一部分:基础篇

1. 谈谈你对 synchronized的理解?

synchronized​ 是 Java 中的一个关键字,它是一种内置的、非公平的、可重入的锁,主要用于解决多线程环境下的数据安全问题,保证了代码的原子性、可见性和有序性

  • 原子性:被 synchronized​ 修饰的代码块在执行期间不可被中断
  • 可见性:一个线程对共享变量的修改,能够立即被其他线程看到。这是通过在解锁时刷新线程本地内存到主内存,加锁时清空本地内存来实现的。
  • 有序性:可以防止指令重排序,保证代码按书写顺序执行。

2. synchronized有哪几种用法?锁的是什么?有什么区别?

:主要有三种用法,其核心区别在于锁的范围(或称锁的粒度)不同,这直接决定了程序的并发能力

  1. 修饰实例方法 (实例锁):

    • 锁对象this​,即当前实例对象
    • 区别与范围:锁只对当前这一个实例有效。多个实例之间有各自的锁,互不影响,并发度较高
  2. 修饰静态方法 (类锁):

    • 锁对象MyClass.class​,即当前类的 Class 对象
    • 区别与范围:锁是全局的,作用于这个类的所有实例。一个线程拿到类锁,会阻塞其他线程访问该类的任何静态同步方法。并发度最低
  3. 修饰代码块

    • 锁对象:括号里指定的对象
    • 区别与范围最灵活,性能最好。因为它允许我们只锁住必要的代码(减小锁粒度),而不是锁定整个方法,从而显著提高并发能力。这是最推荐的用法

3. synchronized是可重入锁吗?在各种锁状态下是如何实现的?

是的,它是可重入锁。并且在偏向锁、轻量级锁和重量级锁三种状态下,都支持可重入,只是实现机制不同。

  • 偏向锁状态:实现最简单。线程进入时,只需检查 Mark Word 中的线程 ID 是否是自己。如果是,就直接进入,无需任何额外操作

  • 轻量级锁状态:通过线程栈上的锁记录(Lock Record) 实现。线程会检查对象 Mark Word 指向的锁记录是否位于自己的栈上。如果是,说明是重入,此时会在栈中压入一个特殊的空标记来计数,退出时移除标记即可。

  • 重量级锁状态:通过底层的 Monitor 实现。Monitor 内部有两个关键字段:_owner​ 指向持有锁的线程,_recursions​ 是一个重入计数器

    • 线程首次获取锁,_owner​ 指向它,_recursions 计为 1。
    • 线程再次重入,发现 _owner​ 是自己,就将 _recursions 的计数值加 1。
    • 退出同步块时,_recursions 减 1。当减到 0 时,线程才真正释放锁。

第二部分:进阶篇 - 锁升级

4. 为什么说 synchronized在 JDK 1.6 后性能得到了巨大提升?

:因为它引入了锁升级机制,避免了在所有情况下都使用重量级锁。

  • 优化前:早期版本直接依赖操作系统的 Mutex Lock,是纯粹的重量级锁。每次加锁解锁都涉及用户态和内核态的切换,成本极高。
  • 优化后:引入了偏向锁轻量级锁。JVM 会根据锁的竞争情况,沿着 无锁 → 偏向锁 → 轻量级锁 → 重量级锁 的路径进行升级,这个过程叫锁膨胀。大部分情况下,锁竞争并不激烈,用偏向锁和轻量级锁就能解决,避免了重量级锁的性能开销。

5. 能简述一下锁升级的过程吗?

:就是根据竞争的激烈程度,从偏向锁一步步升级到重量级锁的过程。

  1. 偏向锁

    • 特点:只有一个线程访问,锁会“偏向”该线程。Mark Word 记录其线程 ID。
    • 升级时机:当第二个线程来竞争时,偏向锁就会被撤销,并升级为轻量级锁。
  2. 轻量级锁

    • 特点:多个线程交替访问,但没有激烈冲突。线程通过 CAS 尝试获取锁,失败后会自旋(空转等待),而不是立即阻塞。
    • 升级时机:如果一个线程自旋了很久还没拿到锁,或者同时自旋的线程太多,JVM 认为竞争很激烈,就会升级为重量级锁。
  3. 重量级锁

    • 特点:竞争非常激烈。所有获取不到锁的线程都会进入阻塞状态,等待操作系统唤醒。
    • 实现:依赖操作系统的 Mutex Lock,通过底层的 Monitor 机制来管理线程排队。

第三部分:高级篇 - 原理与对比

6. Monitor 中的 _recursions _counter有什么区别?

:它们的作用完全不同:_recursions​ 是为锁的持有者服务的,用于实现可重入;而 _counter​ 是为等待者服务的,记录有多少线程在排队。

  • _recursions​:重入次数计数器

    • 谁用:只对当前持有锁的线程(Owner) 有效。
    • 何时变:该线程每重入一次,值加 1;每退出一次,值减 1。
  • _counter​:等待线程计数器(在 ObjectMonitor​ 中,常与 _EntryList 关联)。

    • 谁用:记录有多少个线程在排队等待获取锁
    • 何时变:一个新线程来竞争锁失败被挂起时,值可能增加;一个线程被唤醒时,值可能减少。

7. synchronized ReentrantLock有什么区别?怎么选?

synchronized​ 是 JVM 内置关键字,使用简单且能自动释放锁;ReentrantLock​ 是 API 层面的类,功能更强大,但需要手动释放锁。

对比维度synchronized (关键字)ReentrantLock (类)
用法简单,JVM 自动管理灵活,需在 finally​ 中手动 unlock()
公平性非公平默认非公平,可设置为公平
高级功能
1. 可中断等待 (lockInterruptibly)
2. 可超时等待 (tryLock)
3. 可绑定多个 Condition 实现精准唤醒
性能JDK 1.6 后与 ReentrantLock 差距不大结合高级功能使用,可控性更强

选择建议

  • 首选 synchronized:在锁竞争不激烈、功能简单的场景下,代码更简洁,不易出错。
  • 使用 ReentrantLock​:当需要公平锁、可中断/超时等待、或多条件唤醒等高级功能时。

相关文章

最后更新:2025年10月17日
分享: