当前位置: 首页 > news >正文

AQS与ReentrantLock:从排队抢锁到公平与非公平的工程实践——JUC锁机制的基石

大家好我是程序员小策。先来几个灵魂拷问热热身AQS 的全称是什么它到底是个队列还是个锁ReentrantLock 和 synchronized 都能加锁为什么要有两个公平锁和非公平锁差一行代码性能差几倍CountDownLatch、Semaphore、ReentrantLock它们底层居然是同一套代码AQS 的等待队列是双向链表还是单向链表入队操作为什么不是原子的大部分人能回答前两个到第三个开始犹豫到第五个就卡住了。今天这篇文章就是要把这五个问题一个一个拆开。而且不只是讲 JDK 源码——我会从真实项目出发让你看到 AQS 在生产代码里到底长什么样。问题定义synchronized 够用了为什么还要 AQSJava 已经有synchronized了——关键字一加锁就有了。那 Doug Lea 为什么还要设计一整套java.util.concurrent.locks包因为synchronized有三个硬伤不可中断线程拿到锁之后其他等待的线程只能死等不能被 interrupt 唤醒不可超时没有tryLock(timeout)这种等一会就放弃的机制不可扩展你没法基于synchronized做出 CountDownLatch、Semaphore、ReadWriteLock 这些东西朴素方案每个功能都从零实现一套等待队列 阻塞/唤醒逻辑。问题CountDownLatch 需要队列Semaphore 需要队列ReentrantLock 也需要队列——它们的排队逻辑几乎一模一样只是什么时候算拿到锁的判断条件不同。能不能把排队逻辑抽出来让子类只关心能不能拿到锁这就是 AQS 的设计动机。核心概念AQS 是什么AQSAbstractQueuedSynchronizer一个用于构建锁和同步器的框架内部维护一个 volatile int state 和一个 CLH 变体的双向 FIFO 等待队列。子类只需实现尝试获取/释放 state的逻辑排队和阻塞由 AQS 负责。想象一个高速 ETC 收费站。收费站有一个核心状态栏杆是抬起还是放下对应 AQS 的state。车辆线程到达时先看栏杆——抬着就直接过CAS 抢锁成功放下就排队等候。排队区是一条有序的车道CLH 双向队列先来的车排在前面。当前面的车通过后栏杆抬起广播通知下一辆车unpark唤醒后继节点。但这里有个细节非公平模式下新来的车可以直接插队抢栏杆——如果刚好栏杆抬着新到的车可以直接冲过去不用管后面排了多长的队。公平模式下新来的车必须先看队列里有没有人在等有人等就老老实实排到队尾。翻译回技术语言收费站AQS栏杆状态抬起/放下volatile int state排队车道CLH 双向 FIFO 队列Node 双向链表车辆通过CAS 修改 state 成功排队等候LockSupport.park()阻塞线程通知下一辆车LockSupport.unpark()唤醒后继节点插队抢栏杆非公平锁的 barging 机制代码实现从 JDK 源码看 AQS ReentrantLock第一层AQS 的核心骨架以下代码截取自 OpenJDK AbstractQueuedSynchronizer.java是 Doug Lea 的原版实现publicabstractclassAbstractQueuedSynchronizerextendsAbstractOwnableSynchronizerimplementsjava.io.Serializable{privatevolatileintstate;protectedfinalintgetState(){returnstate;}protectedfinalvoidsetState(intnewState){statenewState;}protectedfinalbooleancompareAndSetState(intexpect,intupdate){returnunsafe.compareAndSwapInt(this,stateOffset,expect,update);}staticfinalclassNode{volatileNodeprev;volatileNodenext;volatileThreadthread;volatileintwaitStatus;}privatetransientvolatileNodehead;privatetransientvolatileNodetail;publicfinalvoidacquire(intarg){if(!tryAcquire(arg)acquireQueued(addWaiter(Node.EXCLUSIVE),arg))selfInterrupt();}publicfinalbooleanrelease(intarg){if(tryRelease(arg)){Nodehhead;if(h!nullh.waitStatus!0)unparkSuccessor(h);returntrue;}returnfalse;}protectedbooleantryAcquire(intarg){thrownewUnsupportedOperationException();}protectedbooleantryRelease(intarg){thrownewUnsupportedOperationException();}}逐段解释state是 AQS 的灵魂。对 ReentrantLock 来说state0表示无锁state1表示被锁定state1表示重入次数。对 CountDownLatch 来说state表示剩余计数。对 Semaphore 来说state表示剩余许可数。同一个字段不同语义——这就是模板方法模式的威力。acquire()是获取锁的入口。它的执行逻辑是先调tryAcquire()尝试抢锁子类实现抢不到就调addWaiter()入队然后调acquireQueued()在队列里自旋等待。整个流程就是抢 → 排队 → 等 → 被唤醒 → 再抢。tryAcquire()和tryRelease()是留给子类的钩子。AQS 不关心什么算拿到锁它只负责排队和阻塞。子类决定抢锁逻辑。第二层ReentrantLock 的公平与非公平以下代码截取自 OpenJDK ReentrantLock.javapublicclassReentrantLockimplementsLock,java.io.Serializable{privatefinalSyncsync;abstractstaticclassSyncextendsAbstractQueuedSynchronizer{abstractbooleaninitialTryLock();finalbooleantryLock(){ThreadcurrentThread.currentThread();intcgetState();if(c0){if(compareAndSetState(0,1)){setExclusiveOwnerThread(current);returntrue;}}elseif(getExclusiveOwnerThread()current){if(c0)thrownewError(Maximum lock count exceeded);setState(c);returntrue;}returnfalse;}protectedfinalbooleantryRelease(intreleases){intcgetState()-releases;if(getExclusiveOwnerThread()!Thread.currentThread())thrownewIllegalMonitorStateException();booleanfree(c0);if(free)setExclusiveOwnerThread(null);setState(c);returnfree;}}staticfinalclassNonfairSyncextendsSync{finalbooleaninitialTryLock(){ThreadcurrentThread.currentThread();if(compareAndSetState(0,1)){// 直接抢不看队列setExclusiveOwnerThread(current);returntrue;}elseif(getExclusiveOwnerThread()current){intcgetState()1;if(c0)thrownewError(Maximum lock count exceeded);setState(c);returntrue;}returnfalse;}}staticfinalclassFairSyncextendsSync{finalbooleaninitialTryLock(){ThreadcurrentThread.currentThread();intcgetState();if(c0){if(!hasQueuedThreads()compareAndSetState(0,1)){// 先看队列有没有人setExclusiveOwnerThread(current);returntrue;}}elseif(getExclusiveOwnerThread()current){intc2getState()1;if(c20)thrownewError(Maximum lock count exceeded);setState(c2);returntrue;}returnfalse;}}publicReentrantLock(){syncnewNonfairSync();}publicReentrantLock(booleanfair){syncfair?newFairSync():newNonfairSync();}publicvoidlock(){sync.lock();}publicvoidunlock(){sync.release(1);}}逐段解释公平和非公平的区别就一行代码。NonfairSync.initialTryLock()直接compareAndSetState(0, 1)抢锁不管队列里有没有人在等。FairSync.initialTryLock()多了一个!hasQueuedThreads()的判断——队列里有人对不起请排队。重入的实现靠getExclusiveOwnerThread() current。如果当前线程已经持有锁state直接 1不需要再 CAS。释放时state - 1减到 0 才真正释放锁。这就是可重入的全部秘密。默认构造器创建的是非公平锁。new ReentrantLock()等价于new ReentrantLock(false)。为什么默认非公平因为吞吐量差距巨大——非公平锁允许新线程插队抢锁减少了线程上下文切换的开销。第三层真实项目中的 AQS 子类AQS 不只是 ReentrantLock 的底层。你日常用的这些类全都是 AQS 的子类类AQS state 语义模式ReentrantLock0无锁1重入次数独占ReentrantReadWriteLock高16位读锁持有数低16位写锁重入数共享独占CountDownLatch剩余计数countDown 减到 0 放行共享Semaphore剩余许可数共享来看一个真实项目中的例子。以下代码来自 AI-Meeting 项目 的UniversalAiChatHandler.javaimportjava.util.concurrent.CountDownLatch;publicvoidstreamToSink(AiPropertiesDOaiProperties,StringuserMessage,ListAiMessageHistoryRespDTOhistoryMessages,FluxSinkStringsink,AIContentAccumulatoraccumulator)throwsException{ChatClientchatClientcreateChatClient(aiProperties);ListMessagemessagesbuildMessages(aiProperties,userMessage,historyMessages);CountDownLatchlatchnewCountDownLatch(1);finalThrowable[]streamErrornewThrowable[1];chatClient.prompt().messages(messages).stream().chatResponse().subscribe(response-{/* 处理流式响应 */},error-{streamError[0]error;latch.countDown();},()-{latch.countDown();});latch.await(60,TimeUnit.SECONDS);}这里CountDownLatch(1)的 state 初始值就是 1。流式响应完成或出错时调countDown()state 减到 0AQS 释放所有等待线程。await()内部调的是 AQS 的acquireSharedInterruptibly()。你可能没写过 AQS 的子类但你每天都在用 AQS。边界与陷阱AQS 的三个大坑看起来很优雅对吧但 AQS 的坑比你想的深。陷阱一tryAcquire()实现不当导致死锁。AQS 的acquire()方法在tryAcquire()返回 false 后才会入队等待。如果你的tryAcquire()实现有 bug——比如永远返回 false或者条件判断写反了——线程会永远阻塞在队列里。后果死锁且线程堆栈显示在LockSupport.park()上你根本看不出是哪里出了问题。解法实现tryAcquire()时确保能拿到锁的路径一定存在。对于独占锁state 0时必须允许获取。陷阱二非公平锁的饥饿问题。非公平锁允许新线程插队在高并发场景下队列中的线程可能永远抢不到锁——因为总有新线程插队成功。后果某些线程长时间得不到执行响应时间 P99 飙升。解法如果业务对响应时间敏感用公平锁。虽然吞吐量低 10%-30%但保证了先来先服务。陷阱三state溢出。ReentrantLock 的state是 int 类型最大重入次数是Integer.MAX_VALUE2147483647。虽然正常代码不可能重入 21 亿次但如果你在循环里lock()忘了unlock()……后果c 0触发Error(Maximum lock count exceeded)。解法永远在 try-finally 里释放锁。这是铁律没有例外。高级考量从单机 AQS 到分布式锁AQS 解决的是单机内的线程同步问题。但当你从单机走向分布式AQS 的等待队列就不够用了——它只能阻塞本 JVM 内的线程跨进程的锁竞争它管不了。Redisson 的分布式锁就是 AQS 思想的分布式延伸。以下代码截取自 Redisson RedissonLock.javapublicclassRedissonLockextendsRedissonBaseLock{TRFutureTtryLockInnerAsync(longwaitTime,longleaseTime,TimeUnitunit,longthreadId,RedisStrictCommandTcommand){returnevalWriteSyncedNoRetryAsync(getRawName(),LongCodec.INSTANCE,command,if ((redis.call(exists, KEYS[1]) 0) or (redis.call(hexists, KEYS[1], ARGV[2]) 1)) then redis.call(hincrby, KEYS[1], ARGV[2], 1); redis.call(pexpire, KEYS[1], ARGV[1]); return nil; end; return redis.call(pttl, KEYS[1]);,Collections.singletonList(getRawName()),unit.toMillis(leaseTime),getLockName(threadId));}}注意看这段 Lua 脚本的逻辑exists检查锁是否存在 → 对应 AQS 的state 0hexists检查是否当前线程持有 → 对应 AQS 的getExclusiveOwnerThread() currenthincrby重入计数 1 → 对应 AQS 的setState(c 1)pexpire设置过期时间 →这是 AQS 没有的分布式锁的看门狗机制Redisson 把 AQS 的 state 从 JVM 内存搬到了 Redis Hash把 CLH 队列从 JVM 线程队列换成了 Redis Pub/Sub Semaphore。思想一样存储介质变了。但分布式锁引入了 AQS 不存在的问题锁过期看门狗续期、网络分区脑裂、Redis 主从切换锁丢失。这些是单机 AQS 永远不需要考虑的。对比表格特性synchronizedReentrantLock非公平ReentrantLock公平实现层面JVM 内置monitorenter 字节码AQS CAS CLH 队列AQS CAS CLH 队列可中断不可lockInterruptibly()可lockInterruptibly()可超时获取不可tryLock(timeout)可tryLock(timeout)可公平性非公平非公平默认公平!hasQueuedThreads()条件变量一个wait/notify多个Condition多个Condition可重入是是state 递增是state 递增吞吐量中高低比非公平低 10%-30%适用场景简单同步、代码块级需要超时/中断/多条件严格先来先服务一句话简单同步用 synchronized需要高级特性用 ReentrantLock对公平性有要求用公平锁。面试追问面试追问 1AQS 的 CLH 队列入队操作为什么不是原子的addWaiter()里先 CAS 设置 tail再设置 prev.next中间如果挂了怎么办→ 回答方向入队分两步——CAS 设置 tail 指向新节点然后pred.next node设置前驱的 next。第二步不是原子的但即使失败了其他线程可以从 tail 向前遍历 prev 链找到所有节点。AQS 的unparkSuccessor()在找不到 next 时就是从 tail 反向遍历的。面试追问 2ReentrantLock 的tryLock()不遵守公平性设置为什么这是 bug 吗→ 回答方向不是 bug是有意设计。JDK 文档明确写了tryLock()会插队——只要锁空闲就立即获取不管队列里有没有线程在等。理由是tryLock()通常用于避免死锁的试探性获取如果强制排队反而可能导致活锁。如果需要遵守公平性用tryLock(0, TimeUnit.SECONDS)。面试追问 3AQS 的 state 为什么是 int 而不是 long如果需要 64 位的 state 怎么办→ 回答方向Doug Lea 在设计时选择了 int因为 CAS 操作在 32 位和 64 位 JVM 上都对 int 有原生支持且对于锁计数、信号量许可数等场景int 的范围足够。如果需要 64 位 state可以参考java.util.concurrent.locks.StampedLock的实现——它用额外的 long 字段配合 Unsafe 的 CAS 操作。面试追问 4CountDownLatch 和 CyclicBarrier 都能实现等待多个线程完成它们在 AQS 层面的本质区别是什么→ 回答方向CountDownLatch 基于 AQS 的共享模式state是计数器只能减不能增一次性使用。CyclicBarrier 不基于 AQS它内部用ReentrantLock Condition实现循环等待可以重复使用。本质区别CountDownLatch 是一个线程等 N 个事件CyclicBarrier 是N 个线程互相等。总结AQS 不是锁是造锁的模具——state 是锁芯CLH 队列是锁体tryAcquire/tryRelease 是钥匙孔的形状。读完这篇你应该能画出 AQS 的 acquire/release 完整流程图、解释 ReentrantLock 公平锁和非公平锁的那一行代码差异、说出 CountDownLatch 和 Semaphore 在 AQS 层面的 state 语义区别、理解 Redisson 分布式锁是对 AQS 思想的分布式延伸。下次面试官问AQS 是什么别只说一个队列——告诉他AQS 是 Doug Lea 用一个 volatile int 和一个双向链表造出了整个 JUC 锁生态的基石。
http://www.zskr.cn/news/1384382.html

相关文章:

  • 2026台式机电脑代工公司排行:选型核心维度全解析 - 奔跑123
  • CausalVLR基准测试报告:在IU X-Ray和MIMIC-CXR数据集上的性能分析
  • UniShopX:PHP版京东/天猫级电商系统完整解决方案
  • 告别SVN恐惧症:美术策划也能轻松上手的Unity PlasticSCM极简入门(附团队项目拉取实战)
  • 基于ATtiny85与干簧管的低功耗智能门状态指示器设计与实现
  • 基于ESP32与RFID的离线密码保险箱:硬件级双因子认证实践
  • 如何彻底解决Windows键盘误触问题:SharpKeys的终极配置指南
  • 还在用Excel排产?制造业车间调度的坑我替你们踩过了,APS如何更优解?
  • <背包问题>
  • 如何破解目标悬空,打通战略执行闭环?论“企业计划”的解法
  • 模糊效果失控?立即执行这4个CLI级修复指令——基于1728组生成日志的故障归因模型
  • 【仅限首批内测用户开放】Sora 2 v2.3.1隐藏API:绕过默认MP4封装器,直出ProRes 422 HQ+MP4双轨包(含Python SDK调用示例)
  • react-native-easy-toast核心API解析:掌握show与close方法的高级用法
  • 13-3 节点流(或文件流)
  • ArcGIS Pro自定义工具箱打包与调用全攻略:从.tbx制作到在Add-in中集成
  • Rocky Linux 9 配置IP后不生效?别只重启NetworkManager,试试这个nmcli组合命令
  • AI+行业场景落地实践指南(2026)
  • OpenKore:Ragnarok Online自动化解决方案的完整技术指南
  • CVAT属性注释模式保姆级教程:用键盘快捷键把标注效率提升3倍
  • 树莓派蓝牙终端实战:用平板打造无线命令行工作站
  • 大数据开发薪资翻倍?2026年大模型应用开发速成指南!本科即可转岗高薪赛道
  • 武汉国电华美串联谐振试验装置,现场用着心里有底
  • OmenSuperHub:释放惠普游戏本性能的纯净开源控制中心
  • 如何快速上手DeepPurpose?5分钟完成你的第一个药物-靶点相互作用预测模型
  • 上海开眼 GEO优化:以十八年搜索技术沉淀,构建 AI 时代企业增长新引擎
  • VtestStudio测试报告生成详解:如何用CAPL的TestStepPass/Fail写出清晰可读的报告
  • 5分钟上手Zotero Attanger:从源路径选择到自定义重命名全攻略
  • Hindsight语义链接创建:如何构建高质量的知识图谱
  • twbs-pagination核心配置详解:从入门到精通的10个关键参数
  • Vibe Coding 使用指南