Synchronized vs ReentrantLock 深度对比
一、简介
1. Synchronized(Java 关键字)
Synchronized 是 Java 内置关键字,依托 JVM 实现。它是 Java 中最基本的互斥同步手段,经过版本迭代(Java 6 之后),性能已大幅提升,支持锁升级(偏向锁 -> 轻量级锁 -> 重量级锁)。
核心原理回顾(配合代码理解)
无锁:对象刚创建(开启偏向锁延迟的情况下)。
偏向锁:只有一个线程抢锁,JVM 会把锁偏向这个线程(Mark Word 记录 Thread ID)。
轻量级锁:有多个线程交替执行(轻微竞争),偏向锁撤销,线程通过 CAS + 自旋抢锁。
重量级锁:多线程同时抢锁(激烈竞争),自旋超过阈值,锁膨胀,线程进入 Monitor 等待队列(Blocking)。
java
import org.openjdk.jol.info.ClassLayout;
public class SynchronizedUpgradeDemo {
// 对象头观测对象
private static final Object LOCK = new Object();
public static void main(String[] args) throws InterruptedException {
System.out.println("====== 准备阶段:对象初始状态 ======");
// 此时可能是无锁或匿名偏向状态
System.out.println(ClassLayout.parseInstance(LOCK).toPrintable());
// 休眠一会儿,绕过 JVM 启动时的偏向锁延迟(JDK 8默认4秒)
Thread.sleep(5000);
System.out.println("\n====== Case 1: 单线程(偏向锁) ======");
singleThreadTest();
// 停顿一下,让上一个锁完全释放
Thread.sleep(2000);
System.out.println("\n====== Case 2: 多线程交替(轻量级锁) ======");
twoThreadsAlternateTest();
Thread.sleep(2000);
System.out.println("\n====== Case 3: 多线程争抢(重量级锁) ======");
multiThreadsContendTest();
}
/**
* 单线程执行同步块
* 预期:偏向锁(Mark Word 记录当前线程ID)
*/
private static void singleThreadTest() {
synchronized (LOCK) {
System.out.println("单线程持有锁中...");
System.out.println(ClassLayout.parseInstance(LOCK).toPrintable());
}
}
/**
* 两个线程交替执行(轻微竞争)
* 预期:轻量级锁(线程栈中有 Lock Record,CAS 操作)
*/
private static void twoThreadsAlternateTest() throws InterruptedException {
Runnable task = () -> {
synchronized (LOCK) {
System.out.println(Thread.currentThread().getName() + " 持有锁中...");
System.out.println(ClassLayout.parseInstance(LOCK).toPrintable());
try {
Thread.sleep(100); // 故意 sleep,让另一个线程处于就绪状态,但不同时抢
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Thread t1 = new Thread(task, "Thread-A");
Thread t2 = new Thread(task, "Thread-B");
t1.start();
// 确保 t1 先拿到锁
Thread.sleep(50);
t2.start();
t1.join();
t2.join();
}
/**
* 多个线程疯狂争抢(激烈竞争)
* 预期:重量级锁(膨胀为 Monitor,线程阻塞/等待)
*/
private static void multiThreadsContendTest() throws InterruptedException {
int threadCount = 4;
Runnable task = () -> {
// 疯狂抢锁,不 sleep,制造高并发
for (int i = 0; i < 5; i++) {
synchronized (LOCK) {
// 打印对象头,观察是否变为重量级锁(10)
if (i == 0) { // 只打印一次,避免刷屏
System.out.println(Thread.currentThread().getName() + " 持有锁中...");
System.out.println(ClassLayout.parseInstance(LOCK).toPrintable());
}
}
}
};
Thread[] threads = new Thread[threadCount];
for (int i = 0; i < threadCount; i++) {
threads[i] = new Thread(task, "Contend-Thread-" + i);
}
for (Thread t : threads) {
t.start();
}
for (Thread t : threads) {
t.join();
}
}
}
2. ReentrantLock(JDK API)
ReentrantLock 是 java.util.concurrent.locks 包下的一个类,基于 AQS(AbstractQueuedSynchronizer) 实现。它提供了比 synchronized 更灵活、更强大的锁控制能力。
核心架构:AQS 的两大基石
ReentrantLock 内部有一个名为 Sync 的抽象类,它继承了 AbstractQueuedSynchronizer。
AQS 依靠两大核心来维持锁的运作:
volatile int state(同步状态):代表锁被占用的次数。
FIFO 双向队列(CLH 队列变体):存放那些抢锁失败的线程。
- 状态位 state 的含义
0:锁未被任何线程占用。
0:锁已被占用。数值代表重入的次数。
例如:线程 A 第一次 lock(),state 从 0 -> 1。
线程 A 再次 lock()(重入),state 从 1 -> 2。
线程 A 执行 unlock(),state 从 2 -> 1。
线程 A 再次 unlock(),state 从 1 -> 0(完全释放)。
- CLH 队列(等待队列)
当线程抢锁失败(CAS 设置 state 失败),AQS 会将当前线程包装成一个 Node 节点,通过 CAS + 自旋 的方式挂载到队列的尾部,然后通过 LockSupport.park() 挂起线程。
类结构关系
bash
ReentrantLock
├── Sync (abstract class extends AbstractQueuedSynchronizer)
│ ├── NonfairSync (默认,非公平锁)
│ └── FairSync (公平锁)
└── ConditionObject (内部类,实现多条件队列)
加锁流程(以非公平锁为例)
java
// ReentrantLock.java
public void lock() {
sync.lock(); // 调用内部类的 lock 方法
}
// ReentrantLock.NonfairSync
final void lock() {
// 1. 【插队】:不管队列里有没有人,先直接 CAS 抢锁
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread()); // 抢成功,标记我是主人
else
acquire(1); // 2. 抢失败,走 AQS 的标准获取流程
}
// AbstractQueuedSynchronizer.java
public final void acquire(int arg) {
// 3. tryAcquire:再次尝试获取(处理重入逻辑)
// addWaiter:获取失败,加入队列
// acquireQueued:在队列中自旋/挂起等待
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
详细步骤拆解:
场景:线程 A 持有锁,线程 B 来抢锁。
抢占阶段:
线程 B 调用 lock(),首先执行 compareAndSetState(0, 1)。
此时 state 是 1(被 A 占用),CAS 失败。
尝试获取(tryAcquire):
进入 acquire(1),调用 tryAcquire(1)。
判断当前线程是不是 exclusiveOwnerThread(持有者)。
如果是(重入),state++,返回成功。
如果不是(这里是线程 B,持有者是 A),返回失败。
入队阶段(addWaiter):
线程 B 被包装成 Node 节点。
通过 CAS 自旋,将节点插入到 CLH 队列的尾部。
挂起阶段(acquireQueued):
线程 B 在队列中检查前驱节点。
如果前驱是 Head 节点,它会再次尝试 tryAcquire(这是自旋等待)。
如果还是失败,调用 LockSupport.park(this),线程 B 阻塞(WAITING 状态),停止空转,等待被唤醒。
解锁流程
java
// ReentrantLock.java
public void unlock() {
sync.release(1);
}
// AbstractQueuedSynchronizer.java
public final boolean release(int arg) {
if (tryRelease(arg)) { // 1. 尝试释放锁
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h); // 2. 唤醒后继节点
return true;
}
return false;
}
// ReentrantLock.Sync
protected final boolean tryRelease(int releases) {
int c = getState() - releases; // state 减 1
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException(); // 不是持有者不能释放
boolean free = false;
if (c == 0) { // 只有 state 归零,才真正释放
free = true;
setExclusiveOwnerThread(null);
}
setState(c); // 写回 state
return free;
}
详细步骤拆解:
场景:线程 A 执行完业务,调用 unlock()。
校验身份:判断当前线程是否是锁的持有者(A),不是则抛异常。
重入计数递减:state = state - 1。
判断释放:
如果 state > 0(重入了多次),锁依然被 A 持有,返回 false。
如果 state == 0,锁完全释放。将 exclusiveOwnerThread 设为 null。
唤醒后继:找到 CLH 队列的头结点,调用 LockSupport.unpark(线程B),唤醒队列中等待最久的线程 B。
线程 B 复活:线程 B 从 park() 处醒来,继续执行 acquireQueued 中的循环,再次尝试 CAS 获取锁。
Condition 的实现原理
ReentrantLock 的 newCondition() 返回的是 AQS 内部的 ConditionObject。
等待队列(单向链表):每个 Condition 对象都维护一个独立的等待队列。
await():线程调用 await() 时,会释放锁(state 归零),并将线程包装成节点放入 Condition 的等待队列,然后 park 挂起。
signal():线程调用 signal() 时,会从 Condition 的等待队列中取出头节点,将其转移到 AQS 的主 CLH 队列中,等待抢锁。
这就是为什么 ReentrantLock 可以实现多条件队列,而 synchronized 只能在一个等待队列上 wait/notify。
二、核心区别对比表
| 特性维度 | Synchronized | ReentrantLock | 优势方 |
|---|---|---|---|
| 实现层面 | JVM 层面(关键字) | JDK 层面(API 类) | - |
| 锁的获取 | 简单,自动获取和释放 | 手动 lock() / unlock() |
Synchronized |
| 锁的释放 | 自动(代码块结束或异常) | 手动 (必须在 finally 中释放) |
Synchronized |
| 公平性 | 仅支持非公平锁 | 可选 (构造参数 fair=true) |
ReentrantLock |
| 可中断锁 | 不支持(等待时无法中断) | 支持 (lockInterruptibly()) |
ReentrantLock |
| 尝试获取 | 不支持 | 支持 (tryLock()) |
ReentrantLock |
| 条件队列 | 单一等待队列(wait/notify) |
多条件队列 (Condition) |
ReentrantLock |
| 性能 | Java 6 后优化明显,低竞争下快 | 高并发下更稳定,减少锁竞争 | 视场景而定 |
| 锁升级 | 支持(偏向 -> 轻量 -> 重量) | 不支持(一直都是重量级逻辑,但实现高效) | Synchronized |
三、代码示例
java
package locktest;
public class SynchronizedDemo {
private int count = 0;
private final Object lock = new Object();
// 1. 修饰代码块
public void incrementBlock() {
synchronized (lock) { // 或 synchronized (this)
System.out.println(Thread.currentThread().getName() + " count: " + count);
count++;
}
}
// 2. 修饰实例方法
public synchronized void incrementMethod() {
count++;
}
// 3. 修饰静态方法
public static synchronized void staticMethod() {
// 锁住的是 SynchronizedDemo.class
}
public static void main(String[] args) {
SynchronizedDemo demo = new SynchronizedDemo();
// 启动多个线程测试
for (int i = 0; i < 5; i++) {
new Thread(() -> {
for (int j = 0; j < 10; j++) {
demo.incrementBlock();
}
}).start();
}
}
}
java
package locktest;
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockDemo {
private int count = 0;
private final ReentrantLock lock = new ReentrantLock();
public void increment() {
lock.lock(); // 获取锁
try {
count++;
System.out.println(Thread.currentThread().getName() + " count: " + count);
} finally {
lock.unlock(); // 必须在 finally 中释放锁,否则异常会导致死锁
}
}
/**
* 用法一:可重入性演示
* 同一线程可以重复获取同一把锁(state 递增),
* 只要 lock 与 unlock 次数配对即可,不会像不可重入锁那样自己等自己。
*/
public void reentrant() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName()
+ " 第一次获取锁,重入次数: " + lock.getHoldCount());
lock.lock(); // 重入:同一线程再次获取,不会阻塞
try {
System.out.println(Thread.currentThread().getName()
+ " 第二次获取锁,重入次数: " + lock.getHoldCount());
} finally {
lock.unlock();
}
} finally {
lock.unlock();
}
}
/**
* 用法二:tryLock() 非阻塞尝试获取锁
* 拿不到锁立刻返回 false,不会挂起线程,适合"拿不到就干别的"的场景,
* 也是避免死锁的常用手段(拿不到就释放已持有的锁再重试)。
*/
public void tryLockDemo() {
if (lock.tryLock()) {
try {
System.out.println(Thread.currentThread().getName() + " tryLock 成功,执行业务");
sleep(1000); // 模拟业务耗时,让其他线程 tryLock 失败
} finally {
lock.unlock();
}
} else {
System.out.println(Thread.currentThread().getName() + " tryLock 失败,去做别的事");
}
}
/**
* 用法三:tryLock(timeout) 限时等待获取锁
* 在指定时间内没拿到锁就放弃,比无限期 lock() 更可控。
*/
public void tryLockWithTimeout() {
try {
// 等待500毫秒,拿不到就走
if (lock.tryLock(500, TimeUnit.MILLISECONDS)) {
try {
System.out.println(Thread.currentThread().getName() + " 500ms 内拿到锁");
sleep(1000);
} finally {
lock.unlock();
}
} else {
System.out.println(Thread.currentThread().getName() + " 等待 500ms 超时,放弃获取");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 等锁期间被中断
}
}
/**
* 用法四:lockInterruptibly() 可中断地获取锁
* 与 lock() 的区别:阻塞等锁期间可以响应 interrupt(),
* 避免线程在锁上无限期死等(synchronized 做不到这一点)。
*/
public void lockInterruptiblyDemo() {
try {
/**
* lockInterruptibly() 的作用:以可响应中断的方式获取锁。
* 它内部大致做了这几件事:
* 先检查中断标志:如果当前线程在调用时已经被中断,直接抛 InterruptedException,锁都不去抢
* 尝试获取锁:锁空闲就直接拿到(和 lock() 一样);如果是自己已持有,重入计数 +1
* 拿不到就挂起等待:进入 AQS 等待队列阻塞,但关键区别在于------阻塞期间如果被其他线程 interrupt(),会立刻从队列中退出并抛出 InterruptedException,而不是继续死等
*/
lock.lockInterruptibly();
try {
System.out.println(Thread.currentThread().getName() + " 获取锁成功");
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + " 等锁时被中断,放弃等待");
}
}
/**
* 用法五:公平锁
* new ReentrantLock(true) 创建公平锁,等待最久的线程优先获取,
* 避免线程饥饿,但吞吐量比默认的非公平锁低。
*/
static class FairLockDemo {
private final ReentrantLock fairLock = new ReentrantLock(true);
public void access() {
for (int i = 0; i < 2; i++) {
fairLock.lock();
try {
// 公平模式下各线程大致轮流打印;换成非公平锁则可能被同一线程连续抢占
System.out.println(Thread.currentThread().getName() + " 获取到公平锁");
} finally {
fairLock.unlock();
}
}
}
}
/**
* 用法六:Condition 实现等待/通知(生产者-消费者)
* 一把锁可以创建多个 Condition,实现精确唤醒;
* 相比 synchronized 的 wait/notify 只有一个等待队列更灵活。
*/
static class BoundedQueue {
private final Queue<Integer> queue = new LinkedList<>();
private final int capacity = 3;
private final ReentrantLock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition(); // 队列未满,生产者等在这里
private final Condition notEmpty = lock.newCondition(); // 队列非空,消费者等在这里
public void put(int value) throws InterruptedException {
lock.lock();
try {
while (queue.size() == capacity) { // 必须用 while 防止虚假唤醒
notFull.await(); // 释放锁并等待
}
queue.offer(value);
System.out.println(Thread.currentThread().getName() + " 生产: " + value);
notEmpty.signal(); // 只唤醒消费者,不会误唤醒其他生产者
} finally {
lock.unlock();
}
}
public int take() throws InterruptedException {
lock.lock();
try {
while (queue.isEmpty()) {
notEmpty.await();
}
int value = queue.poll();
System.out.println(Thread.currentThread().getName() + " 消费: " + value);
notFull.signal(); // 只唤醒生产者
return value;
} finally {
lock.unlock();
}
}
}
public static void main(String[] args) throws InterruptedException {
ReentrantLockDemo demo = new ReentrantLockDemo();
// 启动多个线程测试
for (int i = 0; i < 5; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
demo.increment();
}
}).start();
}
sleep(1000);
System.out.println("\n===== 1. 可重入性 =====");
demo.reentrant();
System.out.println("\n===== 2. tryLock 非阻塞获取 =====");
Thread t1 = new Thread(demo::tryLockDemo, "tryLock-线程1");
Thread t2 = new Thread(demo::tryLockDemo, "tryLock-线程2"); // 大概率失败
t1.start();
sleep(100);
t2.start();
t1.join();
t2.join();
System.out.println("\n===== 3. tryLock 超时获取 =====");
Thread t3 = new Thread(demo::tryLockDemo, "占锁线程"); // 先占锁 1 秒
Thread t4 = new Thread(demo::tryLockWithTimeout, "限时等待线程"); // 等 500ms 超时
t3.start();
sleep(100);
t4.start();
t3.join();
t4.join();
System.out.println("\n===== 4. lockInterruptibly 可中断等锁 =====");
demo.lock.lock(); // 主线程先占住锁
Thread t5 = new Thread(demo::lockInterruptiblyDemo, "可中断线程");
t5.start();
sleep(100);
t5.interrupt(); // 中断等锁中的线程,它会立刻退出而不是死等
t5.join();
demo.lock.unlock();
System.out.println("\n===== 5. 公平锁 =====");
FairLockDemo fair = new FairLockDemo();
for (int i = 0; i < 3; i++) {
new Thread(fair::access, "公平锁-线程" + i).start();
}
sleep(500);
System.out.println("\n===== 6. Condition 生产者-消费者 =====");
BoundedQueue boundedQueue = new BoundedQueue();
new Thread(() -> {
try {
for (int i = 1; i <= 5; i++) {
boundedQueue.put(i);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "生产者").start();
new Thread(() -> {
try {
for (int i = 1; i <= 5; i++) {
boundedQueue.take();
sleep(200); // 消费慢于生产,队列会被填满,触发 notFull.await()
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "消费者").start();
}
private static void sleep(long millis) {
try {
TimeUnit.MILLISECONDS.sleep(millis);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
bash
Thread-4 count: 47
Thread-4 count: 48
Thread-4 count: 49
Thread-4 count: 50
===== 1. 可重入性 =====
main 第一次获取锁,重入次数: 1
main 第二次获取锁,重入次数: 2
===== 2. tryLock 非阻塞获取 =====
tryLock-线程1 tryLock 成功,执行业务
tryLock-线程2 tryLock 失败,去做别的事
===== 3. tryLock 超时获取 =====
占锁线程 tryLock 成功,执行业务
限时等待线程 等待 500ms 超时,放弃获取
===== 4. lockInterruptibly 可中断等锁 =====
可中断线程 等锁时被中断,放弃等待
===== 5. 公平锁 =====
公平锁-线程0 获取到公平锁
公平锁-线程1 获取到公平锁
公平锁-线程2 获取到公平锁
公平锁-线程0 获取到公平锁
公平锁-线程1 获取到公平锁
公平锁-线程2 获取到公平锁
===== 6. Condition 生产者-消费者 =====
生产者 生产: 1
生产者 生产: 2
生产者 生产: 3
消费者 消费: 1
生产者 生产: 4
消费者 消费: 2
生产者 生产: 5
消费者 消费: 3
消费者 消费: 4
消费者 消费: 5
进程已结束,退出代码为 0
异常场景对比
java
import java.util.concurrent.locks.ReentrantLock;
public class ExceptionDemo {
// Synchronized:异常自动释放锁
public synchronized void synchronizedMethod() {
System.out.println("Synchronized 获取锁");
throw new RuntimeException("Synchronized 异常"); // 锁会自动释放
}
// ReentrantLock:异常不会自动释放锁,必须手动处理
private final ReentrantLock lock = new ReentrantLock();
public void reentrantLockMethod() {
lock.lock();
try {
System.out.println("ReentrantLock 获取锁");
throw new RuntimeException("ReentrantLock 异常");
} finally {
lock.unlock(); // 必须手动释放,否则锁永远不释放
System.out.println("ReentrantLock 锁已释放");
}
}
public static void main(String[] args) {
ExceptionDemo demo = new ExceptionDemo();
// 测试 Synchronized
try {
demo.synchronizedMethod();
} catch (Exception e) {
System.out.println("捕获异常: " + e.getMessage());
}
System.out.println("---");
// 测试 ReentrantLock
try {
demo.reentrantLockMethod();
} catch (Exception e) {
System.out.println("捕获异常: " + e.getMessage());
}
}
}
四、如何选择?
使用 Synchronized 的情况:
- 简单场景:简单的同步块或方法。
- 代码简洁性 :不想处理
try/finally和手动解锁。 - 低竞争环境:JVM 会自动进行锁优化(偏向锁、轻量级锁),性能很好。
- 资源受限:不需要高级功能(如中断、公平锁、多条件)。
使用 ReentrantLock 的情况:
- 高并发竞争:需要更精细的并发控制。
- 需要可中断锁:避免死等。
- 需要尝试获取锁:防止死锁,实现非阻塞算法。
- 需要公平锁:防止线程饥饿。
- 复杂同步结构 :需要使用多个
Condition进行精准线程协作。
五、总结
能用
synchronized就用synchronized,除非你需要ReentrantLock的高级特性。
synchronized 是 JVM 的内置公民,随着 JDK 版本的升级不断优化,且写法简单不易出错。ReentrantLock 则是并发专家的工具箱,当你需要打破常规(如中断、限时等待、公平性等)时,它是唯一的选择。
面试速记口诀:
- Synchronized:简单自动,非公平,单一条件
- ReentrantLock:手动灵活,可公平,多条件,可中断,可尝试
六、最佳实践
- 锁的范围最小化:只锁必要的代码块
- 避免嵌套锁:减少死锁风险
- 使用超时机制 :
tryLock防止死锁 - 正确处理异常 :
ReentrantLock必须在finally中释放 - 考虑性能 :低竞争用
synchronized,高并发用ReentrantLock
七、常见面试题
-
Q: Synchronized 和 ReentrantLock 哪个性能更好?
A: Java 6 之后,低竞争下 synchronized 性能更好(JVM 优化);高并发下 ReentrantLock 更稳定。
-
Q: 什么情况下会发生死锁?如何避免?
A: 多个线程互相等待对方释放锁时发生死锁。避免方法:按顺序获取锁、使用超时、避免嵌套锁。
-
Q: ReentrantLock 的 Condition 有什么优势?
A: 可以创建多个等待队列,实现精准唤醒,避免
notifyAll()唤醒所有线程造成的性能浪费。 -
Q: 公平锁一定比非公平锁好吗?
A: 不一定。公平锁保证顺序但性能较低(线程切换开销);非公平锁吞吐量更高,但可能导致线程饥饿。
-
Synchronized 为什么要有锁升级?
锁升级是为了平衡内存开销和 CPU 开销。
偏向锁:在无竞争时,连 CAS 操作都省略了,仅仅在对象头存个 ID,性能最好(空间换时间)。
轻量级锁:在竞争很少时,使用 CAS 自旋,避免线程挂起和唤醒的系统调用开销(CPU 换时间)。
重量级锁:在竞争激烈时,让拿不到锁的线程进入阻塞状态(Park),释放 CPU 资源,避免空转(时间换资源)。