1. 概述

Java 提供了一些仅供内部使用的 API,明确警告开发者不要滥用。比如 JVM 开发者特意将某些类命名为 Unsafe,这本身就是一个危险信号。然而,这并不能阻止开发者们使用这些类。

本教程将深入分析 Unsafe.park() 为何真正不安全。目的不是吓唬人,而是帮助大家理解 park()unpark(Thread) 方法的内部机制。

2. Unsafe

Unsafe 类包含底层 API,专为内部库设计。尽管引入了 JPMSsun.misc.Unsafe 仍然可访问。 这是为了保持向后兼容性,支持依赖此 API 的库和框架。具体原因可参考 JEP 260

本文不会直接使用 Unsafe,而是通过 java.util.concurrent.locks 包中的 LockSupport 类,它封装了对 Unsafe 的调用:

public static void park() {
    UNSAFE.park(false, 0L);
}

public static void unpark(Thread thread) {
    if (thread != null)
        UNSAFE.unpark(thread);
}

3. park() vs. wait()

park()unpark(Thread) 的功能与 wait()notify() 类似。让我们分析它们的差异,理解为何前者存在风险。

3.1. 缺少监视器机制

wait()/notify() 不同,park()/unpark(Thread) 不需要 监视器。任何能获取线程引用的代码都能唤醒它。这在底层代码中可能有用,但会引入额外复杂性和难以调试的问题。

Java 的监视器机制要求线程必须先获取监视器才能使用它,这能防止竞态条件并简化同步。尝试在未获取监视器时唤醒线程会抛出异常:

@Test
@Timeout(3)
void giveThreadWhenNotifyWithoutAcquiringMonitorThrowsException() {
    Thread thread = new Thread() {
        @Override
        public void run() {
            synchronized (this) {
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    // 线程被中断
                }
            }
        }
    };

    assertThrows(IllegalMonitorStateException.class, () -> {
        thread.start();
        Thread.sleep(TimeUnit.SECONDS.toMillis(1));
        thread.notify();
        thread.join();
    });
}

未获取监视器就调用 notify() 会抛出 IllegalMonitorStateException。这种机制强制更好的编码实践,避免潜在问题。

park()/unpark(Thread) 的行为完全不同:

@Test
@Timeout(3)
void giveThreadWhenUnparkWithoutAcquiringMonitor() {
    Thread thread = new Thread(LockSupport::park);
    assertTimeoutPreemptively(Duration.of(2, ChronoUnit.SECONDS), () -> {
        thread.start();
        LockSupport.unpark(thread);
    });
}

✅ 只需线程引用就能控制线程。
❌ 这提供了更强大的锁定能力,但也暴露出更多风险。

显然,park()/unpark(Thread) 在底层代码中有用,但在常规应用代码中应避免使用,否则会增加复杂性和代码晦涩度。

3.2. 上下文信息缺失

由于缺少监视器机制,线程挂起时的上下文信息也更少。当线程挂起时,无法清楚知道挂起原因、时间或是否有其他线程因相同原因挂起。看这个示例:

public class ThreadMonitorInfo {
    private static final Object MONITOR = new Object();
    public static void main(String[] args) throws InterruptedException {
        Thread waitingThread = new Thread(() -> {
            try {
                synchronized (MONITOR) {
                    MONITOR.wait();
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }, "Waiting Thread");
        Thread parkedThread = new Thread(LockSupport::park, "Parked Thread");

        waitingThread.start();
        parkedThread.start();

        waitingThread.join();
        parkedThread.join();
    }
}

使用 jstack 查看 线程转储

"Parked Thread" #12 prio=5 os_prio=31 tid=0x000000013b9c5000 nid=0x5803 waiting on condition [0x000000016e2ee000]
   java.lang.Thread.State: WAITING (parking)
        at sun.misc.Unsafe.park(Native Method)
        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:304)
        at com.baeldung.park.ThreadMonitorInfo$$Lambda$2/284720968.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:750)

