在多线程编程中,共享资源的访问如同没有秩序的十字路口,如果没有有效的协调机制,多个线程同时操作同一份数据,必然会引发数据竞争、结果错乱等一系列问题。想象一下,两个线程同时为一个银行账户执行转账操作,最终余额可能并非你所期望的结果。
作为Java语言提供的核心“交通警察”,Synchronized(内置锁)与ReentrantLock(显式锁)是保障并发安全的两大利器。它们设计理念不同,功能侧重各异。今天我们就来深入剖析这两种锁机制,探讨它们各自的原理、特点,以及在什么场景下应该如何选择。
一、Synchronized:简单高效的“原生守护者”
1.1 上手简单,三种用法全覆盖
Synchronized是Java语言层面的关键字,用法直观,开发者可以快速上手。其主要有三种使用方式,覆盖了绝大多数基础的同步需求:
public class SynchronizedDemo {
// 1. 同步实例方法 - 锁住的是当前对象实例(this)
public synchronized void instanceMethod() {
System.out.println("实例方法加锁,多线程访问需排队");
}
// 2. 同步静态方法 - 锁住的是当前类的Class对象
public static synchronized void staticMethod() {
System.out.println("静态方法加锁,全类共享一把锁");
}
// 3. 同步代码块 - 可灵活指定锁对象,控制粒度更细
private final Object lock = new Object();
public void codeBlock() {
synchronized (lock) {
System.out.println("代码块加锁,按需锁定临界区");
}
}
// 可重入特性演示:同一线程可重复获取已持有的锁
public synchronized void outerMethod() {
System.out.println("进入外层方法,已获取锁");
innerMethod(); // 同一线程可直接进入内层同步方法,不会死锁
}
public synchronized void innerMethod() {
System.out.println("进入内层方法,成功重入");
}
}
1.2 底层原理:从对象头到锁升级
很多人印象中Synchronized性能较差,但现代的JVM已经对其进行了大量深度优化。理解它的工作原理,需要从Java对象的内存布局和锁状态升级说起。
每个Java对象在内存中都有一个对象头,其中的Mark Word区域是记录锁状态的关键。在不同的锁状态下,Mark Word存储的内容完全不同。

