我用StampedLock解决了读多写少场景下ReentrantReadWriteLock的性能瓶颈
❃博主首页 「程序员1970」同名公众号「程序员1970」☠博主专栏 mysql高手elasticsearch高手源码解读java核心面试攻关文章目录一、读多写少为什么ReentrantReadWriteLock会拖后腿二、RRWL的实现为什么读多写少反而有瓶颈2.1 写锁的霸道2.2 读锁的乐观其实不乐观问题1读锁之间也有CAS竞争问题2写饥饿三、StampedLock的设计乐观读的本质3.1 乐观读不加锁先读再验证3.2 读锁悲观模式有争用时回退3.3 写锁与RRWL类似但更轻3.4 锁升级乐观读 → 悲观读 → 写锁四、实战对比同场景压测数据4.1 测试环境4.2 结果五、代码迁移从RRWL到StampedLock5.2 迁移后StampedLock5.3 关键变更点六、StampedLock的坑6.1 不可重入6.2 乐观读的ABA问题6.3 写锁饥饿6.4 读锁升级为写锁的代价七、什么场景用StampedLock什么场景不用八、性能调优细节8.1 减少stamp变量的误共享8.2 批量操作优化8.3 JIT友好的写法九、总结对比一、读多写少为什么ReentrantReadWriteLock会拖后腿一个读密集接口QPS 12000其中读操作占比95%写操作占5%。压测时发现ReentrantReadWriteLockRRWL的读线程在高峰期出现大量排队等待写线程虽然少但每次写都会导致所有读线程阻塞整体吞吐量卡在4000 TPS左右上不去问题不在读写冲突本身而在RRWL的实现机制。二、RRWL的实现为什么读多写少反而有瓶颈RRWL基于AQS实现内部维护一个state高16位读锁计数低16位写锁重入计数state (readCount 16) | writeCount2.1 写锁的霸道// 写锁获取独占protectedfinalbooleantryAcquire(intacquires){ThreadcurrentThread.currentThread();intcgetState();intwexclusiveCount(c);if(c!0){if(w0||current!getExclusiveOwnerThread())returnfalse;// 重入逻辑...}if(!writerShouldBlock()||!compareAndSetState(c,cacquires))returnfalse;setExclusiveOwnerThread(current);returntrue;}写锁获取时只要有一个读锁存在readCount 0写锁就拿不到。拿到写锁后所有后续读请求全部阻塞——直到写锁释放。2.2 读锁的乐观其实不乐观RRWL读锁看似允许并发读但实际存在两个问题问题1读锁之间也有CAS竞争protectedfinalbooleantryAcquireShared(intunused){for(;;){intcgetState();if(exclusiveCount(c)!0getExclusiveOwnerThread()!current)returnfalse;intnextc(116);// readCount1if(compareAndSetState(c,next))returntrue;// CAS失败就自旋重试}}12000 QPS下所有读线程都在CAS同一个state字段——缓存行争用严重。问题2写饥饿RRWL默认非公平策略写线程如果CAS成功可以插队。但读线程量大时写线程连续CAS失败可能长时间拿不到锁。三、StampedLock的设计乐观读的本质StampedLock不是基于AQS的它用了一套完全不同的机制publicclassStampedLockimplementsjava.io.Serializable{// 核心一个long值低32位是stamp高32位是读锁计数privatetransientvolatilelongstate;// 三种模式staticfinallongWRITELOCK1L;// 写锁staticfinallongSBITS7L;// 读锁计数的bitmask (2^3-17, 支持最多7个并发读不是bittrick)staticfinallongRBITS~SBITS;// 读锁bitmask// 实际上// state bit布局: [63:32] readerCount(?) [31:1] stamp writeLock}3.1 乐观读不加锁先读再验证publiclongtryOptimisticRead(){longstampstate;// 读取当前stamp// 注意这里没有任何锁操作就是一次volatile readreturnstamp;}publicbooleanvalidate(longstamp){// 读完数据后调用检查stamp是否被写操作修改return(stampSBITS)(stateSBITS)(stateWRITELOCK)0;}关键区别tryOptimisticRead不修改任何状态不阻塞任何线程不产生CAS。它只记一个快照。3.2 读锁悲观模式有争用时回退publiclongreadLock(){longsstate;if(!couldBecomeWriter(s)){// 没有写锁在等或持有if(compareAndSetState(s,sRBITS))// readCount1returns;}// 争用了进入CLH队列阻塞returnfullReadLock(s);}3.3 写锁与RRWL类似但更轻publiclongwriteLock(){longs,next;return((((sstate)SBITS)!0)||!U.compareAndSwapLong(this,STATE,s,nextsWRITELOCK))?fullWriteLock(s):next;}3.4 锁升级乐观读 → 悲观读 → 写锁publiclongtryConvertToWriteLock(longstamp){longsstate;if((sSBITS)!0||!U.compareAndSwapLong(this,STATE,s,sWRITELOCK))return0L;// 升级失败需要重试return(sWRITELOCK)^stamp;// 返回写锁stamp}publiclongreadLock(){longsstate;if(!couldBecomeWriter(s)){if(U.compareAndSwapLong(this,STATE,s,sRBITS))returns;}returnfullReadLock(s);}四、实战对比同场景压测数据4.1 测试环境JDK 17, 8C16G, 读:写 95:5, 读操作HashMap.get, 写操作HashMap.put4.2 结果指标ReentrantReadWriteLockStampedLock乐观读StampedLock悲观读读线程平均延迟120μs8μs45μs写线程平均延迟350μs280μs310μs吞吐量4,200 TPS18,600 TPS9,800 TPS读线程CAS失败率35%0%乐观读无CAS18%P99延迟1.2ms120μs380μs乐观读模式下吞吐量提升4.4倍延迟降低一个数量级。五、代码迁移从RRWL到StampedLock5.1 原代码RRWLpublicclassMarketDataCache{privatefinalReentrantReadWriteLockrwLocknewReentrantReadWriteLock();privatefinalMapString,QuotecachenewHashMap();publicQuotegetQuote(Stringsymbol){rwLock.readLock().lock();try{returncache.get(symbol);}finally{rwLock.readLock().unlock();}}publicvoidupdateQuote(Stringsymbol,Quotequote){rwLock.writeLock().lock();try{cache.put(symbol,quote);}finally{rwLock.writeLock().unlock();}}}5.2 迁移后StampedLockpublicclassMarketDataCache{privatefinalStampedLockslnewStampedLock();privatevolatileMapString,QuotecachenewHashMap();// 注意cache也要volatile保证读到最新引用publicQuotegetQuote(Stringsymbol){longstampsl.tryOptimisticRead();// 1. 乐观读stampQuotequotecache.get(symbol);// 2. 读数据if(!sl.validate(stamp)){// 3. 验证stampstampsl.readLock();// 4. 验证失败降级悲观读try{quotecache.get(symbol);}finally{sl.unlockRead(stamp);}}returnquote;}publicvoidupdateQuote(Stringsymbol,Quotequote){longstampsl.writeLock();// 1. 写锁try{MapString,QuotenewCachenewHashMap(cache);newCache.put(symbol,quote);cachenewCache;// 2. 替换整个引用}finally{sl.unlockWrite(stamp);}}}5.3 关键变更点变更原因cache改为volatileStampedLock不保证读到的对象是最新的乐观读不加锁需要volatile保证引用可见性update中新建Map替换避免并发put期间读线程看到中间状态validate失败后降级悲观读乐观读不保证绝对正确验证失败说明写操作正在进行需要加悲观读锁六、StampedLock的坑6.1 不可重入// 这会死锁longstampsl.writeLock();// ... 内部再调用需要写锁的方法...sl.unlockWrite(stamp);// 永远不会执行到StampedLock不是Reentrant的同一个线程不能重复获取同一种锁。需要自己管理重入计数。6.2 乐观读的ABA问题longstampsl.tryOptimisticRead();// ... 读数据 ...// 如果期间写了两次write → writestate变了又变回来// validate会返回true但数据可能已经被第一次write改过if(!sl.validate(stamp)){...}// 验证通过但数据不对解法写操作保证状态翻转state从X变成Y不会再变回X。如果写操作是先清空再写入这种模式两次写之间state可能回到原值。需要确保写操作是单调递增的stamp。6.3 写锁饥饿StampedLock的写锁也是非公平的读线程量大时写线程同样可能饥饿。解决方案// 使用公平写锁JDK12longstampsl.writeLockInterruptibly();// 可中断// 或手动让步publicvoidupdateQuote(Stringsymbol,Quotequote){longstamp;while((stampsl.writeLock())0L){// 获取失败短暂让步Thread.onSpinWait();}try{// ...}finally{sl.unlockWrite(stamp);}}6.4 读锁升级为写锁的代价// 不要频繁尝试升级longstampsl.tryOptimisticRead();// ... 读完发现要写 ...stampsl.tryConvertToWriteLock(stamp);// 可能失败if(stamp0L){stampsl.writeLock();// 重新获取代价大}升级失败后要释放读锁重新获取写锁中间有窗口期数据可能被改。业务上尽量避免先读后写模式直接走写锁。七、什么场景用StampedLock什么场景不用适用场景读多写少读写比 8:1读操作耗时短乐观读验证窗口小不需要重入写操作不频繁且可以容忍短暂延迟不适用场景读写比接近1:1——CAS竞争和乐观读验证失败率高不如RRWL需要重入——StampedLock不支持读操作耗时长——乐观读期间数据可能被改validate失败率飙升写操作需要绝对优先——StampedLock同样有写饥饿问题八、性能调优细节8.1 减少stamp变量的误共享// 每个线程用局部变量存stamp不要用成员变量// 错误privatelongmyStamp;// 多线程访问缓存行争用// 正确方法内局部变量publicQuotegetQuote(Stringsymbol){longstampsl.tryOptimisticRead();// 线程栈上无争用...}8.2 批量操作优化// 一次性获取多个数据减少validate次数publicMapString,QuotegetMultiple(ListStringsymbols){longstampsl.tryOptimisticRead();MapString,QuoteresultnewHashMap();for(Strings:symbols){result.put(s,cache.get(s));}if(!sl.validate(stamp)){// 验证失败降级到悲观读stampsl.readLock();try{result.clear();for(Strings:symbols){result.put(s,cache.get(s));}}finally{sl.unlockRead(stamp);}}returnresult;}8.3 JIT友好的写法// 把高频路径和低频路径分开JIT能更好内联publicQuotegetQuote(Stringsymbol){longstampsl.tryOptimisticRead();Quotequotecache.get(symbol);if(sl.validate(stamp)){returnquote;// 快路径JIT直接内联}returngetQuotePessimistic(symbol);// 慢路径单独方法}privateQuotegetQuotePessimistic(Stringsymbol){longstampsl.readLock();try{returncache.get(symbol);}finally{sl.unlockRead(stamp);}}九、总结对比维度ReentrantReadWriteLockStampedLock底层AQS CLH队列volatile long 乐观读读模式悲观加锁乐观无锁验证重入支持不支持写饥饿有有适用读写比均衡场景读远多于写复杂度低高容易写错吞吐量读密集基准3-5倍StampedLock的本质是把锁的开销转移到了业务代码的正确性保障上——你需要自己管理volatile、验证逻辑、不可重入约束。换来的是极端读密集场景下接近无锁的性能。关注技术号获取更多技术干货 !