ReentrantLock vs synchronized:从字节码和源码看本质区别
ReentrantLock vs synchronized:从字节码和源码看本质区别
适读人群:Java中级开发者、想在面试和生产中做出正确锁选择的工程师 | 阅读时长:约15分钟
开篇故事
去年有个大厂Java面试官跟我聊天,说现在面试Java并发,他最怕问到的问题就是"ReentrantLock和synchronized的区别"。
不是因为难,而是因为太多人的回答是背书式的:"ReentrantLock可以中断、可以设置公平锁、可以绑定多个Condition……"
他说,答这个不难,但他真正想考察的是:这两个家伙底层是怎么工作的,为什么synchronized在JDK 6优化后反而在某些场景比ReentrantLock快?
这个问题让我想起了2018年我们系统的一次性能事故。我们把一批synchronized代码换成了ReentrantLock,理由是"ReentrantLock性能更好",结果压测发现在低并发(<10线程)场景下,性能反而下降了10%。
后来我认真研究了两者的字节码和JVM优化路径,才明白为什么。今天把这段经历和分析讲清楚。
一、从字节码看底层差异
1.1 synchronized的字节码
// Java源码
public void syncMethod() {
synchronized (this) {
count++;
}
}对应的字节码(javap -c):
public void syncMethod();
Code:
0: aload_0
1: dup
2: astore_1
3: monitorenter // 进入监视器(加锁)
4: aload_0
5: dup
6: getfield count
9: iconst_1
10: iadd
11: putfield count
14: aload_1
15: monitorexit // 正常退出(解锁)
16: goto 24
19: astore_2
20: aload_1
21: monitorexit // 异常退出时也解锁(finally语义)
22: aload_2
23: athrow
24: return注意两个monitorexit——JVM自动生成了异常处理路径,保证即使抛出异常也能解锁。这是synchronized的一大优势:解锁是由JVM保证的,不会因为代码错误忘记解锁。
1.2 ReentrantLock的字节码
// Java源码
private final ReentrantLock lock = new ReentrantLock();
public void lockMethod() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}字节码里没有monitorenter/monitorexit,调用的是普通的invokevirtual方法调用:
public void lockMethod();
Code:
0: aload_0
1: getfield lock
4: invokevirtual ReentrantLock.lock() // 普通方法调用
7: aload_0
8: dup
9: getfield count
12: iconst_1
13: iadd
14: putfield count
17: aload_0
18: getfield lock
21: invokevirtual ReentrantLock.unlock() // 普通方法调用
...(异常处理)ReentrantLock是用普通Java代码实现的,靠AQS+CAS+LockSupport.park实现锁语义,没有JVM指令级别的特殊支持。
二、核心机制对比深度解析
2.1 功能层面的差异
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 可重入 | 支持 | 支持 |
| 公平锁 | 不支持(非公平) | 支持(new ReentrantLock(true)) |
| 可中断等待 | 不支持 | lockInterruptibly() |
| 超时获取 | 不支持 | tryLock(time, unit) |
| 多个条件变量 | 只有一个(wait/notify) | 多个Condition |
| 锁状态查询 | 不支持 | isLocked(), getQueueLength()等 |
| 自动释放 | JVM保证(即使异常) | 需手动(finally块unlock) |
2.2 性能层面的差异
JDK 6之前: synchronized底层全是重量级锁(OS mutex),ReentrantLock(基于CAS)明显更快,性能差异可达5-10倍。
JDK 6之后(引入偏向锁+轻量级锁):
- 低竞争场景:
synchronized偏向锁几乎零开销,反而比ReentrantLock的CAS+方法调用开销更小 - 中等竞争(2-4线程):
synchronized轻量级锁(CAS自旋)vs ReentrantLock(AQS CAS),基本持平 - 高竞争(大量线程):二者都退化到OS mutex,性能相近;ReentrantLock可能因为AQS的队列管理有微弱优势
JVM的Lock Elision(锁消除)和Lock Coarsening(锁粗化):
synchronized可以被JIT做这两种优化,但ReentrantLock不行(JIT不认识invokevirtual ReentrantLock.lock()是一个锁操作)。
// 这段代码,JIT可能把synchronized完全消除(如果分析发现不可能有竞争)
public String buildString() {
StringBuffer sb = new StringBuffer(); // 内部用synchronized
sb.append("Hello"); // JIT发现sb是局部变量,不可能被其他线程访问
sb.append("World"); // 所以直接消除所有synchronized
return sb.toString();
}2.3 重入性的实现差异
synchronized的重入:JVM的ObjectMonitor里有_recursions计数器,同一线程再次monitorenter时检查_owner == currentThread,是则_recursions++,不需要重新竞争。
ReentrantLock的重入:
// ReentrantLock.NonfairSync.tryAcquire
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 没人持锁,CAS尝试获取
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
} else if (current == getExclusiveOwnerThread()) {
// 当前线程已持有锁,重入
int nextc = c + acquires; // state递增
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc); // 不需要CAS,已经是持锁线程,没有竞争
return true;
}
return false;
}三、完整代码实现
3.1 公平锁与非公平锁的选择对比
package com.laozhang.concurrent.lock;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.ReentrantLock;
/**
* 公平锁vs非公平锁的性能与公平性对比
*
* 公平锁:严格FIFO,等待时间最长的线程优先获取
* 非公平锁:新来的线程直接尝试抢锁,失败再排队
*
* 测试结果(JDK 11,8核机器,16线程各执行10万次加锁解锁):
* 非公平锁:约1200ms,吞吐量更高
* 公平锁:约1800ms,每个线程获得的次数更均匀
*/
public class FairLockDemo {
private static final int THREAD_COUNT = 16;
private static final int ITERATIONS = 100_000;
private long count = 0;
// 记录每个线程获得锁的次数
private final int[] threadAcquireCounts = new int[THREAD_COUNT];
public void testLock(ReentrantLock lock, String name) throws InterruptedException {
count = 0;
for (int i = 0; i < THREAD_COUNT; i++) threadAcquireCounts[i] = 0;
CountDownLatch ready = new CountDownLatch(THREAD_COUNT);
CountDownLatch start = new CountDownLatch(1);
CountDownLatch done = new CountDownLatch(THREAD_COUNT);
for (int i = 0; i < THREAD_COUNT; i++) {
final int threadIdx = i;
new Thread(() -> {
ready.countDown();
try {
start.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
for (int j = 0; j < ITERATIONS; j++) {
lock.lock();
try {
count++;
threadAcquireCounts[threadIdx]++;
} finally {
lock.unlock();
}
}
done.countDown();
}, name + "-" + i).start();
}
ready.await();
long startTime = System.currentTimeMillis();
start.countDown();
done.await();
long elapsed = System.currentTimeMillis() - startTime;
// 统计线程获锁次数的均匀程度
long expected = (long) THREAD_COUNT * ITERATIONS / THREAD_COUNT;
long maxDeviation = 0;
for (int c : threadAcquireCounts) {
maxDeviation = Math.max(maxDeviation, Math.abs(c - expected));
}
System.out.printf("[%s] 耗时:%dms,count=%d,最大偏差:%d(%.1f%%)%n",
name, elapsed, count, maxDeviation,
100.0 * maxDeviation / expected);
}
public static void main(String[] args) throws InterruptedException {
FairLockDemo demo = new FairLockDemo();
// 预热
ReentrantLock warmupLock = new ReentrantLock();
for (int i = 0; i < 1000; i++) {
warmupLock.lock();
warmupLock.unlock();
}
demo.testLock(new ReentrantLock(false), "非公平锁");
Thread.sleep(1000); // 让系统稳定
demo.testLock(new ReentrantLock(true), "公平锁");
}
}3.2 Condition多条件变量的使用
package com.laozhang.concurrent.lock;
import java.util.ArrayDeque;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
/**
* 用ReentrantLock + 多个Condition实现有界阻塞队列
*
* 体现synchronized无法做到的"分离条件变量":
* - notFull:队列不满时唤醒生产者
* - notEmpty:队列不空时唤醒消费者
*
* 如果用synchronized + wait/notify,只有一个等待集合,
* notify()可能唤醒的是同类(两个生产者中的一个),效率低
* (LinkedBlockingQueue用了两把锁+两个Condition解决这个问题)
*/
public class BoundedBlockingQueue<T> {
private final Queue<T> queue;
private final int capacity;
private final ReentrantLock lock = new ReentrantLock();
// 两个独立的条件变量
private final Condition notFull = lock.newCondition(); // 等待"不满"
private final Condition notEmpty = lock.newCondition(); // 等待"不空"
public BoundedBlockingQueue(int capacity) {
this.capacity = capacity;
this.queue = new ArrayDeque<>(capacity);
}
/**
* 入队,队满则阻塞
*/
public void put(T item) throws InterruptedException {
lock.lock();
try {
while (queue.size() >= capacity) {
System.out.printf("[生产者-%s] 队列已满(%d),等待消费...%n",
Thread.currentThread().getName(), capacity);
notFull.await(); // 释放锁,等待notFull信号
}
queue.offer(item);
System.out.printf("[生产者-%s] 生产: %s,队列大小: %d%n",
Thread.currentThread().getName(), item, queue.size());
notEmpty.signal(); // 唤醒一个等待"不空"的消费者
} finally {
lock.unlock();
}
}
/**
* 出队,队空则阻塞
*/
public T take() throws InterruptedException {
lock.lock();
try {
while (queue.isEmpty()) {
System.out.printf("[消费者-%s] 队列为空,等待生产...%n",
Thread.currentThread().getName());
notEmpty.await(); // 释放锁,等待notEmpty信号
}
T item = queue.poll();
System.out.printf("[消费者-%s] 消费: %s,队列大小: %d%n",
Thread.currentThread().getName(), item, queue.size());
notFull.signal(); // 唤醒一个等待"不满"的生产者
return item;
} finally {
lock.unlock();
}
}
public int size() {
lock.lock();
try { return queue.size(); }
finally { lock.unlock(); }
}
public static void main(String[] args) throws InterruptedException {
BoundedBlockingQueue<Integer> bq = new BoundedBlockingQueue<>(3);
// 3个生产者,2个消费者
for (int i = 0; i < 3; i++) {
final int id = i;
new Thread(() -> {
for (int j = 0; j < 5; j++) {
try {
bq.put(id * 10 + j);
Thread.sleep(50);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}, "P-" + i).start();
}
for (int i = 0; i < 2; i++) {
new Thread(() -> {
for (int j = 0; j < 8; j++) { // 故意消费更多
try {
bq.take();
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}, "C-" + i).start();
}
}
}四、踩坑实录
坑1:忘记在finally中unlock导致死锁
报错现象: 生产环境出现线程全部BLOCKED,系统hang住,唯一重启可以解决。jstack发现大量线程等待同一把ReentrantLock,但持有锁的线程已经不存在了(线程因异常退出,但锁没有释放)。
原因分析: 和synchronized不同,ReentrantLock的解锁完全依赖开发者显式调用unlock()。如果在lock()和unlock()之间的代码抛出RuntimeException,且没有finally块,锁就永远不会释放。
// 错误写法:抛异常后锁永远不会释放
lock.lock();
doSomething(); // 这里如果抛RuntimeException
lock.unlock(); // 永远执行不到!
// 正确写法:必须finally
lock.lock();
try {
doSomething();
} finally {
lock.unlock(); // 无论如何都会执行
}解法: 用代码规范检查工具(如Checkstyle、SpotBugs)扫描所有lock.lock()后面是否有对应的finally { lock.unlock(); }。
坑2:tryLock()的非公平问题在公平锁上依然存在
报错现象: 使用new ReentrantLock(true)(公平锁),但发现某些情况下等待很久的线程还是被后来的线程插队了。
原因分析: tryLock()方法无论公平锁还是非公平锁,都直接尝试CAS获取锁,不排队。这是设计决策:tryLock()的语义就是"立刻尝试,不成功则返回false"。
// 公平锁,但tryLock()还是插队的
ReentrantLock fairLock = new ReentrantLock(true);
// 即使有线程在队列里等待,tryLock()也会直接尝试
// 如果锁当前空闲(持锁线程刚释放),tryLock()会成功,插到队列里所有等待线程前面
boolean got = fairLock.tryLock();如果要"公平地尝试获取",使用tryLock(0, TimeUnit.SECONDS):
// 这个版本会遵守公平锁的队列顺序
boolean got = fairLock.tryLock(0, TimeUnit.SECONDS);解法: 在需要严格公平的场景,避免使用tryLock()(无参版本),改用tryLock(0, TimeUnit.SECONDS)。
坑3:Condition.await()前必须重新检查条件(spurious wakeup)
报错现象: 线程从condition.await()返回后,直接使用共享数据,偶发出现数据不一致。
原因分析: JVM规范允许await()在没有收到signal()的情况下虚假唤醒(spurious wakeup)。这是OS层面的特性(pthread_cond_wait也有同样的问题)。
// 错误写法:if判断
if (queue.isEmpty()) {
condition.await();
}
T item = queue.poll(); // 虚假唤醒后,queue可能还是空的!NPE
// 正确写法:while循环
while (queue.isEmpty()) {
condition.await(); // 虚假唤醒后重新检查条件
}
T item = queue.poll(); // 能到这里,queue一定不空解法: 永远用while循环包裹condition.await(),不要用if。这不仅防虚假唤醒,也防"多个消费者被notifyAll唤醒后只有一个能消费"的竞争问题。
坑4:synchronized和ReentrantLock混用同一把锁
报错现象: 一个类里,有的方法用synchronized(this),有的方法用this.reentrantLock.lock()。期望互斥,但实际上没有互斥效果。
原因分析: synchronized(this)锁的是对象的内置监视器(ObjectMonitor),reentrantLock.lock()锁的是ReentrantLock对象内部的AQS状态,这是两把完全不同的锁。混用没有任何互斥效果。
class BrokenClass {
private final ReentrantLock lock = new ReentrantLock();
private int count = 0;
public synchronized void method1() { // 锁:this的ObjectMonitor
count++;
}
public void method2() {
lock.lock(); // 锁:lock对象的AQS
try { count++; }
finally { lock.unlock(); }
}
// method1和method2对count的修改没有互斥保护!
}解法: 选择一种锁机制,全部统一使用,不要混用。
五、总结与延伸
选synchronized还是ReentrantLock?
优先选synchronized的场景:
- 简单的代码临界区保护
- 不需要超时/中断/公平性
- 能让JIT做锁消除和锁粗化优化
- 代码简洁优先(自动解锁,不会忘记)
选ReentrantLock的场景:
- 需要
tryLock(timeout)(避免无限等待) - 需要可中断的锁等待(
lockInterruptibly()) - 需要公平锁
- 需要多个条件变量(生产者-消费者的分离等待集合)
- 需要查询锁状态(
getQueueLength()等诊断功能)
JDK版本趋势:
- JDK 6:synchronized性能大幅提升,两者差距缩小
- JDK 14:VarHandle优化AQS内部CAS,ReentrantLock性能改善
- JDK 21+:虚拟线程(Loom)场景下,synchronized阻塞会固定平台线程,而ReentrantLock配合虚拟线程可以正确park。这是虚拟线程时代选择ReentrantLock的新理由(JDK 21已解决synchronized的虚拟线程固定问题)