Mark Word的状态会根据线程竞争的激烈程度动态演变,这就是JVM著名的“锁升级”优化策略。该策略旨在用最小的代价适应不同级别的并发场景:
- 无锁状态:对象创建后的初始状态,没有线程竞争。
- 偏向锁:当只有一个线程反复访问同步块时,JVM会通过CAS操作在对象头Mark Word中记录该线程ID。之后该线程再进入同步块时,无需进行任何同步操作,直接访问,效率极高(注:从JDK 15开始,偏向锁默认被禁用,需手动开启)。
- 轻量级锁:当有少量线程交替竞争锁时,锁会升级为轻量级锁。线程会在自己的栈帧中创建锁记录(Lock Record),并通过CAS操作尝试将对象头中的Mark Word更新为指向锁记录的指针。这个过程不会使线程阻塞。
- 重量级锁:当竞争非常激烈时,轻量级锁会通过自旋一定次数后升级为重量级锁。此时,锁的实现依赖于操作系统底层的互斥量(Mutex),未获得锁的线程会被挂起,进入阻塞状态,线程上下文切换会带来较大的开销。
此外,Synchronized在编译后的字节码中,会分别在同步代码块的前后插入monitorenter和monitorexit指令。即使同步块内的代码抛出异常,JVM也能确保monitorexit指令被执行,从而自动释放锁,避免了锁泄漏的风险,这也是它“省心”的重要原因之一。
1.3 优缺点分析
| 特性 |
具体说明 |
| 使用成本 |
极低,关键字形式,无需手动管理锁的释放。 |
| 锁管理 |
自动获取和释放,异常时也能安全释放。 |
| 可中断性 |
不支持,线程一旦因获取锁而阻塞,无法被中断。 |
| 公平性 |
非公平锁,但偏向锁模式下近似公平。 |
| 条件等待 |
支持Object.wait()/notify(),但只有一个等待队列,灵活性较差。 |
| 性能 |
经过JVM多轮优化后,在低至中度并发场景下性能表现优异。 |
二、ReentrantLock:灵活强大的“多功能锁”
2.1 用法更灵活,功能更强大
ReentrantLock位于java.util.concurrent.locks包中,是一个显式锁类。它需要开发者手动调用lock()和unlock()方法,上手成本略高于Synchronized,但换来了极大的灵活性和丰富的功能。
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.TimeUnit;
public class ReentrantLockDemo {
// 初始化锁,可通过构造函数指定是否为公平锁
private final ReentrantLock lock = new ReentrantLock(true);
private final Condition condition = lock.newCondition();
private int count = 0;
// 1. 基础用法:必须在finally块中释放锁
public void basicLock() {
lock.lock(); // 手动获取锁
try {
count++;
System.out.println("当前计数:" + count);
} finally {
lock.unlock(); // 确保锁被释放,避免死锁
}
}
// 2. 非阻塞尝试获取锁:获取失败立即返回
public void tryLockDemo() {
if (lock.tryLock()) {
try {
System.out.println("成功获取锁,执行操作");
} finally {
lock.unlock();
}
} else {
System.out.println("获取锁失败,执行备选逻辑");
}
}
// 3. 超时获取锁:在指定时间内尝试
public void tryLockWithTimeout() {
try {
// 尝试在1秒内获取锁
if (lock.tryLock(1, TimeUnit.SECONDS)) {
try {
System.out.println("1秒内获取锁成功");
Thread.sleep(2000); // 模拟耗时业务操作
} finally {
lock.unlock();
}
} else {
System.out.println("获取锁超时,放弃操作");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println("线程被中断,终止操作");
}
}
// 4. 可中断锁:获取锁的过程可响应线程中断
public void interruptibleLock() {
try {
lock.lockInterruptibly(); // 可被中断的锁获取方式
try {
System.out.println("获取锁成功,执行长时间操作");
while (true) {
// 模拟一个无限循环的耗时任务
}
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
System.out.println("锁获取被中断,线程恢复中断状态");
Thread.currentThread().interrupt();
}
}
// 5. 多条件等待:支持创建多个Condition对象
public void conditionDemo() {
lock.lock();
try {
// 条件不满足时,释放锁并等待
while (count < 10) {
System.out.println("当前计数:" + count + ",条件不满足,等待中...");
condition.await(); // 释放锁并进入等待状态
}
System.out.println("条件满足(计数≥10),继续执行");
condition.signalAll(); // 唤醒所有在此condition上等待的线程
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlock();
}
}
}
2.2 核心优势
从其名字中的“Reentrant”(可重入)可知,它同样支持锁重入。除此之外,ReentrantLock还具备诸多Synchronized不具备的优势:
- 公平锁支持:通过构造函数可以指定创建一个公平锁,线程将按照请求锁的顺序(FIFO)来获得锁,有效防止线程饥饿现象。而
Synchronized只能是非公平锁。
- 可中断的锁获取:
lockInterruptibly()方法允许在等待锁的过程中响应中断,避免线程无限期阻塞。
- 尝试锁与超时:
tryLock()方法支持无参(立即返回)和带超时参数两种形式,能在指定时间内尝试获取锁,拿不到就执行其他逻辑,是预防死锁的有效手段。
- 多条件队列:可以调用
newCondition()创建多个Condition对象,实现更精细的线程等待与唤醒控制,典型应用如生产者-消费者模型。Synchronized与之对应的wait/notify只能有一个等待队列。
- 锁状态查询:提供了
isLocked()、hasQueuedThreads()、getQueueLength()等方法,方便监控锁的状态和等待队列情况。
2.3 使用注意事项
更强的灵活性也意味着更大的责任,使用ReentrantLock需要格外小心:
- 必须手动释放锁:锁的释放必须放在
finally块中,确保即使业务代码抛出异常,锁也能被释放,否则将导致永久性死锁。
- 管理复杂度增加:开发者需要手动控制加锁和解锁的时机,容易出现忘记解锁或解锁时机错误的问题。
- 公平锁的性能损耗:公平锁虽然保证了公平性,但因其需要维护有序队列,性能通常略低于非公平锁,需要根据实际场景权衡选择。
三、终极对比与选型建议
经过详细解析,Synchronized和ReentrantLock究竟该如何选择?答案并非绝对,关键在于匹配应用场景。
| 对比维度 |
Synchronized |
ReentrantLock |
| 使用难度 |
低,关键字形式,自动管理 |
中,需手动获取释放,必须搭配try-finally |
| 公平性 |
仅支持非公平锁 |
支持公平 / 非公平锁(默认非公平) |
| 可中断性 |
不支持 |
支持(lockInterruptibly()) |
| 超时获取 |
不支持 |
支持(tryLock(long, TimeUnit)) |
| 条件等待 |
单条件队列(wait/notify) |
多条件队列(Condition) |
| 锁状态查询 |
不支持 |
支持(isLocked()等) |
| 性能 |
低并发下优异,JVM优化充分 |
高并发下表现更稳定,功能丰富 |
实用选择建议
- 简单同步场景,优先使用Synchronized:如果你的需求仅仅是保护一个方法或一小段代码,不需要可中断、超时、公平性等高级特性,那么
Synchronized是最佳选择。它语法简洁,由JVM自动管理锁,经过充分优化后性能完全满足要求,能有效降低代码复杂度和出错风险。
- 复杂同步场景,考虑使用ReentrantLock:当你的业务逻辑需要公平锁、可中断的锁获取(例如实现一个可取消的任务)、尝试锁(避免死锁)、或者需要多个等待条件(如复杂的生产者-消费者模型)时,
ReentrantLock提供的丰富API将成为你的得力助手。
- 性能考量:在低并发或竞争不激烈的场景下,两者性能差异不大。在极高并发、锁竞争激烈的场景中,
ReentrantLock通常能提供更稳定和可预测的性能表现,但前提是代码正确实现了锁的管理。
总而言之,Synchronized是Java提供的开箱即用、安全省心的默认选项;而ReentrantLock则是一把瑞士军刀,功能强大但需要谨慎使用。理解它们的底层机制和适用边界,是每一位进行多线程编程的开发者必备的技能。希望本文的对比分析能帮助你在实际项目中做出更合适的技术选型。如果你想深入探讨更多Java并发或系统设计话题,欢迎到云栈社区与更多开发者交流。