StampedLock
StampedLock
是 Java 8 引入的一种新型锁机制,它综合了读写锁、乐观读锁和悲观写锁的特点,旨在提供更高效的并发访问控制。有人称它为锁的性能之王。
StampedLock
没有实现 Lock
接口和 ReadWriteLock 接口,但它实现了 读写锁 的功能 ,并且性能比 ReentrantReadWriteLock 更高。StampedLock
还把读锁分为了 乐观读锁 和 悲观读锁 两种。
它的核心思想在于,在读的时候如果发生了写,应该通过重试的方式来获取新的值,而不应该阻塞写操作。
这种模式也就是典型的无锁编程思想,和 CAS 自旋的思想一样。这种操作方式决定了 StampedLock
十分适用于读线程非常多而写线程非常少的场景,同时还避免了写饥饿情况的发生。
特点
StampedLock
的主要特点有:
-
无锁方式的乐观读锁机制,允许一个线程在没有任何写入操作发生的情况下读取数据,从而提高了性能。,避免了部分读操作的互斥开销。
-
可重入性:StampedLock 的可重入性发生在独占写锁的情况下。对于悲观读取锁和乐观读取锁,StampedLock不支持可重入性。
-
写锁的获得和释放是互斥的,写锁被持有时,其他的写锁和读锁都无法获取。
-
支持读写锁降级,可以将独占写锁转换为读锁,但不能将读锁转换为写锁。
-
不支持锁的升级,即不能将读锁升级为写锁。
-
没有监视器锁的内置锁饥饿,不会出现长时间阻塞的情况。
-
API 复杂性:由于提供了乐观读锁和锁降级功能,
StampedLock
的 API 相对复杂一些,需要更小心地使用以避免死锁和其他问题。ReentrantReadWriteLock
的 API 相对更直观和容易使用。
常用方法
-
tryOptimisticRead()
:返回一个乐观读取的戳记(stamp)。这是进行乐观读取的第一步。 -
validate(long stamp)
:检查给定的戳记是否仍然有效。如果自上次获取戳记以来没有写入操作发生,则返回true
;否则返回false
。 -
readLock()
:获取一个读锁,阻止其他写入操作,但允许其他读取操作。 -
unlockRead(long stamp)
:释放之前获取的读锁。 -
writeLock()
:获取一个写锁,阻止其他所有读取和写入操作。 -
unlockWrite(long stamp)
:释放之前获取的写锁。 -
tryRead()
:尝试获取一个读锁。如果成功,返回一个有效的戳记;否则返回一个无效的戳记。 -
tryWrite()
:尝试获取一个写锁。如果成功,返回一个有效的戳记;否则返回一个无效的戳记。
StampedLock 模式
StampedLock
支持三种模式的访问控制:读锁 、悲观读锁 、乐观读锁
StampedLock 获取锁会返回一个 long
类型的变量(即 StampedLock 的 Stamp),释放锁的时候再把这个变量传进去
内部 long 变量
StampedLock
用这个 long 类型的变量的前 7 位(LG_READERS
)来表示读锁,每获取一个悲观读锁,就加 1(RUNIT
),每释放一个悲观读锁,就减 1。
悲观读锁最多只能装 128 个(7 位限制),很容易溢出,所以用一个 int 类型的变量来存储溢出的悲观读锁。
写锁用 state
变量剩下的位来表示,每次获取一个写锁,就加 0000 1000 0000(WBIT
)。需要注意的是,写锁在释放的时候,并不是减 WBIT,而是再加 WBIT 。这是为了让每次写锁都留下痕迹 ,解决 CAS 中的 ABA 问题,也为乐观锁检查变化 validate
方法提供基础。
对于乐观读锁,并没有真正改变 state 的值,而是在获取锁的时候记录 state 的写状态,在操作完成后去检查 state 的写状态部分是否发生变化。每次写锁都会留下痕迹,也是为了乐观读锁检查变化提供方便。
写模式
写模式(Exclusive mode):与传统的独占写锁类似,写模式下只允许一个线程独占访问临界区,其他线程无法获取读锁或乐观读锁。
java
StampedLock stampedLock = new StampedLock();
long stamp = stampedLock.writeLock(); // 获取写锁
stampedLock.unlockWrite(stamp); // 释放写锁
悲观读模式
悲观读模式(Shared mode):与传统的共享读锁类似,读模式下允许多个线程同时读取共享资源,但不允许其他线程获取写锁和乐观读锁。
java
StampedLock stampedLock = new StampedLock();
long stamp = stampedLock.readLock(); // 获取悲观读锁
stampedLock.unlockRead(stamp); // 释放悲观读锁
乐观读模式
乐观读模式(Optimistic mode):乐观读模式是一种特殊的读模式,假定在这个锁获取期间,共享变量不会被改变,不会阻塞写操作。
java
StampedLock stampedLock = new StampedLock();
long stamp = stampedLock.tryOptimisticRead(); // 获取乐观读锁
// 检查乐观读锁后是否有其他写锁,有则返回 false
stampedLock.validate(stamp);
stampedLock.unlock(stamp); // 释放所有锁
在乐观读模式下,线程会尝试读取共享资源,并返回一个表示戳记(stamp)的值 ,的 戳记
(stamp)可以是任意的整数值,用来标识特定的读操作。
戳记(stamp)不是唯一的标识,不能用于表示并发版本控制或实现严格的数据一致性,它只是用于在乐观读模式下进行基本的验证。
当一个线程进入乐观读模式时,会获取当前的戳记,并将其保存下来。在读取共享资源后,线程会使用获取时的戳记再次校验共享资源是否发生了写入操作。
如果验证通过,即没有其他线程进行了写入,那么乐观读操作是有效的;如果验证失败,意味着共享资源可能被其他线程修改过,这时候可以选择重试读操作或者进一步获取悲观读锁。
每次共享资源发生写入操作时,戳记的值都会发生变化。因此,在乐观读模式下,通过比较读取时的戳记和当前的戳记,可以判断共享资源是否被修改。
如果在读取期间没有写操作发生,那么读操作是安全的,否则会导致数据不一致。乐观读模式可以用于读操作频繁,写操作较少和不严格一致性要求的场景。
使用示例
下面是一个详细的 StampedLock
使用示例,包括如何进行乐观读取、写入操作以及如何处理读写冲突。我们将创建一个简单的示例,其中包含一个共享资源 x
,多个线程将尝试读取和修改这个资源。
java
import java.util.concurrent.locks.StampedLock;
import java.util.concurrent.TimeUnit;
public class StampedLockExample {
private double x;
private final StampedLock sl = new StampedLock();
public static void main(String[] args) {
StampedLockExample example = new StampedLockExample();
// 启动一个写入线程
Thread writerThread = new Thread(() -> {
for (int i = 0; i < 10; i++) {
example.writeOperation(i);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
});
writerThread.start();
// 启动多个读取线程
for (int i = 0; i < 5; i++) {
Thread readerThread = new Thread(() -> {
while (true) {
double readValue = example.optimisticRead();
System.out.println("Read value: " + readValue);
try {
TimeUnit.MILLISECONDS.sleep(200);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
});
readerThread.start();
}
try {
writerThread.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
public void writeOperation(int newValue) {
long stamp = sl.writeLock(); // 获取写锁
try {
x = calculateNewValue(newValue);
System.out.println("Updated value: " + x);
} finally {
sl.unlockWrite(stamp); // 释放写锁
}
}
public double optimisticRead() {
long stamp = sl.tryOptimisticRead(); // 尝试乐观读
double current = x;
if (!sl.validate(stamp)) { // 检查数据是否被修改
stamp = sl.readLock(); // 获取读锁
try {
current = x;
} finally {
sl.unlockRead(stamp); // 释放读锁
}
}
return current;
}
private double calculateNewValue(int newValue) {
// 这里执行计算新的值
return newValue * 10.0; // 示例代码
}
}
示例说明:
-
初始化共享资源 :我们定义了一个共享资源
x
,它将在写入操作中被更新。 -
创建
StampedLock
实例 :sl
是一个StampedLock
实例,用于管理读写操作。 -
写入操作 :
writeOperation
方法获取一个写锁,更新x
的值,并释放写锁。在本例中,写入操作会将newValue
乘以10
作为新的x
值。 -
乐观读取 :
optimisticRead
方法首先尝试进行乐观读取。如果数据在读取过程中被修改,则重新获取读锁,读取最新的数据值。读取完成后释放读锁。 -
主函数:
- 启动一个写入线程,每隔一秒更新一次
x
的值。 - 启动多个读取线程,每隔 200 毫秒读取一次
x
的值。 - 读取线程将输出读取到的值。
- 启动一个写入线程,每隔一秒更新一次