synchronized 与 ReentrantLock:Java 锁机制原理与实现对比

synchronized 与 ReentrantLock:Java 锁机制原理与实现对比

目录

为什么有两种锁

Java 提供了两种锁机制:synchronized 和 ReentrantLock。很多开发者写并发代码时,会习惯性地用 synchronized,因为语法简单,加个关键字就行。但翻看一些成熟的开源项目,会发现大量使用 ReentrantLock 的地方。

两者的核心区别在于抽象层级不同。synchronized 是 JVM 内置的,编译器自动在字节码中插入加锁和解锁指令,开发者不需要关心锁的获取和释放过程。ReentrantLock 是 JDK 在 API 层面提供的锁实现,需要手动调用 lock() 和 unlock(),但换来的是更丰富的功能:可中断、可超时、公平锁、多条件变量。

这篇文章从字节码和 AQS 的层面拆解两者的实现原理,搞清楚它们各自的设计思路,以及什么时候该用哪个。

synchronized 的本质

synchronized 看起来只是一个关键字,但在字节码层面,它会被编译成两条指令:monitorentermonitorexit

java 复制代码
public void syncMethod() {
    synchronized (this) {
        // 临界区代码
    }
}

编译后的字节码大致是这样:

复制代码
monitorenter    // 尝试获取 this 对象的 Monitor
    // 临界区字节码
monitorexit     // 释放 this 对象的 Monitor
monitorexit     // 异常退出时也需要释放(编译器自动插入)

注意有两个 monitorexit:第二个是编译器为了异常安全自动插入的,确保即使临界区抛出异常,锁也能被释放。这就是为什么 synchronized 不需要手动释放锁,JVM 会帮你兜底。

这里有个关键概念:synchronized 锁的是对象的 Monitor 结构。每个 Java 对象都可以作为 Monitor 的关联对象,HotSpot 会在对象头(Mark Word)中记录锁状态信息。当 synchronized 竞争某个对象时,JVM 会通过 Monitor 机制管理线程同步。Monitor 是一种逻辑结构,不一定在对象创建时就存在,而是在发生锁竞争时才被关联。

synchronized 有三种用法,锁的对象不同:

java 复制代码
// 1. 实例方法 --- 锁的是当前实例对象 this
public synchronized void instanceMethod() { }

// 2. 静态方法 --- 锁的是 Class 对象(类级别的锁)
public static synchronized void staticMethod() { }

// 3. 代码块 --- 锁的是括号里指定的对象
public void blockMethod() {
    synchronized (lockObject) { }
}

实例方法锁 this,静态方法锁 Class 对象,代码块锁你指定的对象。同一个对象的 Monitor 在同一时刻只能被一个线程持有,其他线程想获取就得排队。

JVM 锁优化

早期的 synchronized 直接就是重量级锁,依赖操作系统的 Mutex Lock,涉及用户态到内核态的切换,开销很大。JDK 6 之后,JVM 引入了锁升级机制,根据竞争程度动态调整锁的状态,把锁的开销降了下来。

在早期 HotSpot 中,锁升级分三个阶段:偏向锁 → 轻量级锁 → 重量级锁。但从 JDK 15 开始,偏向锁默认关闭,JDK 18 的 HotSpot 已经彻底移除了偏向锁。现在更准确的描述是轻量级锁和重量级锁之间的动态调整。

复制代码
无锁状态
    │
    ▼ (线程进入 synchronized 块)
轻量级锁
    │
    ▼ (自旋失败,竞争激烈)
重量级锁

轻量级锁:线程在用户态通过 CAS 自旋尝试获取锁,不涉及内核态切换。自旋几次之后通常就能拿到锁,因为大多数锁的竞争窗口很短。

重量级锁:如果自旋多次还是拿不到锁,轻量级锁会膨胀为重量级锁。此时线程会被阻塞,进入 Monitor 的 EntryList 等待队列,涉及用户态到内核态的切换,开销明显增大。

锁膨胀通常不是简单的状态切换,JVM 会根据竞争情况动态调整。过去版本中常描述为"锁升级不可逆",但现代 HotSpot 的实现更加复杂,不能简单理解为永远只能升级。

锁状态 Mark Word 内容 适用场景 开销
轻量级锁 指向栈中锁记录的指针 低竞争、短临界区 CAS 自旋
重量级锁 指向 ObjectMonitor 的指针 高竞争 线程阻塞、内核切换

ReentrantLock 的设计

ReentrantLock 是 JDK 在 API 层面提供的锁,位于 java.util.concurrent.locks 包下。它不依赖 synchronized 的 monitorenter/monitorexit 指令,而是基于 AQS,通过 CAS 和 LockSupport 实现线程竞争、阻塞和唤醒。

java 复制代码
ReentrantLock lock = new ReentrantLock();

lock.lock();
try {
    // 临界区代码
} finally {
    lock.unlock();  // 必须在 finally 中释放
}

和 synchronized 相比,最大的区别是 unlock() 必须手动调用。忘记释放锁会导致死锁,标准写法是把 unlock() 放在 finally 块里。这是 ReentrantLock 的一个使用门槛,换来的是更灵活的控制能力。

ReentrantLock 提供了几个 synchronized 做不到的特性:

可中断等待:synchronized 的线程一旦进入阻塞状态,只能傻等。ReentrantLock 支持在等待锁的过程中响应中断。

java 复制代码
ReentrantLock lock = new ReentrantLock();

Thread t = new Thread(() -> {
    try {
        lock.lockInterruptibly();  // 可中断地获取锁
        // 正常业务逻辑
    } catch (InterruptedException e) {
        System.out.println("等待锁的过程中被中断了");
    } finally {
        if (lock.isHeldByCurrentThread()) {
            lock.unlock();
        }
    }
});
t.start();

