大家好,我是欧阳方超,微信公众号同名。
StampedLock是Java8引入的一种高效的读写锁实现,旨在优化读多写少的场景。它提供了三种主要的锁模式:乐观读锁、悲观读锁和写锁。
1 概述
StampedLock是在Java8中引入的一种锁实现,旨在提供更高效的读写锁机制,允许多个线程同时读取数据,而在写操作时则确保数据一致性,其设计目标是提高并发性能,尤其是在读操作远多于写操作的情况下。
1.1 主要特性
乐观读锁(Optimisitc Read Lock),通过tryOptimisticRead()方法获取,不会阻塞写操作,适合于预计读操作不会与写操作冲突的场景,需要在使用过程中验证锁的有效性。
悲观读锁(Pessimistic Read Lock),通过readLock()获取,保证在读取数据时不会有其他线程进行写操作,类似于传统的读锁,多个线程可以同时持有。
写锁(Write Lock),通过writeLock()获取,独占访问,阻塞其他读写操作。
锁降级
支持从写锁降级为读锁,运行在持有写锁时安全地获取读锁。
2 锁模式详解
2.1 乐观读锁
乐观读锁允许线程在不阻塞其他写操作的情况下读取数据。使用时,线程首先调用tryOptimisticRead()获取一个戳记(stamp),然后执行读取操作。完成后,需要调用validate(stamp)来检查在读取期间是否有其他线程进行了写操作。如果没有,则可以安全地返回读取的数据;如果有,则需要获取悲观锁以确保数据的一致性。
2.2 悲观读锁
悲观锁确保在读取期间没有其他线程可以进行写操作。适用于对数据一致性要求较高的场景。在获取悲观读锁时,如果有其他线程持有写锁,则当前线程将被阻塞。
2.3 写锁
写锁是独占的,任何持有写锁的线程都会阻止其他线程获取悲观读锁和写锁,但并不会阻止线程获取乐观读锁的戳。在进行写操作时,必须持有写锁,并在完成后释放它。
2.4 示例
下面通过一个示例展示StampedLock的三种锁模式。
import java.util.concurrent.locks.StampedLock;
public class StampedLockExample {
private final StampedLock stampedLock = new StampedLock();
private int sharedData = 0;
public void writeData(int value) {
long stamp = stampedLock.writeLock();
try {
System.out.println(Thread.currentThread().getName() + " acquired write lock");
sharedData = value;
System.out.println(Thread.currentThread().getName() + " updated sharedData to " + sharedData);
Thread.sleep(6000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
stampedLock.unlockWrite(stamp);
System.out.println(Thread.currentThread().getName() + " released write lock");
}
}
public int pessimisticReadData() {
long stamp = stampedLock.readLock();
try {
System.out.println(Thread.currentThread().getName() + " acquired pessimistic read lock");
return sharedData;
} finally {
stampedLock.unlockRead(stamp);
System.out.println(Thread.currentThread().getName() + " released pessimistic read lock");
}
}
public int optimisticReadData() {
long stamp = stampedLock.tryOptimisticRead();
System.out.println(Thread.currentThread().getName() + " acquired optimistic read lock");
int data = sharedData;
//通过戳记验证乐观读期间是否发生了写操作,如果发生了,则需要通过悲观读锁重新获取数据
if (!stampedLock.validate(stamp)) {
System.out.println(Thread.currentThread().getName() + " optimistic read failed, acquiring pessimistic read lock");
stamp = stampedLock.readLock();
try {
data = sharedData;
} finally {
stampedLock.unlockRead(stamp);
}
}
System.out.println(Thread.currentThread().getName() + " read data: " + data);
return data;
}
public static void main(String[] args) {
StampedLockExample stampedLockExample = new StampedLockExample();
Thread writer = new Thread(() -> stampedLockExample.writeData(11), "Writer");
Thread pessimisticReader =new Thread(() -> {
int data = stampedLockExample.pessimisticReadData();
System.out.println("Pessimistic Reader read: " + data);
}, "Pessimistic Reader");
Thread optimisticReader = new Thread(() -> {
int data = stampedLockExample.optimisticReadData();
System.out.println("Optimistic Reader read: " + data);
}, "Optimistic Reader");
writer.start();
pessimisticReader.start();
optimisticReader.start();
}
}
下面是程序执行的结果:
Writer acquired write lock
Optimistic Reader acquired optimistic read lock
Writer updated sharedData to 11
Optimistic Reader optimistic read failed, acquiring pessimistic read lock
Writer released write lock
Optimistic Reader read data: 11
Pessimistic Reader acquired pessimistic read lock
Pessimistic Reader released pessimistic read lock
Optimistic Reader read: 11
Pessimistic Reader read: 11
从执行结果可以看出,Writer线程在执行写操作期间,获取乐观读锁的线程似乎也在执行,这说明tryOptimisticRead()方法获取乐观读锁时,并不会阻塞,即使有其他线程持有写锁,它只是返回一个戳(stamp),表示当前当前的锁状态。乐观读锁的关键在于validata()方法,在使用数据之前需要通过该方法检查数据是否被修改,如果该方法返回false,说明在读取期间有写操作发生,数据可能已被修改,此时需要通过获取悲观读锁来获取数据,以确保数据一致性,上面代码中刻意把写线程获取到写锁后sleep了十秒钟,代码执行过程中可以明显看到在乐观读数据的方法中获取悲观读锁的代码被阻塞了,这就是因为写锁还在sleep过程中,而sleep并不释放锁,因此可以说明一个获取到写锁的线程会阻塞其他线程获取悲观读锁。
3 性能优势
与传统ReentrantReadWriteLock相比,StampedLock提供了更高的性能,因为它支持乐观读取,这减少了获取和释放读锁的开销。
获取锁的过程:
使用tryOptimisticRead()方法获取乐观读锁时,实际上并不进行真正的锁定操作(这也是为什么乐观读取不需要显式释放的原因),它只是获取一个戳,代表当前的锁状态,由于没有进行实际的锁定操作,获取乐观读锁的过程非常轻量级,几乎没有开销。
释放锁的过程:
乐观读锁不需要显式释放,因为它没有真正持有锁,它依赖于validate()方法来检查在读取期间数据是否被修改,如果返回true说明数据在读取期间没有被写操作改变,读取操作就可以直接完成,如果返回false,则需要进一步获取悲观读锁,再进行数据读取。
4 使用注意事项
不可重入性:与ReentrantLock不同,StampedLock不支持重入,这意味着同一线程不能再次获得已经持有的锁。
死锁风险:必须使用相同的戳记来释放相应的锁,否则会导致死锁。
CPU使用率问题:不当使用(如在中断时未正确处理)可能导致CPU使用率飙升。
5 总结
StampedLock是一种灵活且高效的同步机制,特别适合于读多写少的场景。通过结合乐观和悲观策略,它能够有效地提高并发性能,同时避免传统读写锁可能出现的问题,如写饥饿等。在设计多线程程序时,合理运用StampedLock可以显著提升程序效率。
我是欧阳方超,把事情做好了自然就有兴趣了,如果你喜欢我的文章,欢迎点赞、转发、评论加关注。我们下次见。