Skip to content

LockSupport 原理与 park/unpark

问题

LockSupport.park()/unpark()Object.wait()/notify() 有什么区别?为什么说 LockSupport 是更底层的线程阻塞工具?在实际生产代码中,我们什么时候该用 LockSupport 而不是传统的 wait/notify?

分析

从 wait/notify 的痛点说起

Object.wait()Object.notify() 是 Java 最古老的线程协调方式,但它们有几个根深蒂固的缺陷:

  1. 必须在同步块内调用wait()/notify() 必须在 synchronized 代码块中调用,否则运行时抛 IllegalMonitorStateException。这意味着调用方必须持有锁,而锁的获取本身就有竞争。
  2. 先 notify 后 wait 永远丢失:如果线程 A 先调用了 notify(),线程 B 随后才执行 wait(),那么 B 会永远等下去。这个时序问题在复杂并发场景下极难调试。
  3. 无法精确唤醒notify() 随机唤醒一个等待线程,notifyAll() 唤醒所有,都没有办法精确唤醒指定线程。
  4. 虚假唤醒(Spurious Wakeup)wait() 在未收到 notify 的情况下也可能返回,所以官方文档要求 wait() 必须永远在循环中调用。

LockSupport 的出现就是为了解决这些问题。它是 JUC(java.util.concurrent)包的基石——AQS、ReentrantLock、ForkJoinPool 等核心组件都依赖于它。

LockSupport 的核心机制

LockSupport 每个线程关联一个许可(permit),本质上是一个二元信号量(0 或 1):

  • unpark(Thread t):设置线程 t 的 permit 为 1(如果已经是 1,则保持不变)
  • park():消费 permit,如果 permit 为 1 则直接返回并将 permit 置为 0;如果 permit 为 0 则阻塞线程

这个 permit 机制解决了 wait/notify 的时序问题:先调用 unpark() 再调用 park() 不会阻塞,因为 permit 已经被提前置为 1,park() 直接消费并返回。

注意:permit 不会被累计。连续调用两次 unpark() 再调用一次 park(),permit 仍是 1。第二次 unpark() 是空操作,不会把 permit 累加到 2。

底层实现

LockSupport.park()unpark() 最终调用 Unsafe.park()Unsafe.unpark() 这两个 native 方法。

Unsafe 层

sun.misc.Unsafe 中:

java
// 阻塞当前线程,isAbsolute=false 时 time 是纳秒超时
// isAbsolute=true 时 time 是绝对时间(毫秒)
public native void park(boolean isAbsolute, long time);
public native void unpark(Object thread);

JVM 层(HotSpot)

在 HotSpot 的 unsafe.cpp 中,Unsafe_ParkUnsafe_Unpark 调用 PlatformEventpark()unpark()

Linux 上(pthread 实现)

cpp
// hotspot/src/os/linux/vm/os_linux.cpp
void PlatformEvent::park() {
    int status = pthread_mutex_lock(_mutex);
    int counter = _counter;  // 相当于 permit
    _counter = 0;
    status = pthread_mutex_unlock(_mutex);
    
    if (counter > 0) return;  // permit 已存在,直接返回
    
    // 使用 pthread_cond_wait 阻塞
    status = pthread_mutex_lock(_mutex);
    while (_counter <= 0) {
        status = pthread_cond_wait(_cond, _mutex);
    }
    _counter = 0;
    status = pthread_mutex_unlock(_mutex);
}

void PlatformEvent::unpark() {
    int status = pthread_mutex_lock(_mutex);
    int s = _counter;
    _counter = 1;
    status = pthread_mutex_unlock(_mutex);
    if (s < 1) {
        // 确保唤醒等待的线程
        status = pthread_cond_signal(_cond);
    }
}

这个实现的关键细节:

  • _counter 就是 permit,park() 先检查 _counter,大于 0 就消耗掉直接返回
  • pthread_cond_wait 可能被虚假唤醒(spurious wakeup),所以外层有个 while 循环
  • unpark() 先设 _counter = 1,再发 pthread_cond_signal,保证信号不会丢失

与 wait/notify 的底层对比

