StampedLock 是读写锁的实现,对比 ReentrantReadWriteLock 主要不同是该锁不允许重入,多了乐观读的功能,使用上会更加复杂一些,但是具有更好的性能表现。StampedLock 的状态由版本和读写锁持有计数组成。 获取锁方法返回一个邮戳,表示和控制与锁状态相关的访问; 这些方法的“尝试”版本可能会返回特殊值 0 来表示获取锁失败。 锁释放和转换方法需要邮戳作为参数,如果它们与锁的状态不匹配则失败。
但是也是由于 StampedLock 大量使用自旋的原因(ReentrantReadWriteLock 也使用了自旋,但是没有 StampedLock 频繁),CPU 的消耗理论上也比 ReentrantReadWriteLock 高。
StampedLock 非常适合写锁中的操作非常快的业务场景。因为读锁如果因为写锁而获取锁失败,读锁会做重试获取和有限次的自旋的方式,比较晚进入到等待队列中。如果在自旋过程中,写锁能释放,那么获取读锁的线程就能避免被操作系统阻塞和唤醒等耗资源操作,增加读锁的响应效率。
三种模式
悲观读锁
与 ReentrantReadWriteLock 的读锁类似,多个线程可以同时获取悲观读锁。这是一个共享锁,允许多个线程同时读取共享资源。
乐观读锁
相当于直接操作数据,不加任何锁。在操作数据前并没有通过 CAS 设置锁的状态,仅仅通过位运算测试。如果当前没有线程持有写锁,则简单地返回一个非 0 的 stamp 版本信息。返回 0 则说明有线程持有写锁。获取该 stamp 后在具体操作数据前还需要调用 validate 方法验证该 stamp 是否己经不可用。
写锁
与 ReentrantReadWriteLock的写锁类似,写锁和悲观读锁是互斥的。虽然写锁与乐观读锁不会互斥,但是在数据被更新之后,之前通过乐观读锁获得的数据已经变成了脏数据,需要自己处理这个。
StampedLock 的读写锁都是不可重入锁,所以在获取锁后释放锁前不应该再调用会获取锁的操作,以避免造成调用线程被阻塞。
在实际应用中,StampedLock 可以用于那些读操作远多于写操作的场景,例如缓存系统、数据报表生成等。在这些场景中,StampedLock 可以显著提高并发性能,同时保证数据的一致性和安全性。
最重要的一点: 在使用时需要特别注意:如果某个线程阻塞在StampedLock的readLock()或者writeLock()方法上时,此
时调用阻塞线程的interrupt()方法中断线程,会导致CPU飙升到100%。
所以尽量在写操作是非常快的场景下使用, 这样读的时候乐观锁释放的非常快,几乎达到无锁模式。
所有接口方法
经典案例
import java.util.concurrent.locks.StampedLock;public class StampedLockExample {private int inventory = 100; // 初始库存为100private final StampedLock lock = new StampedLock();// 扣减库存操作public void decreaseInventory(int quantity) {long stamp = lock.writeLock(); // 获取写锁try {if (inventory >= quantity) {inventory -= quantity; // 扣减库存System.out.println("成功减少库存 " + quantity + ", 当前的库存量: " + inventory);} else {System.out.println("未能减少库存,库存不足");}} finally {lock.unlockWrite(stamp); // 释放写锁}}// 获取当前库存public int getInventory() {long stamp = lock.tryOptimisticRead(); // 乐观读锁int currentInventory = inventory;if (!lock.validate(stamp)) { // 检查乐观读锁是否有效stamp = lock.readLock(); // 乐观读锁无效,转为悲观读锁try {currentInventory = inventory; // 获取当前库存} finally {lock.unlockRead(stamp); // 释放读锁}}return currentInventory; // 返回当前库存}public static void main(String[] args) {StampedLockExample manager = new StampedLockExample();// 多个线程同时扣减库存Thread t1 = new Thread(() -> {manager.decreaseInventory(20); // 线程1扣减库存System.out.println(manager.getInventory());});Thread t2 = new Thread(() -> {manager.decreaseInventory(50); // 线程2扣减库存System.out.println(manager.getInventory());});t1.start();t2.start();}
}
官网案例
public class Point {private double x, y;private final StampedLock sl = new StampedLock();public void move(double deltaX, double deltaY) {使用写锁-独占操作,并返回一个邮票long stamp = sl.writeLock();try {x += deltaX;y += deltaY;} finally {使用邮票来释放写锁sl.unlockWrite(stamp); }}// 使用乐观读锁访问共享资源// 注意:乐观读锁在保证数据一致性上需要拷贝一份要操作的变量到方法栈,并且在操作数据时候可能其 // 他写线程已经修改了数据,而我们操作的是方法栈里面的数据,也就是一个快照,所以最多返回的不是 // 最新的数据,但是一致性还是得到保障的。public double distanceFromOrigin() {使用乐观读锁-并返回一个邮票,乐观读不会阻塞写入操作,从而解决了写操作线程饥饿问题。long stamp = sl.tryOptimisticRead(); 拷贝共享资源到本地方法栈中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 moveIfAtOrigin(double newX, double newY) { // upgrade// Could instead start with optimistic, not read modelong stamp = sl.readLock();try {while (x == 0.0 && y == 0.0) {读锁转换为写锁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);}}}
StampedLock和ReentrantReadWriteLock之间的区别
- 锁的类型与特性:
- StampedLock:提供了乐观读、悲观读和写锁三种模式。乐观读模式允许在写锁未被持有时进行无锁读取,通过验证戳记(stamp)来确保数据的一致性。这种模式减少了锁的竞争,提高了吞吐量。
- ReentrantReadWriteLock:允许多个读线程同时访问,但写线程在访问时必须独占。它支持锁的重入,即同一线程可以多次获取同一把锁。
- 性能:
- StampedLock:通常比ReentrantReadWriteLock具有更高的性能,特别是在读多写少的场景下。由于乐观读的存在,它能够在无竞争的情况下避免不必要的锁开销。
- ReentrantReadWriteLock:在读操作远多于写操作的场景中表现良好,但写锁的饥饿问题和锁降级操作可能影响其性能。
- 实现机制:
- StampedLock:并非基于AQS(AbstractQueuedSynchronizer)实现,而是使用了自己的同步等待队列和状态设计。其状态为一个long型变量,与ReentrantReadWriteLock的设计不同。
- ReentrantReadWriteLock:基于AQS实现,通过内部维护的读写锁来实现多线程间的同步。
- 使用场景:
- StampedLock:更适合于读多写少且对性能要求较高的场景,尤其是当数据争用不严重时。它能够有效减少锁的竞争,提高系统的吞吐量。
- ReentrantReadWriteLock:适用于需要重入锁或需要在写操作后降级为读锁的场景。它提供了更严格的访问控制,但可能在某些情况下牺牲了一定的性能。
- 锁的获取与释放:
- StampedLock:在获取锁时会返回一个戳记(stamp),用于后续的锁释放或转换。这个戳记代表了锁的状态,有助于在释放锁时验证数据的一致性。
- ReentrantReadWriteLock:没有戳记的概念,锁的获取和释放相对简单直接。
综上所述,StampedLock和ReentrantReadWriteLock各有其特点和适用场景。在选择使用哪种锁时,应根据具体的应用需求和性能要求来做出决策。