ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java公平读写锁FairRWLock实现:解决写锁饥饿问题

Java公平读写锁FairRWLock实现:解决写锁饥饿问题 大家好我是专注于并发编程实战的技术博主。在构建高并发系统时读写锁Reader-Writer Lock是保护共享资源、提升读多写少场景性能的利器。然而传统的读写锁如Java的ReentrantReadWriteLock在公平性上存在一个经典难题当读锁频繁获取时写锁线程可能会被无限期地“饿死”Starvation导致数据更新严重延迟。本文将深入剖析一种解决方案——FairRWLock公平读写锁从核心概念、设计原理到完整实现手把手带你构建一个能抵抗饥饿、兼顾公平与性能的读写锁并提供可直接复用的Java代码。本文适合有一定Java并发基础了解synchronized、ReentrantLock的开发者无论是面试准备、框架源码学习还是在实际项目中设计高可靠同步组件都能从中获得系统性的知识和实战经验。1. 读写锁的饥饿问题与公平性诉求在深入FairRWLock之前我们必须先理解问题的根源。1.1 读写锁的基本原理读写锁允许多个线程同时读取共享资源但只允许一个线程进行写入。其核心目标是提升“读多写少”场景下的并发吞吐量。Java标准库中的ReentrantReadWriteLock是其典型代表。1.2 传统读写锁的饥饿Starvation场景假设一个共享资源初始时没有线程持有锁。线程A申请并获得了读锁。在线程A持有读锁期间线程B申请写锁。由于资源正被读取线程B必须等待。在线程B等待期间线程C申请读锁。在非公平策略下线程C可以立即获取读锁因为读锁之间不互斥。线程A释放读锁但线程C仍持有读锁线程B继续等待。后续可能不断有新的读线程D, E, F...加入并成功获取读锁而线程B的写锁请求被无限期地推迟。这就是写锁饥饿。在非公平模式下源源不断的读请求会“淹没”等待中的写请求导致数据无法及时更新。即使在公平模式下ReentrantReadWriteLock的实现也倾向于批量授予读锁写锁的等待时间依然可能很长。1.3 公平性的定义与权衡一个理想的公平读写锁应满足请求顺序性锁的授予应大致遵循请求到达的顺序。避免饥饿无论是读线程还是写线程都不应被无限期延迟。保持高并发在保证公平的前提下尽可能不牺牲读操作的并发性能。设计难点在于平衡严格的先来先服务FIFO会严重损害读并发因为一个写请求会阻塞后面所有的读请求而完全偏向读并发又会引发写饥饿。FairRWLock的目标就是在这个光谱上找到一个更优的平衡点。2. FairRWLock 设计思想与核心原理FairRWLock的核心设计思想是在队列中区分读者和写者并制定一套调度策略确保写者不会因为后续读者的到来而无限等待。2.1 核心数据结构等待队列我们使用一个双向链表或队列来管理等待获取锁的线程。每个队列节点需要记录线程引用。请求类型READER或WRITER。节点状态是否已获得锁、是否已取消。2.2 关键调度策略这是FairRWLock的“灵魂”。我们定义以下规则锁空闲时直接授予第一个等待节点的锁。如果是读者可以继续尝试授予后续连续读者的锁直到遇到写者这在一定程度上保持了读并发。锁被读者持有时新的读请求如果等待队列头部是读者节点且该读者已获得锁则新读者可以“搭便车”直接获取锁提升并发。新的写请求写线程必须入队等待。锁被写者持有时所有新请求读/写都必须入队等待。防写饥饿策略最关键当一个写者节点进入队列并开始等待后它之后新到达的所有读请求必须排在这个写者之后。即使队列前面有其他已获得锁的读者这些新读者也不能“插队”到等待中的写者前面。这确保了写请求不会被后续的读请求无限推后。2.3 状态管理我们需要精确跟踪exclusiveOwnerThread: 当前持有写锁的线程类似ReentrantLock。readerCount: 当前持有读锁的线程数量。head/tail: 等待队列的头尾指针。3. 环境准备与项目结构我们将使用Java实现不依赖任何第三方并发库以彻底理解其内部机制。环境要求JDK 8 或更高版本本文代码基于JDK 11语法但兼容JDK 8。IDEIntelliJ IDEA, Eclipse 或 VS Code。Maven 或 Gradle用于管理依赖和测试但核心实现无额外依赖。项目结构fair-rwlock-demo/ ├── src/main/java/com/example/fairlock/ │ ├── FairRWLock.java // 公平读写锁核心实现 │ ├── LockNode.java // 等待队列节点定义 │ └── demo/ │ └── FairRWLockDemo.java // 演示程序 ├── src/test/java/ // 单元测试可选 └── pom.xml // Maven配置文件4. FairRWLock 核心实现拆解我们将分步实现各个核心组件。4.1 定义等待队列节点 (LockNode)每个节点代表一个等待锁的线程。// 文件路径src/main/java/com/example/fairlock/LockNode.java package com.example.fairlock; import java.util.concurrent.atomic.AtomicReference; /** * 等待队列节点。 * 采用双向链表结构便于删除中间已取消的节点。 */ class LockNode { // 节点类型读者或写者 enum Type { READER, WRITER } final Thread thread; // 请求锁的线程 final Type type; // 请求类型 // 使用AtomicReference保证状态变更的可见性 final AtomicReferenceStatus status; LockNode prev; // 前驱节点 LockNode next; // 后继节点 enum Status { WAITING, // 等待中 GRANTED, // 已获得锁 CANCELLED // 已取消如线程中断 } LockNode(Thread thread, Type type) { this.thread thread; this.type type; this.status new AtomicReference(Status.WAITING); this.prev null; this.next null; } // 将状态原子性地更新为GRANTED boolean tryGrant() { return status.compareAndSet(Status.WAITING, Status.GRANTED); } // 标记为取消 void cancel() { status.set(Status.CANCELLED); } boolean isCancelled() { return status.get() Status.CANCELLED; } boolean isGranted() { return status.get() Status.GRANTED; } }4.2 公平读写锁主体实现 (FairRWLock)这是最复杂的部分我们实现Lock接口并提供readLock()和writeLock()方法。// 文件路径src/main/java/com/example/fairlock/FairRWLock.java package com.example.fairlock; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReadWriteLock; public class FairRWLock implements ReadWriteLock { // 内部读锁实现 private final ReadLock readerLock; // 内部写锁实现 private final WriteLock writerLock; // 等待队列的头节点哑节点便于操作 private volatile LockNode head; // 等待队列的尾节点 private volatile LockNode tail; // 当前持有读锁的线程数量 private volatile int readerCount; // 当前持有写锁的线程可重入 private volatile Thread exclusiveOwnerThread; // 写锁重入次数 private volatile int writeHoldCount; public FairRWLock() { this.readerLock new ReadLock(); this.writerLock new WriteLock(); // 初始化一个哑节点作为头简化边界条件处理 LockNode dummy new LockNode(null, LockNode.Type.READER); dummy.status.set(LockNode.Status.GRANTED); this.head dummy; this.tail dummy; this.readerCount 0; this.exclusiveOwnerThread null; this.writeHoldCount 0; } Override public Lock readLock() { return readerLock; } Override public Lock writeLock() { return writerLock; } // -------------------- 核心队列管理方法 -------------------- // 线程安全地添加新节点到队列尾部 private LockNode enqueue(LockNode.Type type) { LockNode node new LockNode(Thread.currentThread(), type); synchronized (this) { LockNode oldTail tail; oldTail.next node; node.prev oldTail; tail node; // 如果当前没有写者等待且是读请求尝试快速获取 if (type LockNode.Type.READER !hasWaitingWriter(node)) { tryGrantLockToReader(node); } return node; } } // 判断一个节点前面是否有正在等待的写者 private boolean hasWaitingWriter(LockNode node) { LockNode current node.prev; while (current ! head) { // 从尾部向前遍历直到头部哑节点 if (current.type LockNode.Type.WRITER !current.isGranted()) { return true; } current current.prev; } return false; } // 尝试将锁授予一个读者节点及其后面连续的读者 private void tryGrantLockToReader(LockNode startNode) { // 只有队列头部的读者节点可以被直接授予 if (startNode.prev ! head || startNode.type ! LockNode.Type.READER) { return; } synchronized (this) { // 再次检查条件防止竞态 if (startNode.prev head startNode.type LockNode.Type.READER startNode.tryGrant()) { readerCount; // 尝试继续授予后面连续的读者防写饥饿策略的关键遇到写者就停止 LockNode next startNode.next; while (next ! null next.type LockNode.Type.READER !hasWaitingWriter(next)) { if (next.tryGrant()) { readerCount; } next next.next; } } } } // 从队列中移除已取消或已完成的节点 private void cleanQueue() { synchronized (this) { LockNode node head.next; while (node ! null node.isCancelled()) { head.next node.next; if (node.next ! null) { node.next.prev head; } else { tail head; // 如果删除的是尾节点更新tail } node head.next; } } } // -------------------- 读锁实现 -------------------- private class ReadLock implements Lock { Override public void lock() { LockNode node enqueue(LockNode.Type.READER); // 自旋等待直到锁被授予或线程被中断 while (!node.isGranted()) { // 检查是否因为前面有写者而阻塞 if (hasWaitingWriter(node)) { // 如果有写者在前不能“搭便车”必须等待调度 // 此处可以park线程简化起见用yield Thread.yield(); } else { // 再次尝试获取 tryGrantLockToReader(node); } if (Thread.interrupted()) { node.cancel(); cleanQueue(); throw new RuntimeException(new InterruptedException()); } } } Override public void unlock() { synchronized (FairRWLock.this) { if (readerCount 0) { throw new IllegalMonitorStateException(Attempt to unlock read lock, not held by current thread); } readerCount--; // 读锁释放后尝试唤醒队列中的下一个等待者可能是读者或写者 wakeUpNext(); } } // 以下为Lock接口其他方法的基本实现省略部分细节 Override public void lockInterruptibly() throws InterruptedException { lock(); } Override public boolean tryLock() { /* 简化实现立即尝试入队并检查 */ return false; } Override public boolean tryLock(long time, TimeUnit unit) { /* 带超时尝试 */ return false; } Override public Condition newCondition() { throw new UnsupportedOperationException(); } } // -------------------- 写锁实现 -------------------- private class WriteLock implements Lock { Override public void lock() { // 支持写锁重入 if (exclusiveOwnerThread Thread.currentThread()) { writeHoldCount; return; } LockNode node enqueue(LockNode.Type.WRITER); // 写者必须严格排队等待成为队列头部且锁可用 while (true) { synchronized (FairRWLock.this) { // 检查是否可以获取写锁无读者且无其他写者持有锁且本节点是队列中第一个未授予的节点 if (readerCount 0 exclusiveOwnerThread null isFirstUngrantedNode(node)) { if (node.tryGrant()) { exclusiveOwnerThread Thread.currentThread(); writeHoldCount 1; break; } } } if (Thread.interrupted()) { node.cancel(); cleanQueue(); throw new RuntimeException(new InterruptedException()); } Thread.yield(); } } // 判断一个节点是否是队列中第一个未被授予锁的节点 private boolean isFirstUngrantedNode(LockNode node) { LockNode current head.next; while (current ! null) { if (current.isCancelled()) { current current.next; continue; } // 找到第一个未取消的节点检查是否是目标节点且未授予 return current node !current.isGranted(); } return false; } Override public void unlock() { synchronized (FairRWLock.this) { if (exclusiveOwnerThread ! Thread.currentThread()) { throw new IllegalMonitorStateException(Attempt to unlock write lock, not held by current thread); } writeHoldCount--; if (writeHoldCount 0) { exclusiveOwnerThread null; // 写锁释放唤醒队列中的下一个等待者 wakeUpNext(); } } } // 以下为Lock接口其他方法的基本实现 Override public void lockInterruptibly() throws InterruptedException { lock(); } Override public boolean tryLock() { /* 简化实现 */ return false; } Override public boolean tryLock(long time, TimeUnit unit) { return false; } Override public Condition newCondition() { throw new UnsupportedOperationException(); } } // 唤醒队列中下一个合适的等待节点 private void wakeUpNext() { synchronized (this) { LockNode node head.next; while (node ! null node.isCancelled()) { // 跳过已取消的节点 head.next node.next; if (node.next ! null) { node.next.prev head; } else { tail head; } node head.next; } if (node null) { return; // 队列为空 } if (node.type LockNode.Type.READER) { // 如果是读者尝试授予锁可能是一批连续的读者 tryGrantLockToReader(node); } else if (node.type LockNode.Type.WRITER readerCount 0) { // 如果是写者并且当前没有读者尝试授予写锁 if (node.tryGrant()) { exclusiveOwnerThread node.thread; writeHoldCount 1; } } // 如果节点未能被授予它将继续在lock()方法中循环等待 } } // 一些状态查询方法便于监控和测试 public synchronized int getQueueLength() { int count 0; LockNode node head.next; while (node ! null) { if (!node.isCancelled()) { count; } node node.next; } return count; } public synchronized boolean hasQueuedThreads() { return head.next ! null; } }5. 完整实战案例与测试我们编写一个演示程序模拟读多写少的场景并展示FairRWLock如何防止写饥饿。5.1 演示程序模拟读写混合负载// 文件路径src/main/java/com/example/fairlock/demo/FairRWLockDemo.java package com.example.fairlock.demo; import com.example.fairlock.FairRWLock; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; public class FairRWLockDemo { // 共享资源 private static int sharedData 0; private static final FairRWLock lock new FairRWLock(); // 用于统计操作次数 private static final AtomicInteger readCount new AtomicInteger(0); private static final AtomicInteger writeCount new AtomicInteger(0); public static void main(String[] args) throws InterruptedException { System.out.println( FairRWLock 防饥饿演示 ); System.out.println(模拟场景大量读线程中写线程是否会被饿死); int readerThreads 10; int writerThreads 2; int operationsPerThread 100; ExecutorService executor Executors.newFixedThreadPool(readerThreads writerThreads); CountDownLatch latch new CountDownLatch(readerThreads writerThreads); // 启动读线程 for (int i 0; i readerThreads; i) { final int threadId i; executor.submit(() - { try { for (int j 0; j operationsPerThread; j) { readData(threadId); // 读操作间隙模拟一些处理时间 Thread.sleep((long) (Math.random() * 10)); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { latch.countDown(); } }); } // 启动写线程 for (int i 0; i writerThreads; i) { final int threadId i; executor.submit(() - { try { for (int j 0; j operationsPerThread; j) { writeData(threadId, j); // 写操作间隙更长 Thread.sleep((long) (Math.random() * 50)); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { latch.countDown(); } }); } // 等待所有任务完成 latch.await(30, TimeUnit.SECONDS); executor.shutdownNow(); System.out.println(\n 演示结果 ); System.out.println(总读操作次数: readCount.get()); System.out.println(总写操作次数: writeCount.get()); System.out.println(最终共享数据值: sharedData); System.out.println(理论正确值每个写线程累加100次: (writerThreads * operationsPerThread)); System.out.println(队列最大长度近似: lock.getQueueLength()); } private static void readData(int readerId) { lock.readLock().lock(); try { int localCopy sharedData; // 读取共享数据 readCount.incrementAndGet(); // 模拟读取耗时 Thread.sleep(1); // 可以在这里验证读取的数据一致性简单演示不展开 } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { lock.readLock().unlock(); } } private static void writeData(int writerId, int value) { lock.writeLock().lock(); try { int oldValue sharedData; sharedData oldValue 1; // 写操作递增 writeCount.incrementAndGet(); System.out.printf(Writer-%d 成功写入将值从 %d 更新为 %d (写操作总数: %d)%n, writerId, oldValue, sharedData, writeCount.get()); // 模拟写入耗时 Thread.sleep(5); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { lock.writeLock().unlock(); } } }5.2 运行与结果分析运行上述FairRWLockDemo观察控制台输出。你会看到读操作非常频繁。但写操作并没有被完全阻塞它们会穿插在读操作之间执行尽管读线程数量远多于写线程。最终sharedData的值会等于2002个写线程各累加100次证明所有写操作都得到了执行没有丢失。关键现象使用传统非公平ReentrantReadWriteLock时在上述密集读场景下控制台可能长时间看不到写线程的输出或者写操作集中在最后才执行。而FairRWLock的输出中写操作的日志会相对均匀地出现这表明防饥饿策略在起作用。6. 常见问题与排查思路在实现和使用自定义锁时可能会遇到以下问题问题现象可能原因排查与解决思路死锁1.unlock()未在finally块中调用。2. 锁内部状态机错误导致wakeUpNext逻辑无法唤醒后续线程。3. 重入逻辑有误导致锁计数混乱。1. 确保所有lock()调用都有配对的unlock()且放在finally中。2. 使用jstack或可视化工具检查线程状态看哪些线程在BLOCKED或WAITING。3. 在锁实现中添加详细的调试日志打印lock/unlock调用序列和队列状态。写线程依然被长时间阻塞1. 虽然防饥饿策略生效但前面仍有大量已入队的读线程。2.hasWaitingWriter判断逻辑有缺陷未能正确识别“前面的写者”。1. 这是设计权衡绝对公平可能导致吞吐量下降。可以调整策略如允许写者“插队”到少量读者之前。2. 仔细检查hasWaitingWriter的遍历逻辑确保是从当前节点向前遍历到队列头。性能远低于ReentrantReadWriteLock1. 队列操作enqueue,cleanQueue在synchronized块内成为瓶颈。2. 自旋等待Thread.yield()消耗CPU。1. 考虑使用无锁队列如AtomicReferenceCAS管理节点减少同步块范围。2. 将自旋等待替换为LockSupport.park()/unpark()让线程真正挂起减少CPU占用。IllegalMonitorStateException1. 线程尝试释放未持有的锁。2. 读锁计数readerCount或写锁重入计数writeHoldCount在并发下出错。1. 确保锁的持有和释放是线程匹配的。可以在节点中记录持有锁的线程ID在unlock时验证。2. 对所有共享状态readerCount,writeHoldCount的访问必须放在同步块或使用volatile原子操作。7. 最佳实践与工程建议在生产和高级场景中使用或改进FairRWLock时请考虑以下建议7.1 性能优化方向减少全局锁粒度当前实现使用synchronized(this)保护整个队列和状态。可以拆分为使用ReadWriteLock本身来保护内部队列有点递归需谨慎设计。使用StampedLock的乐观读来检查队列状态。将队列操作改为无锁的CAS操作。使用LockSupport替代自旋// 在LockNode中增加一个字段 // private volatile Thread parkedThread; // 在等待循环中 while (!node.isGranted()) { if (!shouldPark) { Thread.yield(); continue; } LockSupport.park(this); // 挂起线程 // 被唤醒后继续检查条件 } // 在wakeUpNext中找到需要唤醒的节点后 LockSupport.unpark(node.thread);引入“写者优先”权重可以在策略上微调允许写者在等待一定时间后优先于新到达的读者获取锁进一步减少写延迟。7.2 功能增强支持可重入读锁当前实现未区分不同线程的读锁重入。需要为每个读线程维护一个持有计数。支持锁降级允许持有写锁的线程同时获取读锁然后释放写锁实现从写锁到读锁的降级。这是ReentrantReadWriteLock支持的有用特性。支持公平模式选择可以提供构造函数参数让用户在“完全公平”、“读优先”、“写优先”等策略间选择。添加监控接口提供getReadLockCount()、getWriteHoldCount()、getQueuedReaderThreads()、getQueuedWriterThreads()等方法便于系统监控和调试。7.3 测试与验证编写全面的单元测试覆盖以下场景读读并发不阻塞。读写互斥。写写互斥。写锁重入。锁的公平性验证写不会被饿死。线程中断响应。压力测试在高并发下如100线程运行长时间检查是否有死锁、活锁或性能骤降。与标准库对比测试在相同负载下对比FairRWLock与ReentrantReadWriteLock公平模式的吞吐量和写延迟。7.4 生产环境使用须知谨慎评估自定义锁的实现非常复杂极易引入隐蔽的并发Bug。除非有明确需求且标准库无法满足否则优先使用经过千锤百炼的ReentrantReadWriteLock或StampedLock。明确需求仅在确实遇到写饥饿问题且调整ReentrantReadWriteLock策略无法解决时才考虑使用或借鉴此类公平读写锁。代码审查对自定义同步器进行严格的多人代码审查重点关注状态转换和并发边界条件。逐步灰度如果决定使用先在非核心、低并发场景试点观察稳定性和性能表现。通过本文我们从问题出发深入探讨了读写锁的饥饿问题并一步步设计并实现了一个具备防饥饿能力的FairRWLock。这不仅是一个可用的同步工具更是一次对并发控制原语的深度实践。理解这些底层机制对于诊断线上锁竞争问题、优化高并发代码性能有极大帮助。建议读者将代码下载到本地运行演示程序并尝试修改参数如读写线程比例、睡眠时间观察锁行为的变化从而获得更直观的理解。
返回列表