// 主线程在 1 秒后中断子线程
Thread.sleep(1000);
t.interrupt();

超时获取:tryLock 支持设置等待时间,超时后自动放弃,不会无限等待。

java 复制代码
if (lock.tryLock(5, TimeUnit.SECONDS)) {
    try {
        // 拿到锁,执行业务
    } finally {
        lock.unlock();
    }
} else {
    // 5 秒内没拿到锁,走降级逻辑
    System.out.println("获取锁超时,执行降级方案");
}

公平锁:synchronized 的锁竞争是非公平的,刚来的线程可能插队抢到锁。ReentrantLock 支持公平模式,严格按照等待顺序分配锁。

java 复制代码
ReentrantLock fairLock = new ReentrantLock(true);  // 公平锁

公平锁保证了先来后到的顺序性,但吞吐量会低于非公平锁,因为每次都要从队列头部取出等待线程,不能直接竞争。大多数场景下用默认的非公平锁就行。

AQS 的核心机制

ReentrantLock 的底层是 AQS(AbstractQueuedSynchronizer),Semaphore、CountDownLatch、ReadWriteLock 底层也是它。AQS 的核心结构就两样东西:一个 volatile int state + 一个 FIFO 队列

java 复制代码
// AQS 的核心字段(简化版)
public abstract class AbstractQueuedSynchronizer {
    private volatile int state;           // 同步状态
    private transient Node head;          // 队列头节点
    private transient Node tail;          // 队列尾节点
}

在 ReentrantLock 中,state 表示锁的重入次数。state = 0 是未锁定,state > 0 是锁定且值为重入次数。但 state 在不同同步器中含义不同------Semaphore 中它是剩余许可数量,CountDownLatch 中它是倒计数数量。AQS 只提供框架,具体语义由子类定义。

ReentrantLock 默认是非公平锁(NonfairSync),这也是大多数场景的首选。非公平锁和公平锁的 acquire 流程有一个关键区别:

非公平锁允许插队:刚到的线程不需要检查队列,直接 CAS 抢锁。如果恰好锁空闲,它就拿到了,省去了入队、出队的开销。这种"插队"看似不公平,但在高并发场景下吞吐量更高,因为减少了线程切换的次数。公平锁则严格按 FIFO 顺序,保证没有饥饿,但每次获取锁都要检查队列,性能会低一些。

来看非公平锁的完整获取流程:

release 流程:

复制代码
release():
    state--
    if state == 0:              // 可重入时减到 0 才真正释放
        exclusiveOwnerThread = null
        LockSupport.unpark(队列中下一个等待线程)

AQS 把锁的竞争和排队逻辑抽象成通用框架。ReentrantLock 只需定义 state 的语义和公平/非公平策略,队列管理、线程阻塞和唤醒这些通用逻辑全交给 AQS。这也是 JUC 包里那么多并发工具都能复用 AQS 的原因。

功能对比

特性 synchronized ReentrantLock
实现层级 JVM 内置,字节码指令 JDK API,AQS + CAS
阻塞机制 ObjectMonitor AQS + LockSupport
锁获取/释放 自动(进入/退出同步块) 手动 lock()/unlock()
可重入 支持 支持
可中断 不支持 lockInterruptibly()
超时获取 不支持 tryLock(timeout)
公平锁 不支持 new ReentrantLock(true)
条件变量 只有 wait/notify 多个 Condition
锁状态查询 不支持 isLocked(), getHoldCount()

synchronized 的优势是简单。关键字级别的语法支持,不用关心锁的释放,JVM 自动保证异常安全。对于"一个方法加锁"这种简单场景,synchronized 写起来更简洁,也更难犯错。

ReentrantLock 的优势是灵活。可中断、可超时、公平锁、多条件变量,这些功能在复杂并发场景下非常有用。比如"尝试获取锁,拿不到就走降级逻辑"这种需求,synchronized 就做不了。

总结

现代 JVM 对 synchronized 的优化已经很深,轻量级锁、锁消除、锁粗化这些技术让两者的性能差异在多数业务场景下并不明显。选择标准是需不需要 ReentrantLock 的额外功能。

简单决策:需要超时获取、可中断等待、多条件变量这三个中的任何一个,用 ReentrantLock;其他场景使用 synchronized 就够了。

相关推荐
aramae1 小时前
C++ IO流完全指南:从C标准库到C++流式编程
服务器·c语言·开发语言·c++·后端
ZHOU_WUYI1 小时前
4. light wam 模型loss计算过程
开发语言·人工智能·python
长不胖的路人甲2 小时前
Serial 串行、Parallel 并行、CMS 并发收集器
java·开发语言·jvm
IT_Octopus3 小时前
Spring Boot 线程池关闭:destroyMethod 的作用与最佳实践
java·spring boot·后端
亦暖筑序3 小时前
AgentScope-Java 入门:完善 Vue 前端、发布 GitHub,并规划下一步
java·前端·vue.js
wddptwd283 小时前
android studio 报错怎么处理 java.lang.NullPointerException
android·java·android studio
麻瓜老宋3 小时前
AI开发C语言应用按步走,表达式计算器calc的第二十四步,结束开发,生成calc 用法指南
c语言·开发语言·atomcode
霸道流氓气质3 小时前
分布式系统中接口时序不确定性处理
java·开发语言·分布式
深入云栈3 小时前
Netty 4.2.x 源码深度解析 (四):HashedWheelTimer —— 时间轮定时调度算法
java