Java CompletableFuture 异步编排实战

引言

锁是 Java 并发编程的基础设施。从最古老的 `synchronized` 到 JDK 5 引入的 `ReentrantLock`,再到 JDK 8 的 `StampedLock`,Java 的锁机制经历了从 JVM 内置到 API 化再到乐观读的演进。理解每种锁的原理和适用场景,是写出高效并发代码的前提。


一、synchronized

1.1 基本用法

```java

// 1. 修饰实例方法:锁当前实例

public synchronized void method() {

// 同步代码

}

// 2. 修饰静态方法:锁当前类的 Class 对象

public static synchronized void staticMethod() {

// 同步代码

}

// 3. 修饰代码块:锁指定对象

public void method() {

synchronized (lock) {

// 同步代码

}

}

```

1.2 锁的原理:Monitor

```

每个 Java 对象关联一个 Monitor(监视器)

synchronized 进入:

monitorenter → 获取 Monitor 锁

synchronized 退出:

monitorexit → 释放 Monitor 锁

Monitor 结构:

┌────────────────────────────┐

│ _owner → 持有锁的线程 │

│ _entry_set → 等待获取锁 │

│ _wait_set → 调用 wait() │

└────────────────────────────┘

```

1.3 锁升级(偏向锁 → 轻量级锁 → 重量级锁)

JDK 6 引入锁升级优化,`synchronized` 不再总是重量级锁:

```

无锁 → 偏向锁 → 轻量级锁 → 重量级锁

│ │ │ │

│ │ │ └─ 竞争激烈,OS 互斥量

│ │ └─ 自旋等待(CAS),适合短时间竞争

│ └─ 同一线程重复获取,无竞争,CAS 都不需要

└─ 对象刚创建,无任何线程访问

升级是单向的(不可降级,G1 下有例外)

```

| 锁状态 | 适用场景 | 获取方式 | 性能 |

|--------|----------|----------|------|

| 偏向锁 | 同一线程反复获取 | 检查偏向线程 ID | 最优 |

| 轻量级锁 | 交替执行,低竞争 | CAS 自旋 | 优 |

| 重量级锁 | 高竞争 | OS 互斥量 | 差 |

1.4 优缺点

| 优点 | 缺点 |

|------|------|

| JVM 内置,使用简单 | 无法中断等待锁的线程 |

| 锁升级自动优化 | 无法设置超时 |

| 异常自动释放锁 | 不支持公平锁 |

| 可重入 | 不支持条件变量(Condition) |

| | 在虚拟线程中会导致固定(Pinning) |


二、ReentrantLock

2.1 基本用法

```java

private final ReentrantLock lock = new ReentrantLock();

public void method() {

lock.lock();

try {

// 同步代码

} finally {

lock.unlock(); // 必须在 finally 中释放

}

}

```

2.2 公平锁 vs 非公平锁

```java

// 非公平锁(默认):新来的线程可能插队

ReentrantLock unfairLock = new ReentrantLock();

// 公平锁:按等待顺序获取锁

ReentrantLock fairLock = new ReentrantLock(true);

```

| 对比 | 非公平锁 | 公平锁 |

|------|----------|--------|

| 获取顺序 | 可能插队 | FIFO 排队 |

| 吞吐量 | 高 | 低 |

| 饥饿风险 | 有(线程可能永远等不到) | 无 |

| 实现原理 | CAS 直接尝试 | 先入队列排队 |

2.3 可中断锁

```java

ReentrantLock lock = new ReentrantLock();

Thread t = new Thread(() -> {

try {

lock.lockInterruptibly(); // 可被中断的锁获取

try {

doWork();

} finally {

lock.unlock();

}

} catch (InterruptedException e) {

System.out.println("被中断,放弃获取锁");

}

});

t.start();

Thread.sleep(1000);

t.interrupt(); // 中断等待锁的线程

```

2.4 超时获取锁

```java

ReentrantLock lock = new ReentrantLock();

if (lock.tryLock()) {

try {

doWork();

} finally {

lock.unlock();

}

} else {

System.out.println("获取锁失败,执行降级逻辑");

}

// 带超时

if (lock.tryLock(5, TimeUnit.SECONDS)) {

try {

doWork();

} finally {

lock.unlock();

}

} else {

System.out.println("5秒内未获取锁");

}

```

2.5 Condition 条件变量

