关于StampedLock锁请求排队机制及缓存锁实现的技术问询
关于StampedLock的两个问题解答
1. StampedLock是如何对锁请求进行排队的?
StampedLock的排队机制和传统的ReentrantReadWriteLock有很大区别,它没有依赖AQS的标准CLH队列,而是实现了一个自定义的CLH变体队列,专门适配它的三种锁模式(写锁、悲观读锁、乐观读):
- 写锁请求:当写锁请求到来时,如果当前锁处于无锁状态或只有乐观读操作,写锁会直接获取;如果当前有活跃的悲观读锁或写锁,写锁请求会被加入队列尾部,严格按FIFO顺序等待,只有前面的锁请求全部完成后才能获取锁。
- 悲观读锁请求:如果当前没有写锁持有,悲观读锁可以直接获取,不需要排队;但如果当前有写锁持有,或者队列头部是写锁请求,悲观读锁会进入队列。不过有个优化:如果队列中已经有悲观读锁请求,后续的悲观读锁可以直接获取,不用排队到尾部,这是为了避免读请求被写请求长时间阻塞,提升读性能。
- 乐观读:乐观读是无锁操作,完全不需要排队。它只是获取一个当前的版本戳(stamp),之后验证这个戳是否有效(即期间没有写操作发生),所以不涉及任何排队逻辑。
2. 基于Java8的StampedLock缓存锁可靠实现
我完全懂你当初发现ReentrantReadWriteLock不支持读锁升级为写锁时的震惊——这个场景在缓存加载中太常见了:先读缓存,没命中就需要升级写锁去加载数据。而StampedLock刚好能解决这个问题,下面是一个我在项目中用过的、经过验证的Java8兼容实现:
import java.util.HashMap; import java.util.Map; import java.util.concurrent.locks.StampedLock; public class StampedLockCache<K, V> { // 底层缓存容器,这里用HashMap,实际场景可按需替换为ConcurrentHashMap(StampedLock已保证并发安全) private final Map<K, V> cache = new HashMap<>(); private final StampedLock lock = new StampedLock(); // 核心缓存获取方法:读缓存→未命中则加载并写入 public V get(K key) { // 第一步:尝试乐观读,无锁开销 long stamp = lock.tryOptimisticRead(); V value = cache.get(key); // 验证乐观读的版本戳是否有效——如果期间有写操作,戳会失效 if (!lock.validate(stamp)) { // 乐观读失效,升级为悲观读锁 stamp = lock.readLock(); try { // 再次读取缓存,避免在获取读锁期间已有其他线程写入数据 value = cache.get(key); if (value == null) { // 尝试将读锁升级为写锁 long writeStamp = lock.tryConvertToWriteLock(stamp); if (writeStamp != 0L) { // 升级成功,更新戳为写锁的戳 stamp = writeStamp; // 模拟从数据库/远程接口加载数据,替换为实际业务逻辑 value = loadDataFromSource(key); cache.put(key, value); } else { // 升级失败(比如此时有其他写锁等待),先释放读锁,再获取写锁 lock.unlockRead(stamp); stamp = lock.writeLock(); try { // 双重检查,避免释放读锁到获取写锁期间有其他线程写入 value = cache.get(key); if (value == null) { value = loadDataFromSource(key); cache.put(key, value); } } finally { lock.unlockWrite(stamp); } } } } finally { // 最后根据当前持有的锁类型释放锁,避免泄漏 if (lock.isReadLockStamp(stamp)) { lock.unlockRead(stamp); } } } return value; } // 模拟数据源加载方法,实际替换为你的业务逻辑 private V loadDataFromSource(K key) { try { // 模拟IO耗时,比如数据库查询、HTTP请求 Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException("加载数据中断", e); } return (V) ("cached_data_" + key); } // 主动更新缓存的方法 public void put(K key, V value) { long stamp = lock.writeLock(); try { cache.put(key, value); } finally { lock.unlockWrite(stamp); } } // 可选:删除缓存的方法 public void remove(K key) { long stamp = lock.writeLock(); try { cache.remove(key); } finally { lock.unlockWrite(stamp); } } }
实现关键点说明:
- 乐观读优先:先尝试无锁的乐观读,只有当发现数据可能被修改时才升级为悲观锁,最大化读性能。
- 安全的读升级写:通过
tryConvertToWriteLock尝试平滑升级,失败则优雅降级为“释放读锁→获取写锁”,同时加入双重检查避免重复加载数据,完美解决了ReentrantReadWriteLock的升级痛点。 - Java8完全兼容:所有API都是Java8中StampedLock提供的,没有使用后续版本新增的方法。
- 锁安全释放:在finally块中根据锁类型释放,避免锁泄漏。
注意事项:
- StampedLock是不可重入的,同一个线程不能重复获取同一锁,否则会导致死锁,这一点和ReentrantReadWriteLock不同,一定要注意。
- 乐观读的
validate步骤不能省略,否则会读到脏数据。 - 如果缓存的Value是可变对象,建议返回不可变副本,避免外部修改导致的并发问题。
内容的提问来源于stack exchange,提问作者Adam
相关产品推荐
相关产品推荐

