1. 概述
Java 提供了一些仅供内部使用的 API,明确警告开发者不要滥用。比如 JVM 开发者特意将某些类命名为 Unsafe,这本身就是一个危险信号。然而,这并不能阻止开发者们使用这些类。
本教程将深入分析 Unsafe.park() 为何真正不安全。目的不是吓唬人,而是帮助大家理解 park() 和 unpark(Thread) 方法的内部机制。
2. Unsafe 类
Unsafe 类包含底层 API,专为内部库设计。尽管引入了 JPMS,sun.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();
}
}
"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 未来可能变更,且不保证兼容性
❌ 还需要深入理解底层机制(不同平台可能不同),否则代码会变得脆弱且难以调试
建议: 优先使用高级并发工具(如 Lock、Semaphore),仅在必要时谨慎使用底层 API。
本文代码可在 GitHub 获取。