"Waiting Thread" #11 prio=5 os_prio=31 tid=0x000000013b9c4000 nid=0xa903 in Object.wait() [0x000000016e0e2000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on <0x00000007401811d8> (a java.lang.Object)
        at java.lang.Object.wait(Object.java:502)
        at com.baeldung.park.ThreadMonitorInfo.lambda$main$0(ThreadMonitorInfo.java:12)
        - locked <0x00000007401811d8> (a java.lang.Object)
        at com.baeldung.park.ThreadMonitorInfo$$Lambda$1/1595428806.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:750)

⚠️ 分析线程转储时,挂起线程的信息明显更少。
使用特定并发结构(如 CyclicBarrier)能提供更丰富的上下文信息,帮助诊断问题。

3.3. 中断标志处理差异

另一个关键差异是 中断 的处理方式。先看等待线程的行为:

@Test
@Timeout(3)
void givenWaitingThreadWhenNotInterruptedShouldNotHaveInterruptedFlag() throws InterruptedException {

    Thread thread = new Thread() {
        @Override
        public void run() {
            synchronized (this) {
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    // 线程被中断
                }
            }
        }
    };

    thread.start();
    Thread.sleep(TimeUnit.SECONDS.toMillis(1));
    thread.interrupt();
    thread.join();
    assertFalse(thread.isInterrupted(), "线程不应保留中断标志");
}

中断等待线程时,wait() 会立即抛出 InterruptedException 并清除 中断标志。因此最佳实践是使用 while 循环检查等待条件,而非依赖中断标志。

而挂起线程的行为完全不同:

@Test
@Timeout(3)
void givenParkedThreadWhenInterruptedShouldNotResetInterruptedFlag() throws InterruptedException {
    Thread thread = new Thread(LockSupport::park);
    thread.start();
    thread.interrupt();
    assertTrue(thread.isInterrupted(), "线程应保留中断标志");
    thread.join();
}

❌ 挂起线程不会立即响应中断,也不会抛出异常。
❌ 中断标志不会被清除(与等待线程不同)。

忽略此特性可能导致中断处理问题。例如,若未在挂起线程中断后重置标志,可能引发隐蔽的 Bug。

3.4. 抢占式许可机制

park/unpark 基于 二元信号量 工作。可以预先为线程提供许可:

private final Thread parkedThread = new Thread() {
    @Override
    public void run() {
        LockSupport.unpark(this);
        LockSupport.park();
    }
};

@Test
void givenThreadWhenPreemptivePermitShouldNotPark()  {
    assertTimeoutPreemptively(Duration.of(1, ChronoUnit.SECONDS), () -> {
        parkedThread.start();
        parkedThread.join();
    });
}

✅ 先调用 unpark() 提供许可,后续 park() 不会挂起线程,而是消耗许可继续执行。

由于是二元信号量,多次 unpark() 不会累积许可:

private final Thread parkedThread = new Thread() {
    @Override
    public void run() {
        LockSupport.unpark(this);
        LockSupport.unpark(this);
        LockSupport.park();
        LockSupport.park();
    }
};

@Test
void givenThreadWhenRepeatedPreemptivePermitShouldPark()  {
    Callable<Boolean> callable = () -> {
        parkedThread.start();
        parkedThread.join();
        return true;
    };

    boolean result = false;
    Future<Boolean> future = Executors.newSingleThreadExecutor().submit(callable);
    try {
        result = future.get(1, TimeUnit.SECONDS);
    } catch (InterruptedException | ExecutionException | TimeoutException e) {
        // 预期线程被挂起
    }
    assertFalse(result, "线程应被挂起");
}

⚠️ 线程只有一个许可,第二次调用 park() 会真正挂起线程。
若处理不当,可能导致意外行为。

4. 总结

本文分析了 park() 方法为何被视为不安全。JVM 开发者隐藏或建议不使用内部 API 是有充分理由的:
❌ 不仅因为当前可能危险且产生意外结果
❌ 更因为这些 API 未来可能变更,且不保证兼容性
❌ 还需要深入理解底层机制(不同平台可能不同),否则代码会变得脆弱且难以调试

建议: 优先使用高级并发工具(如 LockSemaphore),仅在必要时谨慎使用底层 API。

本文代码可在 GitHub 获取。


原始标题:Why Is sun.misc.Unsafe.park Actually Unsafe?