Skip to content

锁机制

synchronized 和 ReentrantLock 区别?

特性synchronizedReentrantLock
实现层JVM 内置关键字Java 代码(AQS)
锁的释放自动释放(出块/异常)必须手动 unlock(),需 finally
可中断等待不支持lockInterruptibly()
超时获锁不支持tryLock(timeout)
公平锁不支持(非公平)构造器传 true 开启
条件变量只有 wait/notify多个 Condition,精确唤醒
性能JDK 6+ 锁升级后差距小高竞争下稍优
java
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
    // 临界区
} finally {
    lock.unlock(); // 必须放 finally!
}

简单场景用 synchronized(代码简洁,JVM 自动优化);需要超时/可中断/多条件时用 ReentrantLock。

ReentrantLock 怎么实现公平锁?

构造器传入 true 即可开启公平锁。区别在于:线程尝试获取锁时,会先检查等待队列里是否有排队的线程。

非公平锁(默认)

新线程直接 CAS 抢锁,不看队列。可能插队成功,吞吐量更高,但可能导致队列中的线程长期等待(饥饿)

非公平锁示意图

公平锁

新线程先检查队列,若有等待线程则乖乖排队。严格按 FIFO 顺序,无饥饿,但线程切换开销大,吞吐量低

公平锁示意图

java
new ReentrantLock();       // 非公平锁(默认,性能更好)
new ReentrantLock(true);   // 公平锁(按等待顺序获锁)

什么是可重入锁?

同一个线程在持有锁的情况下,可以再次获取同一把锁而不会死锁,就叫可重入锁。

实现原理

锁内部维护一个持有线程和重入计数器。加锁时:若是同一线程,计数器 +1;释放时计数器 -1,归零才真正释放锁

为什么必要

synchronized 方法调用另一个 synchronized 方法(同一对象),若锁不可重入就会死锁。Java 的 synchronized 和 ReentrantLock 都是可重入的

java
public synchronized void methodA() {
    methodB(); // 同一线程再次获取 this 的锁
               // 可重入:计数器 2 → 不会死锁
}

public synchronized void methodB() {
    // 计数器在此处 = 2
}
// methodA 返回时计数器从 2→1→0 真正释放

ReentrantLock 同理:同一线程连续 lock() 两次,需要 unlock() 两次才能真正释放。

什么是悲观锁、乐观锁?

这是两种并发控制的思想,不是具体的锁类型。

悲观锁

悲观地认为:每次操作都会有并发冲突,所以每次访问数据前都先加锁。

  • 代表:synchronized、ReentrantLock
  • 适合:写多读少、锁竞争激烈
  • 缺点:加锁/解锁开销,线程阻塞

乐观锁

乐观地认为:大多数时候不会冲突,直接操作,提交时再检查是否有冲突。

  • 代表:AtomicInteger(CAS)、数据库版本号
  • 适合:读多写少、冲突概率低
  • 缺点:冲突多时大量重试,ABA 问题
java
// 乐观锁 CAS 原理(伪代码)
while (true) {
    int old = count.get();      // 1. 读取当前值
    int newVal = old + 1;       // 2. 计算新值
    if (count.CAS(old, newVal)) // 3. 提交:若当前值还是 old 就写入
        break;                  //    成功退出,否则重试
    // 失败说明有人修改了,重试
}

什么是自旋锁?

线程获取锁失败时,不立刻挂起,而是在 CPU 上循环(自旋)等待,期望锁很快会被释放。

优点

避免线程切换(用户态→内核态)的开销。锁持有时间很短时,自旋比挂起更快

缺点

自旋期间占用 CPU。如果锁持有时间长,自旋反而浪费大量 CPU,不如直接阻塞

java
// 自旋等待的本质(简化)
while (!tryAcquire(lock)) {
    // CPU 一直在空转,不阻塞
    Thread.onSpinWait(); // JDK 9+ 提示 CPU 自旋暂停
}

JVM 的自适应自旋:JDK 6+ 引入自适应自旋,根据上次该锁的自旋成功率动态调整自旋次数。成功率高则多自旋,成功率低则减少自旋次数甚至不自旋,直接膨胀为重量级锁。

适用场景

临界区极短、CPU 核数多的多核场景

不适用

单核 CPU(无法让出 CPU)或持锁时间长的场景

Java 中的体现

轻量级锁升级为重量级锁前,JVM 会先自旋等待

什么是偏向锁、轻量级锁、重量级锁?

这是 JVM 对 synchronized 的三级优化,从低开销到高开销逐步升级,核心信息存储在对象头的 Mark Word 里。

