锁机制详解
锁是并发安全的核心工具。选择合适的锁类型能在“安全”与“性能”之间取得平衡,也是一线面试的高频考点。
synchronized:从语法糖到对象头
- 语法:修饰实例方法(锁当前实例)、静态方法(锁 Class 对象)或代码块(锁任意对象)。
- 编译结果:生成
monitorenter/monitorexit指令,JVM 把它映射到底层对象监视器。 - 锁升级过程:偏向锁 → 轻量级锁(自旋) → 重量级锁(OS 互斥量)。锁竞争越激烈,升级级别越高。
- 可重入 & 可见性:获取锁会刷新工作内存;同一线程可重复进入。
public class Counter {
private int total;
public synchronized void incr() {
total++;
}
public int read() {
synchronized (this) {
return total;
}
}
}
面试提示:
synchronized在 JDK 1.6 之后已经通过偏向锁、自旋等优化,性能并不一定弱于ReentrantLock。
ReentrantLock:可定制的互斥锁
| 能力 | 说明 |
|---|---|
| 公平/非公平 | 构造时可指定,默认非公平以提升吞吐 |
| 可中断 | lockInterruptibly() 支持在等待锁时响应中断 |
| 尝试加锁 | tryLock() 可立即返回或设置超时时间 |
| 条件队列 | newCondition() 创建多个等待队列,实现精准唤醒 |
class InventoryService {
private final ReentrantLock lock = new ReentrantLock();
private final Condition notEmpty = lock.newCondition();
private int stock = 0;
void put(int delta) throws InterruptedException {
lock.lock();
try {
stock += delta;
notEmpty.signalAll();
} finally {
lock.unlock();
}
}
int take() throws InterruptedException {
lock.lock();
try {
while (stock == 0) {
notEmpty.await();
}
return stock--;
} finally {
lock.unlock();
}
}
}
面试常问:
ReentrantLock如何精准唤醒?答:利用Condition拆分等待队列,谁等待就唤醒谁,避免notifyAll惹起“惊群”。
ReentrantReadWriteLock:读写分离
适合读多写少的场景。读锁可并发获得,写锁互斥且会阻塞后续读。
- 降级:写锁可在持有时获取读锁实现降级(需按写→读→释放写锁),反向升级不允许。
- 公平性:默认非公平;公平模式下读也会排队,吞吐降低。
class ConfigCenter {
private final Map<String, String> data = new HashMap<>();
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
String get(String key) {
rwLock.readLock().lock();
try {
return data.get(key);
} finally {
rwLock.readLock().unlock();
}
}
void update(String key, String value) {
rwLock.writeLock().lock();
try {
data.put(key, value);
} finally {
rwLock.writeLock().unlock();
}
}
}
面试题:为什么写锁释放前可以持有读锁?解释“降级”用于在刷新后立即对外提供一致视图。
StampedLock:乐观读与写锁
- 三种模式:写锁、悲观读锁、乐观读锁。
- 乐观读:
tryOptimisticRead()返回一个stamp,读完后validate(stamp)判断期间是否被写锁抢占,失败再回退到悲观读。 - 不可重入:与
ThreadLocal绑定关系弱,忘记释放会导致死锁;常用try-finally。
class Point {
private double x, y;
private final StampedLock lock = new StampedLock();
double distanceFromOrigin() {
long stamp = lock.tryOptimisticRead();
double cx = x, cy = y;
if (!lock.validate(stamp)) {
stamp = lock.readLock();
try {
cx = x;
cy = y;
} finally {
lock.unlockRead(stamp);
}
}
return Math.hypot(cx, cy);
}
}
面试题:
StampedLock为何乐观读更快?答:无锁无阻塞,避免了读写竞争下的上下文切换,但必须在读取后验证。
LockSupport:构建同步原语的底座
LockSupport.park() / unpark(thread) 提供“许可”机制,是 AQS、ReentrantLock 等的基础。
public void parkExample() throws InterruptedException {
Thread worker = new Thread(() -> {
System.out.println("wait");
LockSupport.park();
System.out.println("resume");
});
worker.start();
TimeUnit.SECONDS.sleep(1);
LockSupport.unpark(worker);
}
park()可响应interrupt,但不会抛异常,需要Thread.interrupted()手动重置。unpark先于park调用也有效,因为许可会被记录(单个许可,不可累加)。
选型建议
- 临界区简单 →
synchronized - 需要尝试加锁/可中断/多个条件队列 →
ReentrantLock - 读多写少 →
ReentrantReadWriteLock或StampedLock - 对延迟极致敏感、且有容错回退逻辑 →
StampedLock乐观读 - 自定义同步组件 →
LockSupport+ AQS
高频面试题
-
synchronized与ReentrantLock有何差异?
是否可自定义公平性、可中断、尝试加锁、多个条件队列、监控使用情况等。 -
什么是可重入?
持有锁的线程可以再次进入同一锁不会被阻塞,防止递归调用死锁。 -
如何排查死锁?
使用jstack、jconsole,观察线程状态和锁依赖;代码层面统一加锁顺序或超时加锁。 -
读写锁一定比互斥锁快吗?
只有在读占主导且写少的情况下才有优势,否则写锁频繁导致读被阻塞,性能更差。 -
StampedLock 能替代 ReentrantReadWriteLock 吗?
不完全。它不可重入、不支持条件队列,但在读多写少且对性能极敏感时更有优势。
小结
- 理解锁的特性、成本与适用场景,才能写出既安全又高效的并发代码。
- 面试更关注“为什么需要这种锁”和“如何排查问题”,多用真实案例阐述。