```java

// synchronized 只有一个 wait set

// ReentrantLock 支持多个 Condition,实现精准唤醒

private final ReentrantLock lock = new ReentrantLock();

private final Condition notFull = lock.newCondition();

private final Condition notEmpty = lock.newCondition();

private final Object\[\] items = new Object10;

private int count, putIdx, takeIdx;

public void put(Object item) throws InterruptedException {

lock.lock();

try {

while (count == items.length) {

notFull.await(); // 队列满,等待 notFull 条件

}

itemsputIdx = item;

if (++putIdx == items.length) putIdx = 0;

count++;

notEmpty.signal(); // 通知消费者

} finally {

lock.unlock();

}

}

public Object take() throws InterruptedException {

lock.lock();

try {

while (count == 0) {

notEmpty.await(); // 队列空,等待 notEmpty 条件

}

Object item = itemstakeIdx;

itemstakeIdx = null;

if (++takeIdx == items.length) takeIdx = 0;

count--;

notFull.signal(); // 通知生产者

return item;

} finally {

lock.unlock();

}

}

```

2.6 synchronized vs ReentrantLock

| 对比 | synchronized | ReentrantLock |

|------|-------------|---------------|

| 实现 | JVM 内置 | JDK API(AQS) |

| 锁获取 | 自动 | 手动 lock/unlock |

| 可中断 | ❌ | ✅ lockInterruptibly |

| 超时 | ❌ | ✅ tryLock(timeout) |

| 公平性 | 非公平 | 可选公平/非公平 |

| 条件变量 | wait/notify(一个) | Condition(多个) |

| 锁升级 | 偏向→轻量→重量 | 无 |

| 虚拟线程 | 固定(Pinning) | 正常卸载 |

| 异常安全 | 自动释放 | 需 finally 手动释放 |


三、ReadWriteLock

3.1 读写分离

```java

// 读读不互斥,读写互斥,写写互斥

private final ReadWriteLock rwLock = new ReentrantReadWriteLock();

private final Lock readLock = rwLock.readLock();

private final Lock writeLock = rwLock.writeLock();

private Map<String, Data> cache = new HashMap<>();

public Data get(String key) {

readLock.lock();

try {

return cache.get(key);

} finally {

readLock.unlock();

}

}

public void put(String key, Data value) {

writeLock.lock();

try {

cache.put(key, value);

} finally {

writeLock.unlock();

}

}

```

3.2 ReentrantReadWriteLock 特性

| 特性 | 说明 |

|------|------|

| 公平性 | 支持公平/非公平模式 |

| 重入 | 读锁和写锁都支持重入 |

| 锁降级 | 写锁可降级为读锁(获取写锁→获取读锁→释放写锁) |

| 锁升级 | 读锁不能升级为写锁(会死锁) |

```java

// 锁降级:写锁 → 读锁

writeLock.lock();

try {

updateData();

readLock.lock(); // 获取读锁

} finally {

writeLock.unlock(); // 释放写锁,此时持有读锁

}

try {

readData(); // 在读锁保护下读取

} finally {

readLock.unlock();

}

```

3.3 读写锁的问题

  • **写饥饿**:读操作频繁时,写线程可能长时间获取不到写锁

  • **不支持乐观读**:即使只是读取,也需要获取读锁


四、StampedLock

4.1 核心思想

StampedLock 在 ReadWriteLock 基础上引入了**乐观读(Optimistic Read)**:

```

ReadWriteLock:

读锁 → 获取读锁 → 读取 → 释放读锁(每次都要 CAS)

StampedLock:

乐观读 → 获取戳 → 读取 → 验证戳 → 成功则无需 CAS

→ 失败则升级为读锁重试

```

4.2 三种模式

| 模式 | 说明 | 类比 |

|------|------|------|

| 写锁(Writing) | 独占锁,与读锁互斥 | ReadWriteLock 的写锁 |

| 读锁(Reading) | 共享锁,与写锁互斥 | ReadWriteLock 的读锁 |

| 乐观读(Optimistic Reading) | 无锁读取,验证后使用 | 数据库的乐观锁 |

4.3 基本用法

```java

private final StampedLock sl = new StampedLock();

private double x, y;

// 写操作

void move(double deltaX, double deltaY) {

long stamp = sl.writeLock();

try {

x += deltaX;

y += deltaY;

} finally {

sl.unlockWrite(stamp);

}

}

// 乐观读

double distanceFromOrigin() {

long stamp = sl.tryOptimisticRead(); // 1. 获取乐观读戳

double currentX = x, currentY = y; // 2. 读取数据(无锁)

if (!sl.validate(stamp)) { // 3. 验证戳(期间是否有写操作?)

stamp = sl.readLock(); // 4. 验证失败,升级为读锁

try {

currentX = x;

currentY = y;

} finally {

sl.unlockRead(stamp);

}

}

return Math.sqrt(currentX * currentX + currentY * currentY);

}

// 悲观读

double distanceFromOriginPessimistic() {

long stamp = sl.readLock();

try {

return Math.sqrt(x * x + y * y);

} finally {

sl.unlockRead(stamp);

}

}

```

4.4 锁转换