偏向锁

锁偏向第一个获取它的线程。再次获取时只检查线程 ID,无 CAS,无内核调用。开销极小,适合"只有一个线程用"的场景

轻量级锁

有多个线程交替(非同时)竞争锁。用 CAS 在栈帧中创建 Lock Record,不需要操作系统介入。适合"竞争少且短暂"

重量级锁

多线程真正同时竞争。膨胀为操作系统 Mutex,竞争失败的线程被挂起(内核态),开销最大,但保证正确性

三种锁在对象头 Mark Word 中的状态标志不同:

锁升级全过程

偏向锁 → 轻量级锁 → 重量级锁

对象刚创建:初始状态 — 无锁

一个对象刚被 new 出来,Mark Word 处于无锁状态(标志位 001)。

JDK 15 之后默认关闭了偏向锁(因为维护成本高),新创建对象直接进入无锁状态,首次竞争会直接升级为轻量级锁。JDK 6~14 默认开启偏向锁。

java
Object obj = new Object();
// Mark Word: [hashcode | 0 | 01]  无锁

线程A首次加锁:偏向锁 — 线程A独占

线程 A 是第一个获取 synchronized(obj) 的线程。JVM 将线程 A 的 ID 写入 Mark Word,锁偏向线程 A。

之后线程 A 再次进入同步块,只需检查 Mark Word 里的线程 ID 是否是自己,不需要 CAS,不需要 OS 调用,几乎零开销。

java
// 线程A
synchronized(obj) { ... } // 偏向锁建立
synchronized(obj) { ... } // 再次进入:只检查ID,极快
// Mark Word: [ThreadA_ID | 1 | 01]

线程B出现竞争:偏向锁撤销 — 升级为轻量级锁

线程 B 尝试获取同一把锁。JVM 发现锁已偏向线程 A,触发偏向锁撤销(需要等待全局安全点 SafePoint)。

撤销后,若线程 A 已经不在同步块内,锁直接升级为轻量级锁,线程 B 用 CAS 尝试获取。

java
// 线程B 尝试进入
synchronized(obj) {
    // 1. 发现偏向了线程A
    // 2. 等待 SafePoint,撤销偏向
    // 3. 升级为轻量级锁
    // 4. 线程B 在栈帧创建 Lock Record
    // 5. CAS 将 Mark Word 指向 Lock Record
}

线程交替持锁:轻量级锁 — CAS 无阻塞竞争

轻量级锁下,线程不会被挂起,而是用 CAS 自旋等待锁释放,适合"线程交替持锁、持锁时间短"的场景。

java
// 轻量级锁加锁(JVM 内部):
// 1. 在当前线程栈帧创建 Lock Record
// 2. 将 obj 的 Mark Word 复制到 Lock Record
// 3. CAS:将 Mark Word 替换为指向 Lock Record 的指针
// 4. 成功→持有;失败→自旋等待

// Mark Word: [Lock Record 指针 | 00]

自旋是有上限的(自适应)。若自旋次数过多,说明竞争激烈,继续升级为重量级锁。

自旋失败/多线程同时竞争:重量级锁 — 锁膨胀

自旋超过阈值,或有多个线程同时等待锁,轻量级锁膨胀为重量级锁。

锁膨胀后,Mark Word 指向一个 ObjectMonitor(操作系统 Mutex)。竞争失败的线程被操作系统挂起,让出 CPU,进入 Monitor 的 EntryList 阻塞队列。

java
// 重量级锁内部(ObjectMonitor)
ObjectMonitor {
    _owner      → 当前持锁线程
    _EntryList  → 阻塞等待的线程队列
    _WaitSet    → 调用 wait() 后挂起的线程
}
// Mark Word: [Monitor指针 | 10]

锁释放:重量级锁 — 释放与唤醒

持锁线程执行完同步块,释放锁:

java
// 释放流程:
// 1. owner 置 null
// 2. 从 EntryList 唤醒一个线程重新竞争
// 3. 被唤醒线程从内核态恢复,重新竞争锁

重量级锁的每次加锁/解锁都涉及用户态 ↔ 内核态切换,开销远大于前两种。这就是为什么 JVM 要费力地做偏向锁和轻量级锁优化。

锁只能升级,不能降级

升级路径是单向的(无锁→偏向→轻量级→重量级),JVM 不会自动降级。极少数情况下 JVM 可能在 GC 时降级,但不依赖此行为。

锁升级的本质是:根据竞争激烈程度,用尽量小的开销保证正确性。竞争越激烈,开销越大,同步越安全。