Java 锁全景:synchronized、ReentrantLock、ReadWriteLock、StampedLock
Java 中有四种主流锁机制,每种都有适用场景。选错锁,性能差十倍。本文对比四种锁的原理和选型指南。
一、Java 锁全景图
scss
┌─────────────────────────────────────────────────────────────┐
│ Java 锁全景 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ 互斥锁 ┌──────────────────────────┐ │
│ │ synchronized │ │ ReentrantLock │ │
│ │ (JVM 内置) │ │ (AQS 实现) │ │
│ │ 偏向→轻量→重量 │ │ 公平/非公平 │ │
│ └─────────────────┘ └──────────────────────────┘ │
│ │
│ ┌─────────────────┐ 读写锁 ┌──────────────────────────┐ │
│ │ ReadWriteLock │ │ StampedLock (JDK 8+) │ │
│ │ 读写分离 │ │ 乐观读 + 读写分离 │ │
│ │ 读读共享 │ │ 读不加锁(乐观) → 回退 │ │
│ │ 读写互斥 │ │ 读写互斥 │ │
│ └─────────────────┘ └──────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
二、synchronized
java
// 互斥锁,JVM 内置
public class SyncDemo {
private int count = 0;
// 锁 this
public synchronized void increment() {
count++;
}
// 锁指定对象
public void method() {
synchronized(this) {
count++;
}
}
// 锁 Class
public static synchronized void staticMethod() {
// 锁 Class 对象
}
}
arduino
synchronized 特点:
优点: 简单,JVM 管理,自动释放
缺点: 不可中断,不可超时,单条件变量
锁升级: 偏向锁 → 轻量级锁 → 重量级锁
适用: 竞争不激烈的场景
三、ReentrantLock
java
// 可重入锁,AQS 实现
public class ReentrantLockDemo {
private final ReentrantLock lock = new ReentrantLock();
private int count = 0;
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock(); // 必须手动释放
}
}
// 公平锁 (FIFO)
private final ReentrantLock fairLock = new ReentrantLock(true);
// 可中断加锁
public void interruptibleLock() throws InterruptedException {
fairLock.lockInterruptibly(); // 可被 interrupt 打断
try {
// 临界区
} finally {
fairLock.unlock();
}
}
// 超时加锁
public boolean tryLockWithTimeout() throws InterruptedException {
if (lock.tryLock(3, TimeUnit.SECONDS)) {
try {
// 获取锁成功
return true;
} finally {
lock.unlock();
}
}
return false; // 超时未获取
}
// 多条件变量
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
public void awaitCondition() throws InterruptedException {
lock.lock();
try {
while (count == 0) {
notEmpty.await(); // 等待 notEmpty 条件
}
count--;
notFull.signal(); // 通知 notFull 条件
} finally {
lock.unlock();
}
}
}
objectivec
ReentrantLock 特点:
优点: 可中断、可超时、公平锁、多条件变量
缺点: 必须手动释放 (忘记 unlock → 死锁)
实现: AQS (CLH 队列 + state)
适用: 需要高级锁功能的场景
公平 vs 非公平
java
// 非公平锁 (默认): 新来的线程先尝试插队
ReentrantLock lock = new ReentrantLock(false); // 非公平
// 公平锁: 严格按 FIFO 排队
ReentrantLock lock = new ReentrantLock(true); // 公平
// 非公平锁的 tryAcquire:
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 不检查队列! 直接 CAS 获取
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// ...
}
// 公平锁的 tryAcquire:
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 先检查队列中是否有等待者!
if (!hasQueuedPredecessors() && // ← 关键区别
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// ...
}
| 对比维度 | 非公平锁 | 公平锁 |
|---|---|---|
| 吞吐量 | 高 | 低 |
| 饥饿 | 可能 | 不会 |
| 上下文切换 | 少 | 多 |
| 实时性 | 不保证 | 严格 FIFO |
四、ReadWriteLock
java
// 读写分离锁: 读读共享,读写互斥
public class CacheDemo {
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
private final ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();
private final ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();
private Map<String, Object> cache = new HashMap<>();
// 读操作: 多个线程可同时读
public Object get(String key) {
readLock.lock();
try {
return cache.get(key);
} finally {
readLock.unlock();
}
}
// 写操作: 独占,阻塞所有读和写
public void put(String key, Object value) {
writeLock.lock();
try {
cache.put(key, value);
} finally {
writeLock.unlock();
}
}
}
csharp
ReadWriteLock 读写规则:
┌──────────────┬──────────────┬──────────────┐
│ │ 读锁(共享) │ 写锁(独占) │
├──────────────┼──────────────┼──────────────┤
│ 读锁(共享) │ ✅ 共享 │ ❌ 互斥 │
│ 写锁(独占) │ ❌ 互斥 │ ❌ 互斥 │
└──────────────┴──────────────┴──────────────┘
场景:
Thread1: readLock.lock() ─┐
Thread2: readLock.lock() ─┤── 同时读,不阻塞
Thread3: readLock.lock() ─┘
Thread4: writeLock.lock() ── 等待所有读锁释放
Thread4: writeLock.lock() ── 独占写
Thread5: readLock.lock() ─── 等待写锁释放
AQS 实现读写锁
perl
ReentrantReadWriteLock 底层 (AQS state 分解):
AQS state (int, 32 bit) 分为两部分:
┌─────────────────────────────┐
│ 高 16 位: 读锁持有数 (shared) │
│ 低 16 位: 写锁重入数 (exclusive)│
└─────────────────────────────┘
读锁获取:
state 高 16 位 + 1
如果低 16 位 ≠ 0 (有写锁) → 等待
写锁获取:
state 低 16 位 + 1
如果 state ≠ 0 (有读锁或写锁) → 等待
释放:
读锁释放: 高 16 位 - 1
写锁释放: 低 16 位 - 1
ReadWriteLock 的写饥饿问题
csharp
写饥饿: 读锁持有时间过长,写锁一直无法获取
Thread1: readLock.lock() ─┐
Thread2: readLock.lock() │── 不断有新读线程进来
Thread3: readLock.lock() │
Thread4: readLock.lock() ─┘
Thread5: writeLock.lock() ──── 永远等不到!
解决: ReentrantReadWriteLock 默认非公平
→ 新读线程可能插队 → 写线程饥饿
→ 用公平模式可以缓解: new ReentrantReadWriteLock(true)
→ 但吞吐量下降
五、StampedLock(JDK 8+)
java
// StampedLock: 乐观读 + 读写分离
// 解决 ReadWriteLock 的写饥饿问题
public class StampedLockDemo {
private final StampedLock sl = new StampedLock();
private double x, y;
// 乐观读: 不加锁!
public double distanceFromOrigin() {
long stamp = sl.tryOptimisticRead(); // 乐观读 stamp
double currentX = x, currentY = y; // 读取共享变量
if (!sl.validate(stamp)) { // 验证读期间是否有写操作
// 乐观读失败 → 升级为悲观读锁
stamp = sl.readLock();
try {
currentX = x;
currentY = y;
} finally {
sl.unlockRead(stamp);
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
// 悲观读锁
public void moveIfAt(double oldX, double oldY, double newX, double newY) {
long stamp = sl.readLock();
try {
while (x == oldX && y == oldY) {
long ws = sl.tryConvertToWriteLock(stamp); // 尝试升级为写锁
if (ws != 0L) {
stamp = ws;
x = newX;
y = newY;
break;
} else {
sl.unlockRead(stamp);
stamp = sl.writeLock();
}
}
} finally {
sl.unlock(stamp);
}
}
// 写锁
public void move(double deltaX, double deltaY) {
long stamp = sl.writeLock();
try {
x += deltaX;
y += deltaY;
} finally {
sl.unlockWrite(stamp);
}
}
}
kotlin
StampedLock 三种模式:
┌─────────────────────────────────────────────────────┐
│ 1. 乐观读 (tryOptimisticRead + validate) │
│ 不加锁,读完后验证是否有写操作 │
│ 如果无写 → 直接用 (零开销!) │
│ 如果有写 → 升级为悲观读 │
│ 适合: 读远多于写,且能容忍偶尔重读 │
│ │
│ 2. 悲观读 (readLock) │
│ 与 ReadWriteLock 的读锁类似 │
│ 读读共享,读写互斥 │
│ │
│ 3. 写锁 (writeLock) │
│ 独占,阻塞所有读和写 │
└─────────────────────────────────────────────────────┘
乐观读流程:
stamp = tryOptimisticRead() // 获取 stamp (无锁)
读数据 x, y // 读取共享变量
if (validate(stamp)) // 验证读期间是否有写
return compute(x, y) // 无写 → 直接用! (最优路径)
else
readLock() → 重读 → unlockRead() // 有写 → 悲观重试
StampedLock 注意事项
java
// ⚠️ StampedLock 不可重入!
StampedLock sl = new StampedLock();
long stamp = sl.readLock();
try {
method(); // 如果 method 内部又获取读锁 → 死锁!
} finally {
sl.unlockRead(stamp);
}
// ⚠️ 不要在乐观读和悲观读之间调用可能阻塞的方法
public double badMethod() {
long stamp = sl.tryOptimisticRead();
double x = this.x;
Thread.sleep(100); // ❌ 乐观读期间不要阻塞!
double y = this.y;
if (!sl.validate(stamp)) {
// 可能总是失败
}
return Math.sqrt(x*x + y*y);
}
// ⚠️ 虚拟线程中不要用 StampedLock
// StampedLock 在 readLock 时可能触发 park
// 会 pin 虚拟线程
六、四种锁对比
| 对比维度 | synchronized | ReentrantLock | ReadWriteLock | StampedLock |
|---|---|---|---|---|
| 类型 | 互斥 | 互斥 | 读写分离 | 乐观读+读写 |
| 可重入 | ✅ | ✅ | ✅ | ❌ |
| 公平锁 | ❌ | ✅ | ✅ | ❌ |
| 可中断 | ❌ | ✅ | ✅ | ❌ |
| 超时 | ❌ | ✅ | ✅ | ❌(乐观读) |
| 条件变量 | 1(wait/notify) | 多个 Condition | 多个 | ❌ |
| 读读共享 | ❌ | ❌ | ✅ | ✅(乐观读) |
| 乐观读 | ❌ | ❌ | ❌ | ✅ |
| 虚拟线程兼容 | ⚠️(pin) | ✅ | ✅ | ⚠️ |
| 适用场景 | 简单互斥 | 高级互斥 | 读多写少 | 读远多于写 |
性能对比
arduino
性能对比 (读多写少场景):
┌────────────────────────────────────────────────┐
│ 并发读吞吐量 (越高越好) │
│ │
│ synchronized █ │ 1x
│ ReentrantLock ██ │ 1.2x
│ ReadWriteLock ██████████████ │ 8x (读读共享)
│ StampedLock ██████████████████████████ │ 20x (乐观读无锁)
│ │
└────────────────────────────────────────────────┘
读多写少 + 高并发 → StampedLock 最优
读写均衡 → ReadWriteLock
写多读少 → ReentrantLock 或 synchronized
简单场景 → synchronized
七、选型指南
java
锁选型决策树:
场景分析:
├── 纯互斥 (不需要读写分离)?
│ ├── 竞争不激烈 → synchronized
│ ├── 需要可中断/超时/公平 → ReentrantLock
│ └── 虚拟线程场景 → ReentrantLock (避免 synchronized pin)
│
└── 读写分离 (读多写少)?
├── 读写比例 ~1:1 → ReadWriteLock
├── 读远多于写 (10:1+) → StampedLock
└── 不需要严格一致性 → 乐观读模式
特殊场景:
虚拟线程: 避免 synchronized 和 StampedLock (会 pin 线程)
高并发读: StampedLock 乐观读 (无锁最快)
需要条件变量: ReentrantLock + Condition
简单加锁: synchronized (JVM 优化后性能足够)
八、锁的常见陷阱
1. 锁对象不一致
java
// ❌ 锁的不是同一个对象
class BadLock {
public void method1() {
synchronized(this) { ... } // 锁 this
}
public void method2() {
synchronized("lock") { ... } // 锁字符串常量!
}
public void method3() {
Object lock = new Object();
synchronized(lock) { ... } // 每次新建对象 → 无锁效果
}
}
// ✅ 统一锁对象
class GoodLock {
private final Object lock = new Object();
public void method1() {
synchronized(lock) { ... }
}
}
2. 锁泄漏
java
// ❌ ReentrantLock 忘记 unlock
public void badMethod() {
lock.lock();
if (condition) {
return; // ❌ 没有 unlock! 锁泄漏!
}
lock.unlock();
}
// ✅ try-finally
public void goodMethod() {
lock.lock();
try {
if (condition) return; // unlock 在 finally 中执行
} finally {
lock.unlock();
}
}
3. Double-Checked Locking
java
// ❌ 没有 volatile 的 DCL
class Singleton {
private static Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized(Singleton.class) {
if (instance == null) {
instance = new Singleton(); // 指令重排!
}
}
}
return instance;
}
}
// ✅ 必须加 volatile
class Singleton {
private static volatile Singleton instance;
// ...
}
九、常见面试追问
Q1:synchronized 和 ReentrantLock 怎么选?
简单场景用 synchronized(JVM 管理,自动释放,锁升级优化),需要高级功能(可中断、超时、公平、多条件)用 ReentrantLock。虚拟线程场景避免 synchronized(会 pin 线程)。
Q2:StampedLock 为什么快?
乐观读不加锁,完全无锁读取。读多写少场景下,大部分读操作走乐观读路径(tryOptimisticRead + validate),没有 CAS 和阻塞开销。
Q3:ReadWriteLock 有什么问题?
写饥饿:非公平模式下,不断有新读线程获取读锁,写锁可能永远等不到。可以用公平模式缓解但吞吐量下降。StampedLock 的乐观读解决了这个问题。
Q4:什么是锁的公平性?
公平锁严格按 FIFO 顺序获取锁,不会饥饿但吞吐量低。非公平锁允许新线程插队(先尝试 CAS),吞吐量高但可能饥饿。
十、总结
yaml
Java 四种锁:
synchronized: 简单互斥,JVM 优化,自动释放
ReentrantLock: 高级互斥,可中断/超时/公平/多条件
ReadWriteLock: 读写分离,读读共享,写饥饿风险
StampedLock: 乐观读+读写分离,读最优性能
选型:
简单互斥 → synchronized
高级互斥 → ReentrantLock
读多写少 → ReadWriteLock
读远多于写 → StampedLock (乐观读)
虚拟线程 → ReentrantLock (避免 synchronized pin)
锁的选择没有银弹,核心是 根据读写比例、竞争程度和功能需求选择最合适的锁。理解每种锁的底层实现和适用场景,才能在面试和实战中做出正确选择。