```java

// 读锁 → 写锁

long stamp = sl.readLock();

try {

long ws = sl.tryConvertToWriteLock(stamp);

if (ws != 0L) {

stamp = ws;

// 持有写锁

} else {

sl.unlockRead(stamp);

stamp = sl.writeLock();

}

} finally {

sl.unlock(stamp);

}

// 乐观读 → 读锁

long stamp = sl.tryOptimisticRead();

if (!sl.validate(stamp)) {

stamp = sl.readLock();

try {

// 重新读取

} finally {

sl.unlockRead(stamp);

}

}

```

4.5 StampedLock 注意事项

| 注意 | 说明 |

|------|------|

| 不可重入 | 同一线程不能重复获取同一锁 |

| 不支持 Condition | 没有 newCondition() 方法 |

| 不要用 interrupt | 不要在 readLock/writeLock 中调用 interrupt |

| unlock 必须传 stamp | 传入错误的 stamp 会抛 IllegalMonitorStateException |

| 乐观读适合读多写少 | 写频繁时乐观读经常失败,反而更慢 |


五、锁选型指南

5.1 决策树

```

需要锁吗?

├─ 能否用无锁方案?(Atomic* / Concurrent集合)

│ ├─ 能 → 优先无锁

│ └─ 不能 ↓

├─ 读多写少?

│ ├─ 是 → 读远多于写?

│ │ ├─ 是 → StampedLock(乐观读)

│ │ └─ 否 → ReentrantReadWriteLock

│ └─ 否 ↓

├─ 需要高级功能?(中断/超时/多条件/公平)

│ ├─ 是 → ReentrantLock

│ └─ 否 → synchronized(简单场景优先)

├─ 在虚拟线程中使用?

│ ├─ 是 → ReentrantLock(避免 synchronized 固定)

│ └─ 否 → synchronized

```

5.2 性能对比(大致参考)

```

低竞争:synchronized ≈ ReentrantLock > StampedLock

中竞争:StampedLock > ReentrantLock > synchronized

高竞争:StampedLock(乐观读) >> ReentrantReadWriteLock > ReentrantLock > synchronized

读多写少:StampedLock(乐观读) >> ReentrantReadWriteLock >> synchronized

```

5.3 速查表

| 场景 | 推荐锁 | 原因 |

|------|--------|------|

| 简单互斥 | synchronized | 简单,JVM 优化好 |

| 需要中断/超时 | ReentrantLock | lockInterruptibly / tryLock |

| 需要公平锁 | ReentrantLock(true) | synchronized 不支持 |

| 需要多条件唤醒 | ReentrantLock + Condition | synchronized 只有一个 wait set |

| 读多写少 | StampedLock | 乐观读无锁,性能最优 |

| 虚拟线程中 | ReentrantLock | synchronized 导致固定 |

| 缓存场景 | ReentrantReadWriteLock | 读写分离 |

| 高并发点数据 | StampedLock | 乐观读 + 锁转换 |


六、锁的性能优化技巧

6.1 减小锁粒度

```java

// ❌ 锁范围过大

synchronized (this) {

validate(data); // 无需锁

process(data); // 需要锁

log(data); // 无需锁

}

// ✅ 只锁必要部分

validate(data);

synchronized (this) {

process(data);

}

log(data);

```

6.2 减小锁粒度(分段锁)

```java

// ❌ 一把大锁

synchronized (allAccounts) {

transfer(from, to, amount);

}

// ✅ 分段锁(类似 ConcurrentHashMap)

private final Object\[\] locks = new Object16;

{

for (int i = 0; i < locks.length; i++) locksi = new Object();

}

void transfer(Account from, Account to, int amount) {

Object lock1 = locksMath.abs(from.hashCode() % 16);

Object lock2 = locksMath.abs(to.hashCode() % 16);

// 按固定顺序加锁,避免死锁

Object first = System.identityHashCode(lock1) < System.identityHashCode(lock2) ? lock1 : lock2;

Object second = first == lock1 ? lock2 : lock1;

synchronized (first) {

synchronized (second) {

from.debit(amount);

to.credit(amount);

}

}

}

```

6.3 避免锁嵌套

```java

// ❌ 锁嵌套,容易死锁

synchronized (lockA) {

synchronized (lockB) {

doWork();

}

}

// ✅ 拆分:先获取所有锁,再操作

// 或使用 tryLock 避免无限等待

if (lockA.tryLock()) {

try {

if (lockB.tryLock()) {

try {

doWork();

} finally {

lockB.unlock();

}

}

} finally {

lockA.unlock();

}

}

```


总结

| 锁 | 一句话 | 适用场景 |

|----|--------|----------|

| synchronized | 简单可靠,JVM 优化 | 简单互斥,低竞争 |

| ReentrantLock | 功能丰富,API 灵活 | 需要中断/超时/公平/多条件 |

| ReentrantReadWriteLock | 读写分离 | 读多写少,但写不极少 |

| StampedLock | 乐观读,极致性能 | 读远多于写,追求极致吞吐 |