前期回顾利用aqs构建一个自己的非公平独占锁利用aqs构建一个自己的公平独占锁利用 AQS 构建一个自己的非公平共享锁本期目标实现一个公平共享锁在前一篇文章中我们实现了一个非公平的共享锁它允许新来的线程与等待队列中的线程竞争许可可能会导致某些线程长时间得不到执行“饥饿”现象。今天我们将在此基础上进行改造实现一个公平的共享锁。公平锁的核心思想是严格按照线程进入等待队列的顺序来获取锁先等待的线程先获得许可从而避免饥饿。共享锁与独占锁在公平性实现上的主要区别在于在尝试获取锁时需要先判断当前线程是否位于等待队列的头部或队列是否为空如果是才允许参与竞争否则直接进入队列等待。代码实现importjava.util.concurrent.locks.AbstractQueuedSynchronizer;/** * 基于 AQS 实现的公平共享锁 * 特点 * 1. 多个线程可以同时持有共享模式 * 2. 按照等待队列的顺序公平获取许可 * 3. 状态值state表示剩余许可数量 */publicclassMyFairSharedLock{// 内部同步器类privatestaticclassSyncextendsAbstractQueuedSynchronizer{// 构造时指定最大许可数Sync(intpermits){if(permits0)thrownewIllegalArgumentException(Permits must be 0);setState(permits);}/** * 尝试获取共享锁公平模式 * param arg 本次要获取的许可数固定为1 * return 负数表示失败非负数表示成功且返回剩余许可数 */OverrideprotectedinttryAcquireShared(intarg){if(arg!1)thrownewIllegalArgumentException(Arg must be 1);// 公平性判断如果等待队列中有其他线程在排队且当前线程不是队首则直接返回失败// hasQueuedPredecessors() 是 AQS 提供的核心方法// - 返回 true表示队列中有其他线程等待的时间比当前线程更长即当前线程需要排队// - 返回 false表示队列为空 或 当前线程就是队首节点可以尝试获取if(hasQueuedPredecessors()){return-1;// 有前辈在排队当前线程不能插队}// 没有前辈等待尝试 CAS 更新状态for(;;){intcurrentgetState();intnextcurrent-arg;if(next0){// 许可不足获取失败return-1;}if(compareAndSetState(current,next)){// 获取成功返回剩余许可数用于传播唤醒returnnext;}}}/** * 尝试释放共享锁 * param arg 本次要释放的许可数固定为1 * return true 表示释放成功且可能唤醒等待线程 */OverrideprotectedbooleantryReleaseShared(intarg){if(arg!1)thrownewIllegalArgumentException(Arg must be 1);for(;;){intcurrentgetState();intnextcurrentarg;// 简单起见不考虑溢出if(compareAndSetState(current,next)){returntrue;// 释放成功触发后续唤醒}}}// 获取当前剩余许可数intgetPermits(){returngetState();}}privatefinalSyncsync;/** * 构造函数指定最大许可数 */publicMyFairSharedLock(intpermits){syncnewSync(permits);}/** * 获取锁共享模式公平 */publicvoidlock(){sync.acquireShared(1);}/** * 释放锁 */publicvoidunlock(){sync.releaseShared(1);}/** * 尝试获取锁非阻塞 */publicbooleantryLock(){returnsync.tryAcquireShared(1)0;}/** * 当前剩余许可数 */publicintavailablePermits(){returnsync.getPermits();}}关键机制解析1. 公平性的核心hasQueuedPredecessors()与非公平版本相比唯一的关键变化就在 tryAcquireShared 方法中增加了一行判断if(hasQueuedPredecessors()){return-1;// 有前辈在排队当前线程不能插队}hasQueuedPredecessors() 是 AQS 提供的一个工具方法它的判断逻辑是返回 true表示同步队列中已经有其他线程在等待并且当前线程不是队列中的第一个等待节点即存在更早等待的线程。此时当前线程不应该尝试获取锁而应该直接返回失败进入队列排队。返回 false表示队列为空或者当前线程本身就是队列中的第一个等待节点被唤醒后重试。此时可以尝试获取锁。这个简单的检查就彻底消除了“新线程插队”的可能性保证了获取顺序的公平性。2. 为什么公平锁仍需要 hasQueuedPredecessors() 后的循环 CAS既然判断了没有前辈等待那么直接 CAS 不就行了吗为什么还要用 for (; 循环这是因为 hasQueuedPredecessors() 返回 false 的瞬间队列状态可能发生变化例如另一个线程恰好在这一刻释放了锁并唤醒了队首节点。但更重要的原因是当线程被唤醒后再次尝试获取锁时它已经是队首节点了此时 hasQueuedPredecessors() 返回 false但它仍然需要尝试 CAS 来实际获取许可。如果 CAS 失败比如剩余许可被其他并发线程抢先消耗了它需要自旋重试而不是直接返回失败否则会导致虚假唤醒。测试代码importjava.util.concurrent.CountDownLatch;publicclassMyFairSharedLockTest{// 最大许可数设为 1这样一次只能有一个线程持有锁可以更清楚地观察获取顺序privatestaticfinalMyFairSharedLocklocknewMyFairSharedLock(1);privatestaticintsharedData0;staticclassWorkerextendsThread{privatefinalintid;privatefinalCountDownLatchlatch;// 用于让所有线程同时启动Worker(intid,CountDownLatchlatch){this.idid;this.latchlatch;}Overridepublicvoidrun(){try{latch.await();// 等待发令枪确保同时启动System.out.printf([%tT] 线程 %d 开始尝试获取锁...%n,System.currentTimeMillis(),id);lock.lock();System.out.printf([%tT] 线程 %d 成功获取锁开始工作剩余许可: %d%n,System.currentTimeMillis(),id,lock.availablePermits());// 模拟工作保持持有锁一段时间Thread.sleep(1000);System.out.printf([%tT] 线程 %d 工作结束释放锁%n,System.currentTimeMillis(),id);}catch(InterruptedExceptione){Thread.currentThread().interrupt();}finally{lock.unlock();}}}publicstaticvoidmain(String[]args)throwsInterruptedException{intthreadCount6;CountDownLatchstartLatchnewCountDownLatch(1);Worker[]workersnewWorker[threadCount];// 按顺序创建线程id 从 0 到 5for(inti0;ithreadCount;i){workers[i]newWorker(i,startLatch);}// 按顺序启动线程虽然启动有先后但都会在 latch 处等待for(Workerw:workers){w.start();}// 稍作停顿确保所有线程都已进入等待状态Thread.sleep(100);System.out.println( 发令枪响所有线程同时开始竞争 );startLatch.countDown();// 所有线程同时开始尝试获取锁// 等待所有线程结束for(Workerw:workers){w.join();}System.out.println(所有线程执行完毕);}} 发令枪响所有线程同时开始竞争 [20:30:01] 线程 0 开始尝试获取锁... [20:30:01] 线程 1 开始尝试获取锁... [20:30:01] 线程 2 开始尝试获取锁... ... [20:30:01] 线程 0 成功获取锁开始工作剩余许可: 0 [20:30:02] 线程 0 工作结束释放锁 [20:30:02] 线程 1 成功获取锁开始工作剩余许可: 0 [20:30:03] 线程 1 工作结束释放锁 [20:30:03] 线程 2 成功获取锁开始工作剩余许可: 0 ...总结本期我们完成了从非公平共享锁到公平共享锁的演进。核心改动非常小仅仅是在 tryAcquireShared 中添加了 hasQueuedPredecessors() 的前置检查但这一个小小的改动却从根本上改变了锁的获取策略。AQS 的设计体现了“模板方法模式”的精妙之处它把复杂的队列管理、阻塞唤醒等底层细节都封装好了留给我们的只需要根据需求重写几个关键方法tryAcquireShared / tryReleaseShared通过修改 state 的语义和竞争逻辑就能轻松实现各种不同特性的同步器。