跳到主要内容

锁机制详解

锁是并发安全的核心工具。选择合适的锁类型能在“安全”与“性能”之间取得平衡,也是一线面试的高频考点。


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
  • 读多写少 → ReentrantReadWriteLockStampedLock
  • 对延迟极致敏感、且有容错回退逻辑 → StampedLock 乐观读
  • 自定义同步组件 → LockSupport + AQS

高频面试题

  1. synchronizedReentrantLock 有何差异?
    是否可自定义公平性、可中断、尝试加锁、多个条件队列、监控使用情况等。

  2. 什么是可重入?
    持有锁的线程可以再次进入同一锁不会被阻塞,防止递归调用死锁。

  3. 如何排查死锁?
    使用 jstackjconsole,观察线程状态和锁依赖;代码层面统一加锁顺序或超时加锁。

  4. 读写锁一定比互斥锁快吗?
    只有在读占主导且写少的情况下才有优势,否则写锁频繁导致读被阻塞,性能更差。

  5. StampedLock 能替代 ReentrantReadWriteLock 吗?
    不完全。它不可重入、不支持条件队列,但在读多写少且对性能极敏感时更有优势。


小结

  • 理解锁的特性、成本与适用场景,才能写出既安全又高效的并发代码。
  • 面试更关注“为什么需要这种锁”和“如何排查问题”,多用真实案例阐述。