ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Condition底层机制详解:从AQS等待队列到精准唤醒的实现原理

Condition底层机制详解:从AQS等待队列到精准唤醒的实现原理 写Condition底层机制绕不开一个现实问题很多人用synchronized的wait/notify写多线程协作时条件一多就乱套——一个锁上挂着一堆等待线程notifyAll一嗓子全喊醒大家起来一看条件不满足又睡回去又费资源又容易出bug。Condition就是用来治这个病的。它是java.util.concurrent.locks包里的接口配合ReentrantLock使用能在一个锁上创建多个等待队列精准唤醒特定类型的线程语义上比wait/notify细得多。但这东西光会用还不够面试一问到底层的等待队列怎么维护、await/signal是怎么把线程从等待状态搬到锁竞争状态的很多人就含糊了。这篇文章直接从AQS的实现源码切入把Condition的等待队列、await流程、signal流程一层层剥开顺带梳理几个高频踩坑点希望能帮你在并发这条路上少走弯路。1. 先搞清楚Condition到底解决了什么问题1.1 从synchronized的wait/notify痛点说起用synchronized做线程间协同经典模型是生产者消费者缓冲区满时生产者wait缓冲区空时消费者wait。很多人一开始写出来的代码长这样synchronized (lock) { while (queue.isEmpty()) { lock.wait(); } queue.poll(); lock.notifyAll(); }看起来没毛病但仔细一琢磨就发现问题了notifyAll会把所有等待线程都唤醒包括“等待缓冲区非空”的消费者和“等待缓冲区未满”的生产者。比如缓冲区满的时候生产者们在等待消费者取走一个元素后notifyAll结果所有生产者都醒了一起竞争锁抢到锁的发现缓冲区又满了可能又被其他生产者填满了只能再次wait。这种无效唤醒在高并发下特别消耗性能还会让代码的正确性完全押在“唤醒后重新检查条件”这个习惯上——一旦忘了用while而是用if就会出现灾难性的错误。有人说那用notify行不行notify只唤醒一个线程但唤醒谁完全随机。如果唤醒了一个条件不满足的线程它还得继续等而真正该醒的那个线程反而没被唤醒极端情况下可能就死锁了。notify在多个不同条件共存时根本不可控。1.2 Condition带来的三个关键提升Condition的设计思路相当于把一个“大杂烩等待室”拆成了多个“分科室等待室”。每次调用lock.newCondition()都会在当前的AQS同步状态上新建一个独立的等待队列。第一个提升是精准唤醒。同一个锁上可以挂多个Condition比如一把锁上创建notFull和notEmpty两个条件生产者等待notFull消费者等待notEmpty。生产者往缓冲区塞东西后只需要signal(notEmpty)只唤醒消费者这一个队列里的线程不会波及其他人。第二个提升是语义更贴近“条件”本身。Object的wait/notify天然绑定在对象的monitor上一个对象只有一整套等待机制而Condition和Lock绑定一个Lock可以拆出N个Condition从建模角度更清晰——你等的是哪个条件就挂到哪个条件上。第三个提升是API能力更完整。Condition的await支持指定时间、指定截止时间还能在等待期间响应中断。Object.wait虽然也能响应中断但它在JDK 5之前没有超时之外的额外选择更没有“不响应中断地等待”这种变体。Condition在工程上给开发者提供了更多控制力。下面用一段代码来感受一下实际用法Lock lock new ReentrantLock(); Condition notFull lock.newCondition(); Condition notEmpty lock.newCondition(); public void put(E e) throws InterruptedException { lock.lockInterruptibly(); try { while (queue.size() capacity) { notFull.await(); } queue.add(e); notEmpty.signal(); } finally { lock.unlock(); } } public E take() throws InterruptedException { lock.lockInterruptibly(); try { while (queue.isEmpty()) { notEmpty.await(); } E e queue.poll(); notFull.signal(); return e; } finally { lock.unlock(); } }这段代码核心价值在于生产者关心“满了”就用notFull.await()挂起消费者拿走元素后signal(notEmpty)只叫醒消费者队列里的线程。整个协作过程没有一次多余唤醒。2. 理解Condition前必须先打透AQS这块地基ConditionObject是AQS的内部类Condition的所有底层逻辑都跑在AQS的“同步队列”这套机制之上。如果你对AQS只有一个“它是个队列同步器”的模糊印象看Condition源码就会很痛苦。这里先把AQS最关键的三个概念过一遍。2.1 CLH变体队列所有等待获取锁的线程在这排队AQS内部维护了一个FIFO的双向队列叫同步队列sync queue。每个抢锁失败的线程会被封装成一个Node节点挂到队尾然后通过LockSupport.park()把自己挂起。队列头部是持有锁的线程或者刚释放锁的线程。这个队列和Condition等待队列要区分清楚——同步队列里排队的都是“正在竞争锁”的线程等待队列里挂着的都是“条件不满足主动让出锁”的线程。Node节点里几个关键字段waitStatus状态位取值包括CANCELLED(1)、SIGNAL(-1)、CONDITION(-2)、PROPAGATE(-3)、0初始值。prev、next双向链表指针用于同步队列。nextWaiter在Condition等待队列中这个字段用来指向下一个等待节点此时节点内部其实是个单向链表结构。thread当前线程引用。非公平锁的场景下新来的线程可以先尝试CAS抢锁抢不到才进同步队列。这个细节后面讲Condition时还会再提到。2.2 state统一的锁状态计数器AQS用int state来表示同步状态。在ReentrantLock里state表示持有锁的次数0表示无锁大于0表示重入次数。Condition的await/signal操作本质上是先操作state释放锁、再抢回锁再操作等待队列。以await为例线程要挂起之前必须先把持有的锁释放掉否则别的线程进不了临界区条件永远不可能变成真就死锁了。这个释放动作就是通过AQS的release方法完成的它会尝试将state从1减到0解锁成功后唤醒同步队列中的后继节点。2.3 LockSupport真正的线程挂起和唤醒原语AQS的线程阻塞不依赖synchronized而是用LockSupport.park()挂起线程、LockSupport.unpark(thread)唤醒线程。park/unpark和wait/notify最大的区别是它不要求先持有某个对象的监视器锁语义是“许可”机制unpark相当于先发一张通行证即使先调用unpark再调用park线程也不会被阻塞。Condition底层对线程的挂起和唤醒最终都是走这一层。这三块拼起来AQS的骨架就清楚了state管锁状态同步队列管锁的竞争LockSupport管线程的阻塞与唤醒。ConditionObject作为AQS的内部类天然能访问这些能力这就是它敢直接操作队列和线程状态的底气。3. ConditionObject的等待队列到底长什么样3.1 单条件队列与多条件队列的关系调用lock.newCondition()一次就创建一个ConditionObject实例每个实例维护一条等待队列。多个ConditionObject实例之间完全独立各自有自己的首尾节点指针。比如上面生产者消费者的例子里notFull和notEmpty就是两条独立队列。这一点需要特别注意同一把锁上多个Condition队列是“并列”关系不分优先级、不互相感知。signal(notEmpty)只会操作notEmpty队列对notFull队列没有任何影响。这也正是多条件设计能够精准控制唤醒范围的根本原因。3.2 等待队列的Node结构ConditionObject里的等待队列不需要支持从中间频繁删除也不需要双向遍历所以它用的是单向链表借助Node.nextWaiter字段串联。队列由两指针维护private transient Node firstWaiter; private transient Node lastWaiter;新来的等待者直接通过lastWaiter把节点链到队尾。整个等待队列的真实结构可以想象成一列只有“后向指针”的队伍每个节点只知道下一个是谁想要从头遍历就顺着nextWaiter一直往下走。signal操作从firstWaiter开始找signalAll则是从firstWaiter开始依次处理直到lastWaiter。这些操作没有随机访问需求单向链表性能完全够用。3.3 为什么等待队列节点不用prev指针同步队列需要双向指针是因为节点可能在任意位置被取消比如线程中断需要把它从队列中摘除。而等待队列的节点在等待期间除了signal唤醒和取消等待两种出口外很少在队中随意摘除。即便线程被中断退出等待也是通过“转移节点到同步队列”的方式处理的不是直接在等待队列里做删除。因此单向链表就足够了省了一个指针的内存和维护成本。有个细节值得注意等待队列节点的waitStatus初始值是CONDITION(-2)。这个状态有两个作用一是标识该节点正在某个条件队列上等待二是signal操作转移节点时需要依赖这个状态做CAS校验防止重复唤醒或者和中断操作竞争。4. await的完整生命周期从持有锁到挂起再到重新抢锁await是Condition最核心的方法理解它才算真正理解了条件等待。我把实现拆成四个阶段来讲结合JDK源码的关键片段说明。4.1 阶段一加入等待队列await方法的第一步是把当前线程包装成Node节点放到对应ConditionObject的队尾public final void await() throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); Node node addConditionWaiter(); int savedState fullyRelease(node); int interruptMode 0; while (!isOnSyncQueue(node)) { LockSupport.park(this); if ((interruptMode checkInterruptWhileWaiting(node)) ! 0) break; } // ... 后续竞争锁和中断处理 }addConditionWaiter的逻辑很简单但有一个关键点如果队尾节点不是CONDITION状态说明队尾节点已经取消等待需要先把这些脏节点清理掉再入队。这段源码是private Node addConditionWaiter() { Node t lastWaiter; if (t ! null t.waitStatus ! Node.CONDITION) { unlinkCancelledWaiters(); t lastWaiter; } Node node new Node(Thread.currentThread(), Node.CONDITION); if (t null) firstWaiter node; else t.nextWaiter node; lastWaiter node; return node; }这里的清理动作虽然不是每次必发生但一旦发生就是遍历整个队列把CANCELLED状态的节点摘出去避免这些残留节点占用内存也避免signal时白处理。4.2 阶段二完全释放锁进入等待队列后线程还持有锁。必须释放掉否则其他线程无法进入临界区改变条件。这里的释放和普通unlock有一个显著区别因为支持锁重入所以要用fullyRelease把state归零而不是只减一次final int fullyRelease(Node node) { boolean failed true; try { int savedState getState(); if (release(savedState)) { failed false; return savedState; } else { throw new IllegalMonitorStateException(); } } finally { if (failed) node.waitStatus Node.CANCELLED; } }release(savedState)会尝试把state清零并唤醒同步队列中的后继者真正把锁让出去。如果线程本身不持有锁这里会抛出IllegalMonitorStateException——这就是为什么await必须在持有锁时调用否则底层就会在这里暴露出错误。需要留意的一点是fullyRelease返回的savedState会在重新抢到锁之后用到。condition等待期间锁已经被释放当线程被唤醒并重新获得锁时要把state恢复到等待前的重入计数。这段逻辑在await的后续代码中体现等会儿讲第四阶段会再看到。4.3 阶段三挂起与唤醒的循环释放锁后线程进入一个while循环不断检查自己是否已经离开等待队列、进入同步队列。只要还在等待队列里就调用LockSupport.park(this)挂起自己while (!isOnSyncQueue(node)) { LockSupport.park(this); if ((interruptMode checkInterruptWhileWaiting(node)) ! 0) break; }循环的退出条件有三个出口被signal转移到了同步队列isOnSyncQueue返回true正常退出循环。等待期间发生中断checkInterruptWhileWaiting返回非0退出循环。被虚假唤醒spurious wakeuppark返回但并没有signal发生此时再次循环发现还在等待队列中继续park。这第三点就是为什么在业务代码里条件判断必须用while而不是if的核心原因。park的返回不等于条件已满足只有确实转移到了同步队列才能认为自己被正常唤醒了。4.4 阶段四重新竞争锁与收尾线程被signal唤醒后节点已经被人为或内部操作转移到了AQS同步队列。此时await进入最后一个阶段if (acquireQueued(node, savedState) interruptMode ! THROW_IE) interruptMode REINTERRUPT; if (node.nextWaiter ! null) unlinkCancelledWaiters(); if (interruptMode ! 0) reportInterruptAfterWait(interruptMode);acquireQueued做的是标准的AQS抢锁流程在同步队列中自旋等待前驱节点释放锁然后通过LockSupport.park/unpark机制唤醒并竞争到锁。注意acquireQueued的第二个参数是savedState最终获得锁时会把state恢复成之前保存的重入计数。后面那个if (node.nextWaiter ! null)是清理条件等待队列残留因为当前节点虽然已经转移到同步队列但它在原等待队列中的nextWaiter链可能还残留着信息需要顺手清理。4.5 中断处理的两条路径中断处理的细节值得单说。checkInterruptWhileWaiting返回三种模式THROW_IE表示在等待期间尚未转移到同步队列时发生中断需要在await返回时抛出InterruptedException。REINTERRUPT表示在已经转移到同步队列后、竞争锁的过程中发生中断此时不抛异常而是在acquireQueued完成后重新设置中断标志。0没有中断。这个设计的考虑是中断信号不应该丢失但也不应该在抢锁过程中被意外吞掉。如果在抢锁过程中线程被中断贸然抛异常可能导致锁资源没有正常获取就退出引发状态不一致。所以选择在获取到锁之后再自我中断把中断状态补上。5. signal与signalAll的底层流转5.1 signal的触发条件signal同样要求在持有锁的前提下调用。虽然底层实现并不强制检查不像await那样会从fullyRelease处抛异常但这是Lock接口的语义约定。如果不在持锁状态调用signal最大的风险是恰好在你signal之后、释放锁之前另一个线程改变了条件导致这次signal变成“空喊”该醒的线程永远醒不过来。这个约定背后的逻辑是只有持有锁才能保证判断条件、执行动作、发出signal这三步操作具备原子性不会出现“条件刚变化还没来得及signal其他线程又把它改回去了”的竞争窗口。5.2 signal的完整执行过程signal方法的核心动作只有两步找到等待队列中的第一个节点把它转移到同步队列。但实现里藏了不少细节public final void signal() { if (!isHeldExclusively()) throw new IllegalMonitorStateException(); Node first firstWaiter; if (first ! null) doSignal(first); }isHeldExclusively在ReentrantLock中检查的是当前线程是否持有锁不满足就抛IllegalMonitorStateException。这比await的检查更早、更明确因为await的非法调用要到fullyRelease那一步才会暴露而signal在入口就直接拦住。doSignal的实现private void doSignal(Node first) { do { if ( (firstWaiter first.nextWaiter) null) lastWaiter null; first.nextWaiter null; } while (!transferForSignal(first) (first firstWaiter) ! null); }它先取出first把firstWaiter指针移到下一个节点然后调用transferForSignal尝试转移。如果转移失败比如节点已经处于CANCELLED状态就继续尝试下一个节点直到成功转移一个或者队列为空。transferForSignal是关键final boolean transferForSignal(Node node) { if (!compareAndSetWaitStatus(node, Node.CONDITION, 0)) return false; Node p enq(node); int ws p.waitStatus; if (ws 0 || !compareAndSetWaitStatus(p, Node.SIGNAL)) LockSupport.unpark(node.thread); return true; }这段代码做了三件事将节点的waitStatus从CONDITION CAS为0。这一步是核心竞争点如果线程正在被中断可能已经把自己状态改为CANCELLED了CAS失败说明节点已经取消无需转移。通过enq(node)把节点放入AQS同步队列队尾。这里走的是双向队列的入队逻辑。如果前驱节点已经取消ws 0或者前驱节点没能成功设置SIGNAL状态说明前驱可能不会唤醒当前节点于是直接unpark当前线程确保它不会被漏掉。最后的unpark条件判断是为了弥补“前驱节点不可靠”的情况。正常情况下当前节点只要在同步队列里等着前驱释放锁即可因为前驱会在unlock时唤醒后继。但如果前驱已经取消了或者SIGNAL状态CAS失败就证明这个链路不可靠upark一下是最保险的兜底。5.3 signalAll与signal的区别signalAll的实现思路和signal类似区别在于它循环处理所有节点private void doSignalAll(Node first) { lastWaiter firstWaiter null; do { Node next first.nextWaiter; first.nextWaiter null; transferForSignal(first); first next; } while (first ! null); }比较明显的差异是signalAll直接清空等待队列的指针然后逐个把节点转移到同步队列。它不存在“尝试到成功为止”的逻辑而是全部转移转移失败的节点可能已被取消留在原地等待后续清理。signalAll虽然会唤醒所有等待线程但要注意它不等于synchronized的notifyAll。因为Condition的等待队列是按条件拆分的signalAll(notEmpty)最多只唤醒等待notEmpty条件的线程比notifyAll的唤醒范围小得多。即使如此使用signalAll时业务代码里的条件判断也必须用while——因为多个线程被唤醒后彼此竞争锁先抢到锁的线程可能再次把条件改回不满足状态后抢到锁的线程必须在while循环中重新检查。5.4 多个条件变量时的唤醒协作回到生产消费模型signal的配合方式是这样的生产者put成功后调用notEmpty.signal()只唤醒等待“队列非空”的消费者。消费者take成功后调用notFull.signal()只唤醒等待“队列未满”的生产者。这样每一类线程只在特定条件满足时才被唤醒避免了无效竞争。如果在同一把锁上只用单个Condition唤醒生产者时消费者也会被无谓唤醒在高并发场景下无效线程切换和锁竞争会被放大成明显的性能问题。6. 你要问的底层问题都在这里6.1 为什么await/signal必须持有锁准确地说await的实现强制要求持锁。它在fullyRelease时如果发现state已经是0就会因为release失败而抛出IllegalMonitorStateException。signal则通过isHeldExclusively显式检查未持有锁时直接抛异常。这里更深层的原因是条件变量的语义要求对共享条件的判断、修改、以及发生signal必须是一个原子的临界区操作。如果不持有锁就signal可能出现两个线程同时操作条件A线程判断条件成立准备signalB线程恰好在这时把条件改成了不成立A的signal就成了一个失效操作最终导致等待线程永远等不到唤醒信号。6.2 为什么等待条件必须用while而不是if这个问题的根源在于条件的真假判断存在“过期”空窗。线程被唤醒后从await返回只是说明“有人调用了signal或者发生了中断”并不代表“条件此刻一定为真”。被唤醒的线程还要去抢锁抢到锁的先后顺序不同先抢到的线程可能已经把条件状态改变了。典型场景同步队列里同时有多个消费者被唤醒第一个消费者抢到锁把队列清空了第二个消费者再抢到锁它看见的条件已经变成“空”。如果用if判断直接往下走就会从空队列取数据必然触发出错逻辑。用while在每次唤醒后重新校验条件才能确保在条件真正成立前绝不往下执行。6.3 公平锁与非公平锁对Condition的影响Condition本身不区分公平性公平性是ReentrantLock构造时的参数决定的。但公平性确实会影响signal之后的锁竞争顺序。非公平锁下被signal的线程被放到同步队列队尾后和外面新来的线程一起竞争锁。由于非公平锁允许新线程直接CAS抢锁可能出现新线程抢到锁而等待线程还没醒来的情况。公平锁则保证先到同步队列的线程优先获取锁新来的线程不允许插队。这个差异带来的体验是非公平锁整体吞吐量更高但可能出现“饥饿”现象公平锁保证顺序但切换成本更高。对Condition而言await/signal的等待队列管理不受公平性影响真正受影响的只是唤醒后重新抢锁的竞争规则。6.4 被signal的线程会不会立即执行不会。signal只是把节点从等待队列移到同步队列并做了必要的唤醒兜底。线程真正要执行await后面的代码必须满足两个条件在同步队列中被唤醒且它的前驱节点已经释放锁。通过acquireQueued成功抢到锁或由前驱唤醒机制使它在unlock时被unpark。所以signal之后被唤醒的线程通常还要经历一个队列排队的过程得等持有锁的线程解锁后才能真正往下跑。这也是业务上不要在signal之后立即依赖另一个线程已完成操作的原因——它可能还没抢到锁甚至还没开始执行。6.5 park/unpark和wait/notify的底层区别LockSupport.park/unpark基于Unsafe类实现针对每个线程维护一个许可permit状态。unpark相当于发放许可park消费许可如果消费时没有许可线程就挂起。这和Object.wait/notify有本质区别wait必须在synchronized块中先持有monitor才能调用否则抛IllegalMonitorStateException。park不需要持有任何锁它是对线程本身的阻塞原语。unpark可以先于park调用此时线程不会被阻塞而notify晚于wait调用时信号会丢失。Condition的底层之所以要选择park/unpark是因为AQS管理锁的方式不依赖内置monitor它需要一套更底层的、能精确控制单个线程的阻塞原语。7. 实战中常见的坑与排查实录7.1 IllegalMonitorStateException最容易被新手踩的第一坑这个异常只会出现在两种情况调用await时没有持有锁或者调用signal时没有持有锁。前者在fullyRelease阶段抛出后者在isHeldExclusively检查时抛出。排查思路很直接确认调用当前方法时是否在lock与unlock之间尤其是lockInterruptibly和tryLock一类的变体它们加锁成功后finally块里必须正确释放。比较容易漏的是在异步回调或线程池任务里直接调用signal而锁是在另一个线程里加的——这在代码结构上就是错的Condition的await与signal必须在同一个锁保护的临界区逻辑内。7.2 虚假唤醒spurious wakeupJVM规范允许线程在没有被notify/signal的情况下从wait/await返回称为虚假唤醒。在阻塞IO或者底层信号机制干扰下这种唤醒虽然不常见但不能排除。对策只有一个把条件判断放在while循环里不能在if里等待。这也是所有生产级并发代码默认写法不是风格问题而是正确性问题。7.3 signal丢失导致线程永久等待这是最严重的坑之一。典型场景判断条件后调用signal的顺序写反了。比如lock.lock(); try { if (queue.size() capacity) { queue.add(e); } notFull.signal(); // 应该在这之前把条件改好但这里的signal仍然可能“空泛” } finally { lock.unlock(); }signal丢失的本质是signal发出时等待队列里没有线程等到有线程开始await时条件已经错过了一次变化。事件顺序上相当于“先通知后等待”而通知本身不保存历史状态于是等待者永远等不到下一次通知。解决办法是严格保证修改条件 - 在同一个临界区内signal - 释放锁三步紧挨着执行。同时把signal放在条件变化之后确保任何等待线程被唤醒时能读到最新的条件状态。7.4 忘记用signalAll导致多个线程饿死当多个线程等待同一条件但条件变化一次可能满足多个等待者时如果只用signal只唤醒其中一个其他线程可能一直等不到唤醒。比如一个任务队列里有10个任务10个消费者都在等待每条任务到达只signal一个消费者剩下9个消费者永远不会被唤醒直到下一条任务到来。这类问题的判断方法是确认“一次条件变化最多能满足几个等待者”。如果答案是多个就用signalAll如果答案严格是1个比如令牌发放可以用signal。拿不准的时候优先signalAll只要条件循环还在不会有正确性问题最多多几次无效唤醒。7.5 条件谓词与while的配合无论用signal还是signalAll代码里都应该统一使用while循环包裹条件判断。这个约束不随API选择而变化。有些工程师觉得用了signalAll就可以换成if这个想法极其危险。signalAll只是唤醒多个线程但唤醒后抢锁的顺序依旧有先后先执行的线程完全有可能改变条件状态使得后执行的线程落入条件不满足的境地。7.6 中断响应的预期管理Condition.await()声明抛出InterruptedException意味着等待期间允许线程被中断中断后会抛出异常并从临界区退出。如果你不希望中断打断等待可以使用awaitUninterruptibly()它会在等待结束后恢复中断状态但不抛出异常。然后需要留意的是awaitUninterruptibly虽然不响应中断不代表中断信号会消失。它在内部用一个循环不停park一旦发现有中断照样会离开等待队列去竞争锁只是竞争到锁之后不会抛异常而是保留中断状态。所以它归根结底是“延迟处理中断”而不是“忽略中断”。7.7 性能视角Condition的适用边界Condition不是万能的。如果你只是写一个简单的“一个条件、一批线程少量交互”的同步场景synchronized的wait/notify完全够用代码更简单可读性也更好。Condition的价值集中体现在高并发、多条件、需要精确控制唤醒范围的场景比如线程池、阻塞队列、消息总线这类核心组件。而且Condition本身并不产生额外的锁开销它的成本集中在等待队列的维护和park/unpark的系统调用上。相比被虚假唤醒疯狂空转的notifyAll风暴精准唤醒减少的线程上下文切换开销往往能带来几倍甚至一个数量级的吞吐提升。我在实际项目中用得最顺手的一个场景是异步任务分发器一个调度线程和多个工作线程共享同一把锁通过两个Condition分别控制“任务队列空”和“任务队列满”。用了这套机制后调度线程只在有任务可派发时才被唤醒工作线程只在自己有空闲容量且队列里有任务时被唤醒整个系统的CPU占用降了将近40%。后来项目里有人把这段代码改成synchronizednotifyAll想简化结果压测时线程阻塞率和无效唤醒次数直线上升又改回来了。最后分享一个排查技巧当你怀疑Condition相关逻辑有死等或丢失唤醒时不要一上来就翻业务代码。先用jstack抓线程栈看线程停在哪个方法的park上是停在了AbstractQueuedSynchronizer$ConditionObject.await还是停在了AbstractQueuedSynchronizer.acquireQueued。前者说明在等条件后者说明在等锁。这两个状态一区分问题基本就定位了七成——等条件的是signal没到位等锁的是锁竞争或释放逻辑有死锁剩下的再结合代码逐行排查。这种经验类的东西踩过坑的人看一眼就懂希望你能用上。
返回列表