资讯动态

Java进阶篇之ReadWriteLock:让读取并行,让更新保持完整

发布时间:2026/9/30 8:55:15 来源:尧图企业网站定制
上一篇通过Condition讨论了“业务条件什么时候满足”。接下来换一个常见场景一份共享数据被频繁查询偶尔更新。所有查询都排成一队有时会浪费并行机会放任读写交错又容易看到更新到一半的数据。ReadWriteLock为这类问题提供了读锁与写锁的配对协议。本文基于Java 21以ReentrantReadWriteLock为例先把访问边界划清再讨论它适合解决什么问题。一、读与写先看操作是否改变共享状态“查询方法”这个名字并不保证方法只读。例如获取数据时顺手更新访问次数、填充缓存就已经包含写操作。划分读写应检查实际行为。不同线程的访问组合是否允许同时持锁读与读可以读与写不可以写与写不可以同一个ReadWriteLock对象管理这一对锁。读路径和写路径若各自创建一个锁对象就失去了共同约束。遵守同一锁协议时写锁释放前的更新对后续获得对应读锁的线程可见。接口契约见Java 21 ReadWriteLock文档。二、把锁边界放在数据结构外层下面用商品目录作教学例子。内部是普通HashMap更新持有写锁读取时在读锁保护下复制快照然后把不可修改的结果交给调用者。图中的并排阅读对应多个读锁持有者等待更新的卡片对应写请求。它是访问规则的示意不表示公平队列中的精确调度顺序。三、完整示例读取快照再更新目录保存为ReadWriteDemo.java使用JDK21运行importjava.util.HashMap;importjava.util.Map;importjava.util.concurrent.*;importjava.util.concurrent.locks.ReentrantReadWriteLock;publicclassReadWriteDemo{staticclassCatalog{privatefinalMapString,IntegerpricesnewHashMap();privatefinalReentrantReadWriteLocklocknewReentrantReadWriteLock();voidput(Stringname,intprice){lock.writeLock().lock();try{prices.put(name,price);}finally{lock.writeLock().unlock();}}MapString,Integersnapshot(){lock.readLock().lock();try{returnMap.copyOf(prices);}finally{lock.readLock().unlock();}}}publicstaticvoidmain(String[]args)throwsException{CatalogcatalognewCatalog();catalog.put(book,42);try(ExecutorServicepoolExecutors.newFixedThreadPool(2)){FutureMapString,Integerfirstpool.submit(catalog::snapshot);FutureMapString,Integersecondpool.submit(catalog::snapshot);System.out.println(reader-1: first.get());System.out.println(reader-2: second.get());pool.submit(()-catalog.put(book,45)).get();System.out.println(after update: catalog.snapshot());}}}javac ReadWriteDemo.javajavaReadWriteDemo输出reader-1: {book42} reader-2: {book42} after update: {book45}这里通过Future.get明确等待两个查询结束再提交更新因此输出便于复现。线程池提供了两个工作线程但这段输出不能证明两次读取在时间上重叠也不能当作性能测试。示例重点是共享数据的访问封装。Map.copyOf在持锁期间复制结构返回后调用者无法修改这个Map。示例的String和Integer也是不可变对象。如果value换成可变对象浅复制仍会共享value引用应进一步复制数据或约束对象的修改方式。四、容易踩坑的地方升级、降级与复查ReentrantReadWriteLock支持写锁降级为读锁持有写锁时先取得读锁再释放写锁。这样从更新转入读取时中间没有允许其他写线程插入的空档。相反线程持有读锁时直接调用writeLock().lock不能完成读锁升级。需要更新时应先释放读锁再竞争写锁拿到写锁后重新检查业务状态。释放与重新获取之间别的线程可能已经完成了更新。具体约束见ReentrantReadWriteLock文档。例如“缓存无效则刷新”的流程应包含两次判断读锁内发现无效释放读锁并取得写锁后再判断是否仍然无效。第二次判断能避免多个线程接连做同一份刷新工作。降级适合“更新后需要继续在保护下使用数据”的情况。如果拿到的是完整不可变快照通常可以释放锁后处理快照结构也更简单。五、使用前再核对四个问题所有访问是否遵守协议不能一部分方法加锁另一部分直接操作内部Map也不要把内部可变集合原样返回。锁内是否有慢操作网络调用、文件访问和耗时计算可能长时间挡住写线程。可以先在锁外准备候选数据再用短写锁提交但提交前要检查版本或前提是否仍成立。读多是否就一定更快读写锁自身也有协调成本。数据很小、读操作极短或写入频繁时收益需要通过贴近实际负载的基准测试确认。本文不提供未经测量的加速比例。是否需要公平性或Condition默认构造是非公平策略可使用new ReentrantReadWriteLock(true)选择公平策略无参数tryLock不会遵守公平排队设置。只有写锁支持newCondition读锁调用会抛出UnsupportedOperationException。这也衔接了上一篇Condition的使用边界。对于独立key更新ConcurrentHashMap可能更合适需要协调多个字段、多个key或整体快照时可以考虑在共同的数据边界上使用读写锁。选择之前先写清楚一次业务操作必须一起保持一致的内容。六、 思维导图ReadWriteLock访问规则读读共享读写互斥数据边界同一对锁快照不泄漏可变状态锁转换写锁可降级释放读锁后重新检查使用取舍缩短持锁时间按实际负载测量七、总结总结要点读写分离的锁协议允许读取共享、更新独占。实际访问必须使用同一对锁内部数据不能从其他路径绕开保护。快照与对象边界同样重要。保护Map结构不等于保护其中所有可变对象返回值也应符合并发约定。锁转换与性能取舍需要具体分析。释放读锁后要复查状态降级按顺序完成是否值得引入读写锁应由实际工作负载验证。下一篇继续讨论StampedLock理解乐观读取为什么需要校验以及它与可重入读写锁的区别。如果你觉得这篇文章对你有所帮助欢迎点赞、收藏、分享

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