synchronized vs ReentrantLock:不只是"可重入",深挖公平锁和条件变量
synchronized vs ReentrantLock:不只是"可重入",深挖公平锁和条件变量
适读人群:Java中高级开发 | 难度:★★★★☆ | 出现频率:高
开篇故事
有一次面试,候选人简历上写"熟悉Java并发编程",我就问了synchronized和ReentrantLock的区别。
他回答了四点:可重入、性能、公平锁、可中断。
听起来挺全面的,我就追问:"可重入锁是指什么?synchronized也是可重入的,你能说一下ReentrantLock的可重入是怎么实现的吗?"
他支支吾吾说了半天,最后说"和synchronized一样"。
我又问:"你提到了公平锁,能说一下公平锁和非公平锁的具体实现差异吗?什么场景下用公平锁?"
他说:公平锁就是按顺序来……然后就说不下去了。
这个候选人背了一个答案模板,但没有深入理解。今天我要带你做到真正的深入。
一、高频考点拆解
这道题的考察维度:
第一维:功能对比 synchronized vs ReentrantLock的功能差异,这是最基础的,要全面。
第二维:底层实现 synchronized的Monitor机制,ReentrantLock的AQS(AbstractQueuedSynchronizer),这是核心原理。
第三维:应用场景 什么时候用synchronized,什么时候必须用ReentrantLock(需要公平锁、需要条件变量、需要可中断等)。
二、深度原理分析
2.1 synchronized的底层实现——Monitor
每个Java对象都关联一个Monitor(监视器锁),Monitor是OS层面的重量级锁,在JDK早期版本性能较差。JDK 6引入了锁升级机制来优化性能。
锁升级是单向的(偏向 → 轻量级 → 重量级),不能降级。
synchronized的字节码:
monitorenter // 获取对象的监视器锁
// 同步代码块
monitorexit // 释放锁对于synchronized方法,是通过方法的ACC_SYNCHRONIZED标志实现的,不是monitorenter/monitorexit指令。
2.2 ReentrantLock的底层实现——AQS
ReentrantLock基于AQS(AbstractQueuedSynchronizer,抽象队列同步器)实现。AQS是Java并发包的基石,CountDownLatch、Semaphore、ReadWriteLock都基于它实现。
AQS的核心是一个int类型的状态变量和一个等待队列(CLH队列的变体)。
获取锁的流程:
可重入的实现:
// ReentrantLock非公平锁的tryAcquire简化版
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// state=0,没有线程持有锁,CAS获取
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
} else if (current == getExclusiveOwnerThread()) {
// 当前线程已经持有锁(可重入!)
int nextc = c + acquires;
setState(nextc); // state递增,记录重入次数
return true;
}
return false;
}
// 释放锁
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
// state减到0才真正释放
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}2.3 公平锁 vs 非公平锁
非公平锁(默认):
// 非公平锁:新来的线程直接尝试CAS,不管队列里有没有等待的线程
final void lock() {
if (compareAndSetState(0, 1)) // 直接插队!
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}公平锁:
// 公平锁:先检查队列里有没有等待的线程,有的话直接排队
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// hasQueuedPredecessors():检查队列里有没有等待更久的线程
if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// ...可重入逻辑
}公平锁 vs 非公平锁的性能差异:
非公平锁性能通常远优于公平锁,原因是:
- 公平锁严格排队,每次锁释放后必须唤醒队头线程,有大量线程上下文切换开销
- 非公平锁允许"插队",刚好来了一个新线程可以直接获取锁,避免了一次线程挂起+唤醒的开销
- 实测非公平锁吞吐量是公平锁的5-10倍
什么场景用公平锁:当业务要求严格按顺序处理,不能容忍饥饿(某个线程长时间获取不到锁)时。比如有优先级保证需求的队列处理。
2.4 条件变量(Condition)
ReentrantLock配合Condition可以实现精确的线程等待/通知,这是synchronized+wait/notify无法做到的。
synchronized只有一个等待队列(waitSet),所有wait的线程都在同一个队列里,notifyAll会唤醒所有线程,造成竞争。
ReentrantLock可以创建多个Condition,不同类型的等待线程分开存放,可以精确唤醒特定类型的等待线程。
三、标准答案 + 代码验证
3.1 功能对比表格
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 锁的类型 | 隐式锁(关键字) | 显式锁(API) |
| 可重入 | 是 | 是 |
| 公平锁 | 否(非公平) | 可选(构造器参数) |
| 条件变量 | 一个(wait/notify) | 多个(newCondition) |
| 可中断 | 否 | 是(lockInterruptibly) |
| 超时获取 | 否 | 是(tryLock(timeout)) |
| 非阻塞获取 | 否 | 是(tryLock()) |
| 必须手动释放 | 否(自动) | 是(必须在finally中unlock) |
| 底层实现 | JVM Monitor | AQS |
3.2 生产者消费者:Condition精确唤醒
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class ProducerConsumerWithCondition {
private final Queue<Integer> queue = new LinkedList<>();
private final int capacity = 5;
private final ReentrantLock lock = new ReentrantLock();
// 两个条件变量:一个专门给生产者等,一个专门给消费者等
private final Condition notFull = lock.newCondition(); // 队列满时生产者等
private final Condition notEmpty = lock.newCondition(); // 队列空时消费者等
public void produce(int value) throws InterruptedException {
lock.lock();
try {
while (queue.size() == capacity) {
System.out.println("队列已满,生产者等待...");
notFull.await(); // 生产者在notFull条件上等待
}
queue.offer(value);
System.out.println("生产:" + value + ",队列大小:" + queue.size());
notEmpty.signal(); // 只唤醒一个消费者,而不是唤醒所有线程
} finally {
lock.unlock();
}
}
public int consume() throws InterruptedException {
lock.lock();
try {
while (queue.isEmpty()) {
System.out.println("队列为空,消费者等待...");
notEmpty.await(); // 消费者在notEmpty条件上等待
}
int value = queue.poll();
System.out.println("消费:" + value + ",队列大小:" + queue.size());
notFull.signal(); // 只唤醒一个生产者
return value;
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
ProducerConsumerWithCondition pc = new ProducerConsumerWithCondition();
// 3个生产者
for (int i = 0; i < 3; i++) {
final int id = i;
new Thread(() -> {
try {
for (int j = 0; j < 5; j++) {
pc.produce(id * 10 + j);
Thread.sleep(100);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();
}
// 2个消费者
for (int i = 0; i < 2; i++) {
new Thread(() -> {
try {
for (int j = 0; j < 7; j++) {
pc.consume();
Thread.sleep(200);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();
}
}
}3.3 可中断锁和超时锁
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.TimeUnit;
public class ReentrantLockAdvanced {
private final ReentrantLock lock = new ReentrantLock();
// 可中断锁:等待过程中可以被中断,而不是无限等待
public void lockInterruptiblyDemo() {
Thread t = new Thread(() -> {
try {
lock.lockInterruptibly(); // 可以被中断
try {
System.out.println("获取到锁,处理业务...");
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
System.out.println("等待锁的过程中被中断,可以优雅处理");
}
});
t.start();
t.interrupt(); // 中断等待的线程
}
// 超时锁:等待一段时间,超时返回false
public void tryLockWithTimeout() {
try {
if (lock.tryLock(500, TimeUnit.MILLISECONDS)) {
try {
System.out.println("500ms内获取到了锁");
// 处理业务
} finally {
lock.unlock();
}
} else {
System.out.println("500ms内没获取到锁,放弃本次操作");
// 可以重试、降级处理等
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
// 非阻塞尝试获取锁
public void tryLockDemo() {
if (lock.tryLock()) { // 立即尝试,不等待
try {
System.out.println("立即获取到了锁");
} finally {
lock.unlock();
}
} else {
System.out.println("锁被占用,立即返回");
}
}
}四、面试官追问
追问1:synchronized在JDK6之后做了哪些优化,为什么现在说它性能不差?
我的回答:JDK6对synchronized做了四个主要优化。第一是锁升级(偏向锁→轻量级锁→重量级锁),无竞争时用偏向锁只需标记线程ID,轻微竞争时用CAS自旋,只有真正激烈竞争才升级到OS互斥量。第二是锁消除,JIT编译器通过逃逸分析,发现一个锁对象不可能被多线程访问时,直接消除这个锁,减少不必要的同步开销。第三是锁粗化,把相邻的多个synchronized块合并成一个,减少加锁解锁次数。第四是自适应自旋,轻量级锁的自旋次数不固定,根据历史成功率动态调整。这些优化让synchronized在大多数场景下和ReentrantLock性能差不多。
追问2:什么情况下一定要用ReentrantLock而不是synchronized?
我的回答:有四种场景必须用ReentrantLock。第一,需要公平锁时,synchronized只有非公平锁,无法控制顺序。第二,需要可中断等待时,synchronized的等待不能被中断,ReentrantLock的lockInterruptibly()可以响应中断。第三,需要超时获取锁时,synchronized只能无限等待,tryLock(timeout)可以设置超时时间,避免死等。第四,需要多个条件变量时,synchronized只有一个等待队列,通过notifyAll唤醒所有线程再竞争,效率低;ReentrantLock可以创建多个Condition,精确唤醒特定线程,实现更细粒度的控制,比如生产者-消费者模式。
追问3:AQS除了用在ReentrantLock,还用在哪些地方?
我的回答:AQS是Java并发包的核心框架,大量工具类基于它实现。CountDownLatch基于AQS实现计数器同步,state初始值是latch数量,countDown()递减state,await()等待state变为0。Semaphore基于AQS实现信号量,tryAcquire()通过CAS减少state,release()增加state。ReadWriteLock(ReentrantReadWriteLock)用一个int的高16位表示读锁计数,低16位表示写锁计数,读锁是共享锁,写锁是独占锁。CyclicBarrier内部用ReentrantLock+Condition实现屏障等待和全部到达后的通知。
五、同类题目举一反三
volatile关键字能替代synchronized吗?
不能完全替代。volatile保证了可见性和有序性(禁止重排序),但不保证原子性。volatile int count; count++不是原子操作,并发下仍然会有问题。volatile适合用于:状态标志位(boolean flag)、double check单例模式(防止对象未完全初始化就被看到)、一写多读的场景。需要原子性时,要用synchronized或AtomicInteger等原子类。
六、踩坑实录
坑一:synchronized方法中抛出异常,锁会释放吗?
会。synchronized是关键字,JVM保证即使方法抛出异常,也会自动释放锁(类似finally语义)。但ReentrantLock就不同了,如果忘了在finally中unlock,异常抛出后锁就永远释放不了,造成死锁。我见过好几次这种低级错误,有人在ReentrantLock代码中没有写try-finally,异常导致unlock没执行,后续所有请求全部阻塞。
坑二:锁对象选错
// 错误:用Integer做锁对象,Integer有缓存,可能多个地方用同一个对象
synchronized (user.getId()) { // Integer[-128, 127]范围内会被缓存复用
// 可能无意中和其他代码共享同一把锁
}
// 错误:用String字面量做锁对象
synchronized ("lock") { // 字符串常量池中只有一个"lock"对象
// 整个应用所有用"lock"的地方共享同一把锁
}
// 正确:用明确的、专属的锁对象
private final Object lock = new Object();
synchronized (lock) { ... }坑三:ReentrantLock忘记unlock导致死锁
// 错误写法
public void badMethod() {
lock.lock();
if (someCondition()) {
return; // 提前return,没有unlock!
}
doSomething();
lock.unlock(); // 只有走到这里才unlock
}
// 正确写法
public void goodMethod() {
lock.lock();
try {
if (someCondition()) {
return; // finally中仍然会unlock
}
doSomething();
} finally {
lock.unlock(); // 无论如何都会执行
}
}七、总结
synchronized和ReentrantLock不是简单的替代关系,而是各有适用场景:
- 代码简单、无特殊需求:用synchronized,JDK6后性能不差,不需要手动unlock
- 需要公平锁、可中断、超时、多条件变量:用ReentrantLock
面试时不要只说"ReentrantLock功能更强",要能说出具体功能差异和底层原理(AQS+CAS vs Monitor+synchronized字节码),同时说清楚公平锁的实现和适用场景,这才是大厂面试官想要的回答。