特性LockSupportObject.wait/notify
底层原语PlatformEvent (pthread_cond + mutex)对象监视器 (ObjectMonitor)
需要持有对象锁
存储介质线程内 _counter 字段对象头中的 Monitor
精确唤醒unpark(Thread) 指定线程notify 随机/notifyAll 全部
信号丢失不会(permit 持久化)会(先 notify 后 wait 丢失)
中断行为不抛异常,默默返回抛 InterruptedException
调用代价更轻量(无锁竞争逻辑)较重(可能涉及锁升级)

AQS 中 LockSupport 的典型用法

AQS 中 acquireQueued 方法的核心循环:

java
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            // 关键:获取锁失败后 park 阻塞
            if (shouldParkAfterFailedAcquire(p, node))
                interrupted |= parkAndCheckInterrupt();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);  // 阻塞当前线程
    return Thread.interrupted();  // 清除中断标志并返回
}

这里有一个常被忽略的坑:parkAndCheckInterrupt() 返回后,interrupted 标志位已被清除。如果外部代码在 park() 被中断后直接返回而没有正确处理,线程可能无法感知到中断信号。这就是为什么 ReentrantLocklockInterruptibly()lock() 需要分开实现的根本原因——lock() 在中断后要重试继续等,lockInterruptibly() 在中断后要抛异常。

中断响应行为的底层差异

LockSupport.park() 对中断的响应是"默默返回"——它不会抛 InterruptedException,而是直接返回,同时清除中断标志位(Thread.interrupted() 会清除标志)。调用方必须手动检查中断状态。

这与 wait() 的行为不同:wait() 抛出 InterruptedException,调用方必须显式处理。

java
// LockSupport 的 park 中断后:线程继续执行,需要自己检查
LockSupport.park();
if (Thread.interrupted()) {
    // 手动处理中断
    System.out.println("被中断了,需要自行处理");
}

生产踩坑:我见过一个线上故障,入口是 ThreadPoolExecutorWorker 线程池。当线程池调用 shutdownNow() 时,对工作线程发了中断。但有一个 ForkJoinPool 的 ForkJoinWorkerThread 内部调用了 LockSupport.park(),中断后 park() 默默返回,但调用方没有检查 Thread.interrupted(),直接进入了下一轮循环再次 park()。结果线程池 shutdown 后该线程一直无法退出,导致应用无法正常关闭。排查时用 jstack 看到该线程处于 WAITING (parking) 状态且没有 expected termination 标志。

带超时的 park

LockSupport.parkNanos(long nanos)parkUntil(long deadline) 可以实现定时阻塞:

java
// 等 500ms
LockSupport.parkNanos(500_000_000L);

// 等直到某个绝对时间点
LockSupport.parkUntil(System.currentTimeMillis() + 500);

parkNanos 的底层实现也是通过 pthread_cond_timedwait 实现,区别在于:

  • parkNanos:相对时间,从调用时开始计时
  • parkUntil:绝对时间,到某个时间点就返回

注意parkNanos() 返回后不一定是超时了,也可能是被 unpark() 唤醒或因为中断返回。所以调用方需要判断条件是否满足,不能假设超时就表示条件已达成。

实际使用场景

  1. AQS 同步器框架:ReentrantLock、CountDownLatch、Semaphore 等底层都用 LockSupport 实现线程阻塞和唤醒。核心是 acquireQueued() 中的 parkAndCheckInterrupt()

  2. ForkJoinPool 工作窃取:工作线程在没有任务时调用 park() 等待,被窃取线程通过 unpark() 唤醒。ForkJoinPool.scan() 方法中,工作线程扫描自己的双端队列,如果没找到任务,就随机去偷别人的任务,都偷不到就 park()

  3. 自定义阻塞队列:当需要精确控制线程的阻塞/唤醒时机时,LockSupport 比 wait/notify 更灵活。例如实现一个支持多消费者、每个消费者可以独立被唤醒的阻塞队列。

  4. 延迟队列实现:配合 parkNanos 实现精确的定时唤醒,比如 ScheduledThreadPoolExecutorDelayedWorkQueue 底层就用了 parkNanos

  5. Phaser:这个同步器的 arriveAndAwaitAdvance() 内部也用 LockSupport(不是在循环中 wait/notify),因为 FJP 的线程需要精确唤醒。

