Why Is sun.misc.Unsafe.park Actually Unsafe? – 为什么 sun.misc.Unsafe.park 实际上不安全?

最后修改: 2023年 11月 13日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

Java provides certain APIs for internal use and discourages unnecessary use in other cases. The JVM developers gave the packages and classes names such as Unsafe, which should warn developers.  However, often, it doesn’t stop developers from using these classes.

Java 为内部使用提供了某些 API,并不鼓励在其他情况下不必要地使用这些 API。JVM开发人员给包和类起了Unsafe这样的名字,这应该会给开发人员带来警告。 然而,这往往无法阻止开发人员使用这些类。

In this tutorial, we’ll learn why Unsafe.park() is actually unsafe. The goal isn’t to scare but to educate and provide a better insight into the interworking of the park() and unpark(Thread) methods.

在本教程中,我们将了解为什么 Unsafe.park() 实际上是不安全的。本教程的目的不是为了吓唬人,而是为了教育和更好地了解 park()unpark(Thread) 方法之间的交互作用。

2. Unsafe

2.不安全</em

The Unsafe class contains a low-level API that aims to be used only with internal libraries. However, sun.misc.Unsafe is still accessible even after the introduction of JPMS. This was done to maintain backward compatibility and support all the libraries and frameworks that might use this API. In more detail, the reasons are explained in JEP 260,

Unsafe类包含一个低级应用程序接口,其目的是仅用于内部库。然而,即使在引入 JPMS 之后,sun.misc.Unsafe 仍然可以访问。这样做是为了保持向后兼容性,并支持可能使用此 API 的所有库和框架。JEP 260 中解释了更详细的原因、

In this article, we won’t use Unsafe directly but rather the LockSupport class from the java.util.concurrent.locks package that wraps calls to Unsafe:

在本文中,我们不会直接使用 Unsafe 类,而是使用 java.util.concurrent.locks 包中的 LockSupport 类,该类封装了对 Unsafe 的调用:</em

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

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

3. park() vs. wait()

3.park()wait()

The park() and unpark(Thread) functionality are similar to wait() and notify(). Let’s review their differences and understand the danger of using the first instead of the second.

park()unpark(Thread) 功能与 wait()notify() 类似。让我们回顾一下它们的区别,并了解使用前者而非后者的危险性。

3.1. Lack of Monitors

3.1.缺乏监督员

Unlike wait() and notify(), park() and unpark(Thread) don’t require a monitor. Any code that can get a reference to the parked thread can unpark it. This might be useful in low-level code but can introduce additional complexity and hard-to-debug problems. 

wait()notify() 不同,park()unpark(Thread) 不需要 监视器。任何代码只要能获得已停机线程的引用,就能解除停机。这在底层代码中可能有用,但会带来额外的复杂性和难以调试的问题。

Monitors are designed in Java so that a thread cannot use it if it hasn’t acquired it in the first place. This is done to prevent race conditions and simplify the synchronization process. Let’s try to notify a thread without acquiring it’s monitor:

Java 中设计了监视器,因此如果线程首先没有获取监视器,就无法使用监视器。这样做是为了防止出现竞赛条件并简化同步过程。让我们尝试在未获取监视器的情况下通知一个线程:

@Test
@Timeout(3)
void giveThreadWhenNotifyWithoutAcquiringMonitorThrowsException() {
    Thread thread = new Thread() {
        @Override
        public void run() {
            synchronized (this) {
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    // The thread was interrupted
                }
            }
        }
    };

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

Trying to notify a thread without acquiring a monitor results in IllegalMonitorStateException. This mechanism enforces better coding standards and prevents possible hard-to-debug problems.

在未获取监视器的情况下尝试通知线程会导致 IllegalMonitorStateException 异常。这种机制可执行更好的编码标准,并防止可能出现难以调试的问题。

Now, let’s check the behavior of park() and unpark(Thread):

现在,让我们检查一下 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);
    });
}

We can control threads with little work. The only thing required is the reference to the thread. This provides us with more power over locking, but at the same time, it exposes us to many more problems.

我们只需少量工作就能控制线程。 这为我们提供了更强大的锁定功能,但同时也让我们面临更多问题。

It’s clear why park() and unpark(Thread) might be helpful for low-level code, but we should avoid this in our usual application code because it might introduce too much complexity and unclear code.

很明显,park()unpark(Thread) 对底层代码可能有帮助,但我们应该避免在常规应用代码中使用,因为它可能会带来太多复杂性和不清晰的代码。

3.2. Information About the Context

3.2.背景信息

The fact that no monitors are involved also might reduce the information about the context. In other words, the thread is parked, and it’s unclear why, when, and if other threads are parked for the same reason. Let’s run two threads:

没有监视器参与的事实也可能会减少有关上下文的信息。换句话说,线程停滞了,但不清楚原因、时间以及其他线程是否因相同原因停滞。让我们运行两个线程:

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();
    }
}

Let’s check the thread dump using jstack:

让我们使用 线程转储检查 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)

While analyzing the thread dump, it’s clear that the parked thread contains less information. Thus, it might create a situation when a certain thread problem, even with a thread dump, would be hard to debug.

