Java线程阻塞/唤醒-LockSupport

350 阅读4分钟

LockSupport.park()unpark(Thread) 的底层原理涉及 JVM 与操作系统(OS)的线程调度机制,是 Java 并发编程中实现线程阻塞/唤醒的核心机制之一。下面详细讲解其底层实现原理。


🔍 一、核心概念:许可机制(Permit)

每个线程内部维护一个许可(permit),这是理解 park()unpark() 行为的关键:

操作许可变化线程行为
初始状态permit = 0不可运行
调用 unpark(Thread t)permit = 1(最多为1)如果线程正在 park,则被唤醒
调用 park()如果 permit > 0 → permit -1,立即返回;否则线程进入等待状态阻塞当前线程

⚠️ 注意:多次调用 unpark() 同一线程不会叠加许可,只会保留一个(即 permit 最多为 1)。


🧠 二、底层实现原理详解

1. JVM 内部实现机制(以 HotSpot 为例)

  • LockSupport 是通过 JVM 提供的 native 方法 实现的:
    • private static native void park(boolean isAbsolute, long time);
    • private static native void unpark0(Thread thread);

这些方法最终会调用操作系统的线程调度 API,如:

平台底层机制
Linux使用 pthread_cond_waitfutex 系统调用
Windows使用 WaitOnAddressCondition Variable
Solaris使用 lwp_park / lwp_unpark

2. 线程挂起与恢复流程

✅ park() 的执行过程:

  1. 检查当前线程的 permit 值:
    • 如果 permit == 1 → 清除 permit,立即返回。
    • 如果 permit == 0 → 当前线程进入等待状态。
  2. 等待其他线程调用 unpark() 或发生中断。
  3. 被唤醒后,继续执行后续代码。

✅ unpark(Thread thread) 的执行过程:

  1. 找到目标线程的 JVM 内部表示(JavaThread*)。
  2. 尝试将目标线程的 permit 设置为 1。
    • 如果目标线程正在 park(),则它会被唤醒。
    • 如果目标线程尚未调用 park(),那么下次调用时可以直接返回(因为 permit 已经为 1)。

📦 三、与 Object.wait() 的区别(底层视角)

对比项LockSupport.park/unparkObject.wait/notify
实现层级JVM + OS 调度支持(更底层)基于对象监视器(Monitor)
是否依赖 synchronized❌ 否✅ 是
是否可以跨线程精确控制✅ 可以指定线程❌ notifyAll() 或 notify()
是否有虚假唤醒风险✅ 有✅ 有
是否清除中断标志park() 不清除中断标志wait() 抛出异常并清除中断标志
性能开销更轻量级相对较重(需要 Monitor 获取)

🎯 四、为什么 unpark() 可以“先调用”?

这是 LockSupport 的一个重要特性,也是其优于 wait/notify 的地方。

  • unpark() 会把目标线程的 permit 设置为 1
  • 如果之后该线程调用 park(),由于 permit == 1,会直接返回而不会阻塞。
  • 这意味着你可以在任意时机唤醒线程,而不必担心它是否已经进入等待状态。

💡 举例说明:

Thread t = new Thread(() -> {
    LockSupport.park(); // 允许在 park 之前 unpark
});
t.start();

LockSupport.unpark(t); // 先 unpark

在这种情况下,线程 t 在执行 park() 时会立即返回,不会阻塞。


🧩 五、实际应用中的典型场景

1. AQS(AbstractQueuedSynchronizer)中的使用

AQS 中大量使用了 LockSupport 来实现线程的阻塞和唤醒:

  • acquireQueued():在线程获取锁失败时调用 park() 挂起自己。
  • release():释放锁后调用 unpark() 唤醒同步队列中的下一个节点。
final boolean acquireQueued(final Node node, int arg) {
    try {
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                return false;
            }
            if (shouldParkAfterFailedAcquire(p, node))
                LockSupport.park(this); // 挂起当前线程
        }
    } catch (InterruptedException ie) {
        selfInterrupt();
        return true;
    }
}

2. ForkJoinPool 中的任务调度

ForkJoinPool 使用 unpark() 显式唤醒工作线程来处理任务,提高并发效率。


🧪 六、伪唤醒(Spurious Wakeup)问题

虽然不像 wait() 那样频繁,但 park() 也可能出现伪唤醒(Spurious Wakeup),即线程未被显式 unpark() 却从 park() 返回。

✅ 正确做法:

始终将 park() 放入循环中,并配合状态变量判断是否应该继续阻塞:

while (!canProceed()) {
    LockSupport.park();
}

🧾 七、总结

特性描述
park()阻塞当前线程,直到被 unpark() 或中断唤醒
unpark(Thread)唤醒指定线程,即使它尚未调用 park() 也能生效
许可机制每个线程有一个 permit(0 或 1)
可靠性支持跨线程精确唤醒,避免 notify() 的不确定性
底层实现依赖 JVM native 方法,调用 OS 线程调度接口
应用场景AQS、线程池、ForkJoin、自定义锁等并发框架