完整对比表

维度LockSupportObject.wait/notifyCondition.await/signal
调用前提无限制必须持有 synchronized 锁必须持有 Lock
唤醒精度精确到线程随机/全部精确到线程(signal)
信号丢失否(permit 持久化)是(先 notify 后 wait 丢失)否(await 在队列中)
中断响应默默返回,不抛异常抛 InterruptedException抛 InterruptedException
虚假唤醒有(底层 pthread_cond_wait)
是否释放锁否(不涉及锁)是(释放对象锁)是(释放 Lock)
底层实现PlatformEvent (pthread_cond)ObjectMonitorAbstractQueuedSynchronizer.ConditionObject
典型使用AQS 框架、ForkJoinPool传统 synchronized 块ReentrantLock 配合
使用频率底层框架代码业务代码过渡方案

代码示例

1. LockSupport 实现生产者-消费者(精确唤醒版)

java
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.LockSupport;

public class LockSupportDemo {
    private static final Queue<String> queue = new LinkedList<>();
    private static final int CAPACITY = 5;
    
    private static volatile boolean running = true;
    private static volatile boolean producerBlocked = false;
    private static volatile boolean consumerBlocked = false;
    private static Thread producerThread;
    private static Thread consumerThread;
    
    public static void main(String[] args) throws InterruptedException {
        producerThread = new Thread(() -> {
            int seq = 0;
            while (running) {
                synchronized (queue) {
                    while (queue.size() == CAPACITY) {
                        producerBlocked = true;
                        // 释放锁后 park,避免死锁
                    }
                    // 出同步块后再 park,演示 LockSupport 不需要锁的特性
                }
                
                // 队列满时 park,不需要持有锁
                if (producerBlocked) {
                    LockSupport.park();
                    producerBlocked = false;
                }
                
                synchronized (queue) {
                    queue.offer("msg-" + seq++);
                    System.out.println("生产: msg-" + (seq-1) + " 队列大小: " + queue.size());
                    // 如果有消费者在等,精确唤醒
                    if (consumerBlocked) {
                        LockSupport.unpark(consumerThread);
                        consumerBlocked = false;
                    }
                }
                sleep(200);
            }
        });
        
        consumerThread = new Thread(() -> {
            while (running) {
                synchronized (queue) {
                    while (queue.isEmpty()) {
                        consumerBlocked = true;
                    }
                }
                
                if (consumerBlocked) {
                    LockSupport.park();
                    consumerBlocked = false;
                }
                
                synchronized (queue) {
                    String msg = queue.poll();
                    System.out.println("消费: " + msg + " 队列大小: " + queue.size());
                    if (producerBlocked) {
                        LockSupport.unpark(producerThread);
                        producerBlocked = false;
                    }
                }
                sleep(500);
            }
        });
        
        producerThread.start();
        consumerThread.start();
        
        Thread.sleep(5000);
        running = false;
        // 唤醒可能还在等待的线程
        LockSupport.unpark(producerThread);
        LockSupport.unpark(consumerThread);
        producerThread.join();
        consumerThread.join();
    }
    
    // 2. 演示 LockSupport 的 permit 机制
    public static void demonstratePermit() {
        Thread t = new Thread(() -> {
            System.out.println("线程启动,准备 park");
            LockSupport.park();
            System.out.println("park 返回,继续执行");
            
            // 再次 park - 因为 permit 已经被消费,这里会阻塞
            System.out.println("再次 park 会阻塞,但先让主线程 unpark");
            LockSupport.park();
            System.out.println("第二次 park 返回");
        });
        
        t.start();
        
        // 先 unpark,演示 permit 机制
        LockSupport.unpark(t);
        System.out.println("主线程提前 unpark");
        
        sleep(1000);
        LockSupport.unpark(t);
        System.out.println("主线程再次 unpark");
    }
    
