面试必备:synchronized 与 ReentrantLock 区别
先说结论
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 底层实现 | JVM 内置关键字 | JDK 层面 API |
| 锁类型 | 非公平锁 | 公平/非公平可配 |
| 等待唤醒 | Object.wait/notify | Condition.await/signal |
| 可中断 | 否 | 支持 tryLock |
| 多条件 | 只能有一个 | 可绑定多个 Condition |
| 释放方式 | 自动释放 | 必须 finally 释放 |
synchronized 详解
用法
// 1. 修饰代码块(指定锁对象)
public void method() {
synchronized (this) {
// 临界区
}
}
// 2. 修饰实例方法(锁 this)
public synchronized void method() {
// 临界区
}
// 3. 修饰静态方法(锁类对象)
public synchronized static void staticMethod() {
// 临界区
}
底层原理(JDK 1.6+)
锁升级过程
无锁 → 偏向锁 → 轻量级锁 → 重量级锁
↓
只有一个线程
↓
多个线程竞争
↓
竞争激烈
| 锁状态 | 原理 | 适用场景 |
|---|---|---|
| 偏向锁 | 对象头记录线程 ID | 只有一个线程 |
| 轻量级锁 | CAS + 自旋 | 短时间竞争 |
| 重量级锁 | OSMutex | 长时间竞争 |
对象头结构(64位 JVM)
┌─────────────────────────────────────────────────────┐
│ Mark Word (64 bits) │
├─────────────────────────────────────────────────────┤
│ 无锁:25位 hashCode | 4位分代年龄 | 1位偏向锁 | 2位锁类型 │
│ 偏向锁:54位线程ID | 2位 Epoch | 4位分代年龄 | 1位偏向 | 2位 │
│ 轻量级锁:62位指向栈中锁记录的指针 │
│ 重量级锁:62位指向互斥量(重量级锁)的指针 │
└─────────────────────────────────────────────────────┘
synchronized 的特点
- 自动释放:代码块执行完或异常,自动释放锁
- 可重入:同一个线程可以多次获取同一把锁
- 非公平:不保证等待顺序
ReentrantLock 详解
基本用法
// 创建可重入锁(默认非公平)
private final ReentrantLock lock = new ReentrantLock();
// 获取锁(阻塞)
public void method() {
lock.lock();
try {
// 临界区
} finally {
lock.unlock(); // 必须手动释放
}
}
// 尝试获取锁(非阻塞)
public void tryMethod() {
if (lock.tryLock()) {
try {
// 临界区
} finally {
lock.unlock();
}
} else {
// 获取失败
}
}
// 带超时获取
public void timeoutMethod() {
try {
if (lock.tryLock(5, TimeUnit.SECONDS)) {
try {
// 临界区
} finally {
lock.unlock();
}
}
} catch (InterruptedException e) {
// 等待被中断
}
}
公平锁 vs 非公平锁
// 公平锁:按等待顺序获取
ReentrantLock fairLock = new ReentrantLock(true);
// 非公平锁:可能有插队(默认)
ReentrantLock unfairLock = new ReentrantLock(false);
// synchronized 是非公平锁
公平锁开销大,因为要维护等待队列。非公平锁效率高,但可能产生"饥饿"。
Condition 条件变量
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
// 等待
public void await() throws InterruptedException {
lock.lock();
try {
while (/*条件不满足*/) {
condition.await(); // 等待,释放锁
}
// 条件满足,继续执行
} finally {
lock.unlock();
}
}
// 唤醒
public void signal() {
lock.lock();
try {
// 条件满足,唤醒一个等待线程
condition.signal();
} finally {
lock.unlock();
}
}
对比:
synchronized+Object.wait():只能有一个等待集ReentrantLock+Condition:可以创建多个条件
对比详解
1. 等待唤醒机制
// synchronized
synchronized (obj) {
while (条件不满足) {
obj.wait();
}
// 处理
}
obj.notify(); // 或 notifyAll
// ReentrantLock + Condition
lock.lock();
try {
while (条件不满足) {
condition.await();
}
// 处理
} finally {
lock.unlock();
}
condition.signal(); // 或 signalAll
2. 可中断等待
// ReentrantLock 支持
public void interruptible() throws InterruptedException {
lock.lockInterruptibly(); // 可中断的获取
try {
// 临界区
} finally {
lock.unlock();
}
}
synchronized 不支持中断,只能一直等待。
3. 多条件支持
// ReentrantLock 可以创建多个 Condition
private final ReentrantLock lock = new ReentrantLock();
private final Condition conditionA = lock.newCondition();
private final Condition conditionB = lock.newCondition();
// 等待 buffer 满
conditionA.await();
// 等待 buffer 空
conditionB.await();
// 生产者唤醒消费者,消费者唤醒生产者
conditionA.signal(); // buffer 满,唤醒消费
conditionB.signal(); // buffer 空,唤醒生产
synchronized 只有一个隐式的等待集,多条件场景不灵活。
生产者-消费者示例
synchronized 版本
public class Buffer {
private int[] items = new int[100];
private int count = 0;
public synchronized void put(int item) throws InterruptedException {
while (count == items.length) {
wait(); // buffer 满
}
items[count++] = item;
notifyAll(); // 唤醒消费者
}
public synchronized int take() throws InterruptedException {
while (count == 0) {
wait(); // buffer 空
}
int item = items[--count];
notifyAll(); // 唤醒生产者
return item;
}
}
ReentrantLock 版本
public class Buffer {
private final ReentrantLock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
private int[] items = new int[100];
private int count = 0;
public void put(int item) throws InterruptedException {
lock.lock();
try {
while (count == items.length) {
notFull.await(); // buffer 满
}
items[count++] = item;
notEmpty.signal(); // 唤醒消费者
} finally {
lock.unlock();
}
}
public int take() throws InterruptedException {
lock.lock();
try {
while (count == 0) {
notEmpty.await(); // buffer 空
}
int item = items[--count];
notFull.signal(); // 唤醒生产者
return item;
} finally {
lock.unlock();
}
}
}
面试高频问题
Q1:synchronized 是可重入锁吗?
是。同一个线程可以多次获取同一把锁。
public synchronized void methodA() {
methodB(); // 可以获取,因为是同一个线程
}
public synchronized void methodB() {
// 同一把锁,可以进入
}
Q2:synchronized 和 ReentrantLock 性能哪个好?
- JDK 1.6 之前:ReentrantLock 性能更好
- JDK 1.6 之后:synchronized 引入锁升级,性能已接近
- 现在:普通场景用 synchronized(更简洁),复杂场景用 ReentrantLock(更灵活)
Q3:Lock 和 synchronized 选择?
// 简单同步 → synchronized
synchronized (obj) {
// 临界区
}
// 需要这些特性 → ReentrantLock
// - 公平锁
// - 可中断
// - 多条件
// - 超时获取
Q4:synchronized 加在静态方法和实例方法上的区别?
class User {
// 锁的是 Class 对象(User.class)
public static synchronized void staticMethod() { }
// 锁的是 this(具体对象)
public synchronized void method() { }
}
总结
选 synchronized:简单场景,代码简洁,自动释放
选 ReentrantLock:需要公平锁、多条件、可中断、超时等高级特性
synchronized:
├─ JVM 内置,字节码 monitorenter/monitorexit
├─ 自动释放
├─ 锁升级(偏向→轻量→重量)
└─ 无法中断、无多条件
ReentrantLock:
├─ JDK API,可重入
├─ 手动释放(必须 finally)
├─ 公平/非公平
├─ Condition 多条件
└─ tryLock 超时、可中断