在分析线程转储时,很明显停滞线程包含的信息较少。因此,这可能会造成一种情况,即即使有线程转储,也很难调试出某个线程问题。

An additional benefit of using specific concurrent structures or specific locks would provide even more context in the thread dumps, giving more information about the application state. Many JVM concurrent mechanisms are using park() internally. However, if a thread dump explains that the thread is waiting, for example, on a CyclicBarrier, it’s waiting for other threads.

使用特定并发结构或特定锁的另一个好处是可以在线程转储中提供更多上下文,从而提供有关应用程序状态的更多信息。许多 JVM 并发机制都在内部使用 park()。但是,如果线程转储说明线程正在等待(例如,在 CyclicBarrier 上),则说明该线程正在等待其他线程。

3.3. Interrupted Flag

3.3.中断标志

Another interesting thing is the difference in handling interrupts. Let’s review the behavior of a waiting thread:

另一个有趣的地方是在处理 中断时的不同。让我们回顾一下等待线程的行为:

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

    Thread thread = new Thread() {
        @Override
        public void run() {
            synchronized (this) {
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    // The thread was interrupted
                }
            }
        }
    };

    thread.start();
    Thread.sleep(TimeUnit.SECONDS.toMillis(1));
    thread.interrupt();
    thread.join();
    assertFalse(thread.isInterrupted(), "The thread shouldn't have the interrupted flag");
}

If we’re interrupting a thread from its waiting state, the wait() method would immediately throw an InterruptedException and clear the interrupted flag. That’s why the best practice is to use while loops checking the waiting conditions instead of the interrupted flag.

如果我们将线程从其等待状态中断,wait() 方法将立即抛出InterruptedException并清除中断标志。这就是为什么最佳做法是使用while循环检查等待条件而不是中断标志。

In contrast, a parked thread isn’t interrupted immediately and rather does it on its terms. Also, the interrupt doesn’t cause an exception, and the thread just returns from the park() method. Subsequently, the interrupted flag isn’t reset, as happens while interrupting a waiting thread:

相比之下,驻留线程不会立即被中断,而是按自己的意愿进行中断。此外,中断不会导致异常,线程只是从 park() 方法中返回。随后,中断标志不会重置,就像中断等待线程时发生的那样: park() 方法中的中断标志不会重置,就像中断等待线程时发生的那样: park() 方法中的中断标志不会重置。

@Test
@Timeout(3)
void givenParkedThreadWhenInterruptedShouldNotResetInterruptedFlag() throws InterruptedException {
    Thread thread = new Thread(LockSupport::park);
    thread.start();
    thread.interrupt();
    assertTrue(thread.isInterrupted(), "The thread should have the interrupted flag");
    thread.join();
}

Not accounting for this behavior may cause problems while handling the interruption. For example, if we don’t reset the flag after the interrupt on a parked thread, it may cause subtle bugs.

不考虑这种行为可能会在处理中断时造成问题。例如,如果我们不在停机线程中断后重置标志,可能会导致微妙的错误。

3.4. Preemptive Permits

3.4.优先许可

Parking and unparking work on the idea of a binary semaphore. Thus, we can provide a thread with a preemptive permit. For example, we can unpark a thread, which would give it a permit, and the subsequent park won’t suspend it but would take the permit and proceed:

停放和取消停放基于 二进制信号的理念。因此,我们可以为线程提供抢占式许可。例如,我们可以取消停放一个线程,这将给它一个许可,随后的停放将不会暂停它,而是接受许可并继续运行: <br

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();
    });
}

This technique can be used in some complex synchronization scenarios. As the parking uses a binary semaphore, we cannot add up permits, and two unpark calls wouldn’t produce two permits:

这种技术可用于某些复杂的同步场景。由于泊车使用的是二进制信号,我们无法将许可证累加,而且两次取消泊车调用也不会产生两个许可证:

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) {
        // Expected the thread to be parked
    }
    assertFalse(result, "The thread should be parked");
}

In this case, the thread would have only one permit, and the second call to the park() method would park the thread. This might produce some undesired behavior if not appropriately handled.

在这种情况下,线程将只有一个许可证,而对 park() 方法的第二次调用将停滞线程。如果处理不当,可能会产生一些不希望出现的行为。

4. Conclusion

4.结论

In this article, we learned why the park() method is considered unsafe. JVM developers hide or suggest not to use internal APIs for specific reasons. This is not only because it might be dangerous and produce unexpected results at the moment but also because these APIs might be subject to change in the future, and their support isn’t guaranteed.

在本文中,我们了解了 park() 方法被认为不安全的原因。JVM 开发人员出于特定原因隐藏或建议不要使用内部 API。这不仅是因为它在当前可能是危险的并会产生意想不到的结果,还因为这些 API 在将来可能会发生变化,而且它们的支持也得不到保证。

Additionally, these APIs require extensive learning about underlying systems and techniques, which may differ from platform to platform. Not following this might result in fragile code and hard-to-debug problems.

此外,这些应用程序接口需要广泛学习底层系统和技术,而这些系统和技术可能因平台而异。如果不遵循这一点,可能会导致代码脆弱和难以调试的问题。

As always, the code in this article is available over on GitHub.

与往常一样,本文中的代码可在 GitHub 上获取。