    // 3. 演示 parkNanos 的超时等待
    public static void demonstrateParkNanos() {
        Thread t = new Thread(() -> {
            System.out.println("开始等待 500ms");
            long start = System.nanoTime();
            LockSupport.parkNanos(500_000_000L);
            long elapsed = System.nanoTime() - start;
            System.out.println("等待结束,实际经过 " + (elapsed / 1_000_000) + "ms");
        });
        t.start();
    }
    
    private static void sleep(long ms) {
        try {
            Thread.sleep(ms);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

运行结果分析

主线程提前 unpark
线程启动,准备 park
park 返回,继续执行
再次 park 会阻塞,但先让主线程 unpark
主线程再次 unpark
第二次 park 返回

注意第一行输出顺序:主线程先调 unpark(t),此时 t 还没执行到 park(),但 permit 已经被置为 1。当 t 执行 park() 时,发现 permit 为 1,直接消费并返回,不会阻塞。这就是 permit 机制解决时序问题的关键。

常见踩坑

1. park 返回后不一定是 unpark 了

park() 返回有三种可能:

  • unpark() 唤醒
  • 被中断(Thread.interrupt()
  • 虚假唤醒(spurious wakeup,但概率极低)

所以 park 的调用必须放在条件循环中:

java
// 错误的写法
while (!condition) {
    LockSupport.park();  // 如果被虚假唤醒,可能 condition 还没满足
}

// 正确的写法
while (!condition) {
    LockSupport.park();
}

2. park 调用前后的线程状态

park() 阻塞时,线程状态是 WAITING (parking)jstack 中能看到:

"pool-1-thread-1" #11 prio=5 os_prio=0 tid=0x00007f1234000000 nid=0x2a3b waiting on condition [0x00007f122fbfe000]
   java.lang.Thread.State: WAITING (parking)
        at sun.misc.Unsafe.park(Native Method)
        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:304)

"WAITING (parking)" 是 LockSupport 特有的状态,区别于 wait() 的 "WAITING (on object monitor)"。

3. park 和 unpark 的线程安全性

LockSupport.unpark(Thread t) 可以在任意线程调用,不要求当前线程持有任何锁。这是 wait/notify 做不到的——notify() 必须在 synchronized 块中调用。

但注意:unpark() 内部的 pthread_cond_signal 本身不是线程安全的,HotSpot 通过 _mutex 保护了 _counter 的读写。所以 unpark() 本身是线程安全的,但调用方仍需保证业务逻辑的一致性。

4. 不能用于替代 wait/notify 的全部场景

LockSupport 不释放任何锁。如果你在 synchronized 块内 park(),其他线程要想获取同一个锁,也会被阻塞。这个场景下应该用 wait() 而非 park()

总结

LockSupport 是 Java 并发编程中的底层基础设施,它比 wait/notify 更灵活、更精确、更安全。核心要点:

  1. permit 机制:每个线程关联一个二元许可,unpark 最多将 permit 置为 1,park 消费 permit。这从根本上解决了先 notify 后 wait 的时序丢失问题。
  2. 不需要同步块park() 不释放锁,也不要求调用方持有锁,使用门槛更低。
  3. 精确唤醒unpark(Thread t) 可以精确唤醒指定线程,不像 notify() 那样随机。
  4. 中断响应park() 对中断默默返回,调用方需自行检查中断状态。
  5. 生产实践:AQS、ForkJoinPool、ReentrantLock 等核心组件都依赖 LockSupport,理解它是理解 JUC 框架的钥匙。
  6. 底层差异:Linux 基于 pthread_cond_wait 实现,macOS 基于 Mach 线程原语,Windows 基于 Event 对象。了解这些差异有助于跨平台调试。

参考资料

  • 《Java 并发编程的艺术》第 5 章
  • JDK 源码:java.util.concurrent.locks.LockSupport
  • AQS 源码:AbstractQueuedSynchronizer.acquireQueued()
  • HotSpot 源码:src/hotspot/os/linux/os_linux.cpp 中的 PlatformEvent 实现

手撕 → 框架 → 生产化,一步步把 AI Agent 工程化搞透。