什么是 Java 的 StampedLock?

Sherwin.Wei Lv7

什么是 Java 的 StampedLock?

回答重点

StampedLock 是 Java 8 引入的一个锁机制,与传统的 ReentrantLockReadWriteLock 相比,StampedLock 通过引入乐观读锁和时间戳(stamp)的概念,提升了读写性能,尤其是在读多写少的场景下。

核心特性

  1. 写锁(write lock):独占模式的锁,和 ReentrantLock 类似,保证写操作的排他性。
  2. 悲观读锁(read lock):共享模式的锁,多个线程可以同时持有读锁,但写锁需要等待。
  3. 乐观读锁(optimistic read lock):无需阻塞的读锁机制,允许在没有竞争的情况下进行快速读取。当检测到有写操作发生时,才会回退到悲观读锁或重试。

时间戳(stamp)

  • 每个锁操作都会返回一个 stamp,代表了锁的状态,后续操作需要根据这个 stamp 来验证锁是否有效。

扩展知识

StampedLock 的优缺点

优点

  • 乐观读模式提供了更高效的并发读操作,避免了传统锁机制下的线程阻塞。
  • 更灵活的锁机制,允许不同的锁模式进行切换,适合不同场景。

缺点

  • 不可重入StampedLock 是非重入锁,这意味着同一个线程不能在持有锁时再次获取同类型的锁,否则会造成死锁。
  • 读锁饥饿:在高写入负载的场景下,悲观读锁可能会被长期阻塞,导致读操作饥饿。
  • CPU飙升风险:如果线程使用 writeLock() 或者readLock() 获得锁之后,线程还没执行完就被 interrupt() 的话,会导致CPU飙升,需要用 readLockInterruptibly 或者 writeLockInterruptibly

CPU 飙升示例代码

我本地跑了下复现了:

pic.jpg

内部原理

  • StampedLock 通过一个长整型值来管理状态,低位用于表示锁的类型(写锁、读锁),高位用于表示锁的计数。当乐观读锁被获取时,生成一个时间戳(stamp),并在后续验证时判断是否有写操作发生。
  • validate(stamp) 方法可以有效判断在持有乐观读锁的情况下,是否有其他写操作干扰(被修改了),从而决定是否要变为悲观读锁。

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import java.util.concurrent.locks.StampedLock;

public class StampedLockExample {
private double x, y;
private final StampedLock lock = new StampedLock();

// 写操作:移动坐标
public void move(double deltaX, double deltaY) {
long stamp = lock.writeLock(); // 获取写锁
try {
x += deltaX;
y += deltaY;
} finally {
lock.unlockWrite(stamp); // 释放写锁
}
}

// 悲观读操作:获取坐标
public double distanceFromOrigin() {
long stamp = lock.readLock(); // 获取读锁
try {
return Math.sqrt(x * x + y * y);
} finally {
lock.unlockRead(stamp); // 释放读锁
}
}

// 乐观读操作:获取坐标
public double optimisticDistanceFromOrigin() {
long stamp = lock.tryOptimisticRead(); // 获取乐观读锁
double currentX = x, currentY = y;
if (!lock.validate(stamp)) { // 检查乐观读锁是否被其他写操作干扰
stamp = lock.readLock(); // 回退到悲观读锁
try {
currentX = x;
currentY = y;
} finally {
lock.unlockRead(stamp); // 释放读锁
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
}

关键点

  • 写锁用于修改共享资源,多个线程无法同时持有写锁。
  • 悲观读锁保证数据一致性,适合写操作较多的场景。
  • 乐观读锁适用于读多写少的场景,可以在大部分时间无锁读取,大大提高性能。
Comments