Java高并发底层原理(二十五)—— LockSupport 如何挂起和唤醒线程

发布时间:2026/7/16 13:00:23
Java高并发底层原理(二十五)—— LockSupport 如何挂起和唤醒线程 前面分析 AQS、ReentrantLock、线程等待队列时经常会看到一个底层工具LockSupport。它不是一把锁也不维护等待队列而是提供两个更底层的能力让当前线程暂停以及唤醒指定线程。理解LockSupport时先不要把它和完整的锁混在一起。完整的锁至少还需要状态判断、等待队列和唤醒策略LockSupport只负责其中最底层的一步线程如何停下以及如何被指定线程唤醒。1. park 和 unpark 操作的是线程自己的 permitLockSupport给每个线程关联了一个许可证可以先把它理解成一个最多为 1 的 permitpermit 0没有许可证park() 会阻塞 permit 1有许可证park() 会直接返回并消耗这个许可证所以LockSupport.park();含义不是“无条件挂起当前线程”而是当前线程尝试获取自己的 permit如果没有 permit就阻塞等待。对应地LockSupport.unpark(thread);含义也不是“强行唤醒线程”而是给指定线程发放一个 permit如果这个线程正在park()中阻塞就让它返回如果它还没有执行到park()这个 permit 会先保留下来。一个最小例子如下importjava.util.concurrent.locks.LockSupport;publicclassParkUnparkDemo{publicstaticvoidmain(String[]args)throwsInterruptedException{ThreadworkernewThread(()-{System.out.println(before park);LockSupport.park();System.out.println(after park);});worker.start();Thread.sleep(1000);LockSupport.unpark(worker);}}子线程执行到park()时如果还没有 permit就会阻塞。主线程一秒后调用unpark(worker)相当于给worker发放 permit子线程因此从park()返回继续向下执行。2. unpark 可以早于 park但 permit 不会累加LockSupport和wait/notify一个重要区别是unpark()可以先于park()发生。importjava.util.concurrent.locks.LockSupport;publicclassUnparkBeforeParkDemo{publicstaticvoidmain(String[]args){ThreadworkernewThread(()-{System.out.println(before park);LockSupport.park();System.out.println(after park);});worker.start();LockSupport.unpark(worker);}}即使主线程先执行了unpark(worker)子线程后执行park()时也可以直接通过因为 permit 已经提前发放给了这个线程。park()发现 permit 存在就消耗它并返回。但是 permit 不会累加。连续调用多次unpark()最多也只是把线程的 permit 设为 1LockSupport.unpark(worker);LockSupport.unpark(worker);LockSupport.unpark(worker);这三次调用不会给worker三个 permit。后续如果线程连续调用两次park()第一次可能直接通过第二次如果没有新的unpark()仍然会阻塞LockSupport.park();// 消耗已有 permit直接返回LockSupport.park();// 没有新的 permit可能阻塞所以LockSupport的 permit 更像一个 0/1 状态而不是一个可以累加的计数器。3. park 返回不代表条件已经满足park()只负责让线程暂停和恢复不负责判断业务条件是否满足。线程从park()返回可能有三种原因返回原因含义unpark(thread)其他线程给当前线程发放了 permitinterrupt()当前线程被中断伪唤醒极少数情况下线程可能无原因返回因此park()外面通常不能只写一次if判断if(!ready){LockSupport.park();}更稳妥的写法是使用while反复检查条件while(!ready){LockSupport.park();}这里的核心不是循环本身而是职责分离park()只负责阻塞当前线程ready这类共享状态才负责表达线程是否真的可以继续执行。例如importjava.util.concurrent.locks.LockSupport;publicclassConditionParkDemo{privatestaticvolatilebooleanreadyfalse;privatestaticThreadworker;publicstaticvoidmain(String[]args)throwsInterruptedException{workernewThread(()-{while(!ready){LockSupport.park();}System.out.println(condition satisfied);});worker.start();Thread.sleep(1000);readytrue;LockSupport.unpark(worker);}}这段代码里unpark(worker)只是让worker有机会从park()返回真正决定它能不能继续执行的是ready true。如果线程因为中断或伪唤醒提前返回while会再次检查条件条件不满足就继续等待。4. interrupt 也会让 park 返回LockSupport.park()遇到中断时会返回但它不会抛出InterruptedException也不会清除线程的中断标记。importjava.util.concurrent.locks.LockSupport;publicclassParkInterruptDemo{publicstaticvoidmain(String[]args)throwsInterruptedException{ThreadworkernewThread(()-{System.out.println(before park);LockSupport.park();System.out.println(after park);System.out.println(isInterrupted Thread.currentThread().isInterrupted());});worker.start();Thread.sleep(1000);worker.interrupt();}}这段代码正常情况下会输出before park after park isInterrupted true执行过程是worker线程先打印before park然后执行LockSupport.park()进入等待。主线程休眠 1 秒后调用worker.interrupt()中断会让正在park()的线程返回。由于park()不会抛出InterruptedException也不会清除中断标记所以线程继续向下执行打印after park最后通过Thread.currentThread().isInterrupted()读取到当前线程的中断标记结果为true。这里如果把最后一行改成System.out.println(interrupted Thread.interrupted());System.out.println(isInterrupted Thread.currentThread().isInterrupted());输出会变成before park after park interrupted true isInterrupted false原因是Thread.interrupted()会读取并清除当前线程的中断标记而isInterrupted()只读取不清除。这个差异也是后面理解“先记录中断再恢复中断标记”的基础。这里要区分两个方法方法作用对象是否清除中断标记Thread.currentThread().isInterrupted()当前线程否thread.isInterrupted()指定线程否Thread.interrupted()当前线程是Thread.interrupted()是静态方法它检查的是当前线程并且会清除当前线程的中断标记。这个细节在基于park()写等待逻辑时非常重要。5. 等待过程中被中断通常要记录并恢复中断标记如果一个等待逻辑的语义是“即使被中断也继续等待条件满足”那么它不能因为park()被中断返回就直接退出。但它也不能把中断信号彻底吞掉所以常见做法是先清除并记录最后再恢复。booleaninterruptedfalse;while(!ready){LockSupport.park(this);if(Thread.interrupted()){interruptedtrue;}}if(interrupted){Thread.currentThread().interrupt();}这段代码可以拆开理解。线程在park(this)中等待时如果其他线程调用了当前线程的interrupt()park()会返回。随后执行Thread.interrupted()如果当前线程确实被中断过这个方法会返回true同时把当前线程的中断标记清除为false。代码再用局部变量记录这件事interruptedtrue;为什么要先清除中断标记因为如果中断标记一直保持为true后面再次执行park()时可能会立即返回线程无法真正阻塞等待循环就可能变成忙等。等条件最终满足后再执行Thread.currentThread().interrupt();这一步是恢复中断标记。它表达的意思是虽然当前等待逻辑没有因为中断而退出但等待期间确实发生过中断不能把这个信号吞掉。恢复之后上层代码可以通过isInterrupted()感知到这件事再决定是退出、清理、返回还是继续执行。6. park(Object blocker) 用来记录阻塞原因除了无参park()源码中还经常能看到LockSupport.park(this);这里的this是 blocker表示当前线程是因为哪个对象而阻塞的。它不改变park/unpark的语义只是把阻塞原因记录下来方便排查线程问题。例如importjava.util.concurrent.locks.LockSupport;publicclassBlockerDemo{publicvoidwaitHere(){LockSupport.park(this);}}当线程阻塞在这里时使用线程 Dump 工具可能会看到类似信息worker-1 WAITING at java.util.concurrent.locks.LockSupport.park(...) - parking to wait for 0x0000000712345678 (a BlockerDemo)如果只写LockSupport.park()线程 Dump 只能看到线程停在park()如果写LockSupport.park(this)还能看到它是因为哪个对象进入等待。这个参数不是唤醒条件也不是锁本身的特殊逻辑只是排查信息。7. LockSupport 不维护等待队列LockSupport.unpark(thread)必须明确传入一个ThreadLockSupport.unpark(worker);这说明LockSupport不知道“有哪些线程在等待”也不会自动选择应该唤醒谁。它只会对指定线程发放 permit。因此如果有多个线程需要排队等待就必须由上层结构维护等待关系。例如 AQS 会维护同步队列记录哪些线程在等、谁排在前面、释放锁时该唤醒谁。LockSupport只在最终一步发挥作用对某个确定的线程执行unpark(thread)。可以把职责分成三层职责由谁负责条件是否满足上层状态例如锁状态、任务状态、业务标志哪些线程在等待上层队列例如 AQS 同步队列线程如何暂停和唤醒LockSupport.park/unpark所以不要把LockSupport理解成完整的锁。它只是构建锁、同步器、阻塞队列等工具时使用的底层线程阻塞原语。总结本章的因果链条可以从线程等待开始理解当一个线程暂时不能继续执行时需要一种方式让它停止占用 CPU于是有了park()当条件可能发生变化时又需要其他线程能够指定唤醒它于是有了unpark(thread)。为了避免唤醒先于等待而丢失信号LockSupport使用每个线程自己的 permit 保存一次唤醒机会为了避免线程被错误唤醒后直接继续执行等待逻辑又必须在park()外层反复检查条件。中断进一步说明了park()的边界它只负责让线程返回不负责决定线程是否退出等待也不负责清理中断语义。是否继续等待、是否恢复中断标记、是否把中断交给上层处理都属于调用方的设计。最终LockSupport解决的是“线程怎么停、怎么被指定线程叫醒”但它不解决“条件是否满足”也不解决“线程如何排队”。