理解java.lang.Thread.State:WAITING(Parking)

2023/06/07

1. 概述

在本文中,我们将讨论Java线程状态-具体来说,Thread.State.WAITING。我们将研究线程进入此状态的方法以及它们之间的区别。最后,我们将仔细研究LockSupport类,该类提供了几个用于同步的静态工具方法。

2. 进入Thread.State.WAITING

Java提供了多种将线程置于WAITING状态的方法。

2.1 Object.wait()

我们可以将线程置于WAITING状态的最标准方法之一是通过wait()方法当一个线程拥有一个对象的监视器时,我们可以暂停它的执行,直到另一个线程完成一些工作并使用notify()方法将其唤醒。暂停执行时,线程处于WAITING(在对象监视器上)状态,程序的线程转储中也报告了这一点:

"WAITING-THREAD" #11 prio=5 os_prio=0 tid=0x000000001d6ff800 nid=0x544 in Object.wait() [0x000000001de4f000]
   java.lang.Thread.State: WAITING (on object monitor)

2.2 Thread.join()

我们可以用来暂停线程执行的另一种方法是通过join()调用当我们的主线程需要等待工作线程先完成时,我们可以从主线程调用工作线程实例上的join()。执行将被暂停,主线程将进入WAITING状态,从jstack报告为WAITING(在对象监视器上):

"MAIN-THREAD" #12 prio=5 os_prio=0 tid=0x000000001da4f000 nid=0x25f4 in Object.wait() [0x000000001e28e000]
   java.lang.Thread.State: WAITING (on object monitor)

2.3 LockSupport.park()

最后,我们还可以使用LockSupport类的park()静态方法将线程设置为WAITING状态。调用park()将停止执行当前线程并将其置于WAITING状态-更具体地说,jstack报告将显示的WAITING(parking)状态

"PARKED-THREAD" #11 prio=5 os_prio=0 tid=0x000000001e226800 nid=0x43cc waiting on condition [0x000000001e95f000]
   java.lang.Thread.State: WAITING (parking)

由于我们想更好地理解线程parking和unparking,让我们仔细看看它是如何工作的。

3. Parking和Unparking

正如我们之前看到的,我们可以使用LockSupport类提供的工具来park和unpark线程。这个类是Unsafe类的包装器,它的大部分方法都立即委托给它。但是,由于Unsafe被认为是内部Java API,不应使用,因此LockSupport是我们可以访问parking实用程序的官方方式

3.1 如何使用LockSupport

使用LockSupport非常简单。如果我们想停止线程的执行,我们调用park()方法。我们不必提供对线程对象本身的引用-代码会停止调用它的线程。

让我们看一个简单的parking示例:

public class Application {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            int acc = 0;
            for (int i = 1; i <= 100; i++) {
                acc += i;
            }
            System.out.println("Work finished");
            LockSupport.park();
            System.out.println(acc);
        });
        t.setName("PARK-THREAD");
        t.start();
    }
}

我们创建了一个最小的控制台应用程序,用于累积从1到100的数字并将它们打印出来。如果我们运行它,我们会看到它打印“Work finished”而不打印结果。当然,这是因为我们之前调用了park()。

要让PARK-THREAD完成,我们必须unpark它。为此,我们必须使用不同的线程。我们可以使用主线程(运行main()方法的线程)或创建一个新线程。

为了简单起见,让我们使用主线程:

t.setName("PARK-THREAD");
t.start();

Thread.sleep(1000);
LockSupport.unpark(t);

我们在主线程中添加一秒钟的睡眠,让PARK-THREAD完成累加并自行park。之后,我们通过调用unpark(Thread)来unpark它。正如预期的那样,在unparking期间,我们必须提供对要启动的线程对象的引用。

通过我们的更改,程序现在可以正确终止并打印结果5050。

3.2 Unpark许可

parking API的内部结构通过使用许可来工作。实际上,这类似于单个许可的信号量。park许可在内部用于管理线程的状态,park()方法使用它,而unpark()使它可用。

由于每个线程只能有一个可用的许可,因此多次调用unpark()方法没有任何效果。单个park()调用将禁用该线程。

然而,有趣的是,parked的线程等待许可变得可用以再次启用它自己。如果在调用park()时许可已经可用,则该线程永远不会被禁用。许可被消耗,park()调用立即返回,线程继续执行。

我们可以通过在前面的代码片段中删除对sleep()的调用来看到这种效果:

// Thread.sleep(1000);
LockSupport.unpark(t);

如果我们再次运行我们的程序,我们会看到PARK-THREAD的执行没有延迟。这是因为我们立即调用unpark(),这使得许可可用于park()。

3.3 Park重载

LockSupport类包含park(Object blocker)重载方法。blocker参数是负责线程parking的同步对象。我们提供的对象不会影响该parking过程,但它会在线程转储中报告,这可以帮助我们诊断并发问题。

让我们更改代码以包含一个同步器对象:

Object syncObj = new Object();
LockSupport.park(syncObj);

如果我们删除对unpark()的调用并再次运行应用程序,它就会挂起。如果我们使用jstack来查看PARK-THREAD在做什么,我们将得到:

"PARK-THREAD" #11 prio=5 os_prio=0 tid=0x000000001e401000 nid=0xfb0 waiting on condition [0x000000001eb4f000]
   java.lang.Thread.State: WAITING (parking)
        at sun.misc.Unsafe.park(Native Method)
        - parking to wait for  <0x000000076b4a8690> (a java.lang.Object)

正如我们所看到的,最后一行包含PARK-THREAD正在等待的对象。这对于调试目的很有帮助,这就是为什么我们应该更倾向于park(Object)重载。

4. Parking与Waiting

由于这两个API都为我们提供了类似的功能,我们应该更喜欢哪个?通常,LockSupport类及其功能被视为低级API。此外,API很容易被滥用,从而导致难以理解的死锁。对于大多数情况,我们应该使用Thread类的wait()和join()

使用parking的好处是我们不需要进入synchronized块来禁用线程。这很重要,因为同步块在代码中建立了happens-before关系,这会强制刷新所有变量,如果不需要,可能会降低性能。但是,这种优化应该很少发挥作用。

5. 总结

在本文中,我们探讨了LockSupport类及其parking API。我们研究了如何使用它来禁用线程并在内部解释了它是如何工作的。最后,我们将它与更常见的wait()/join() API进行了比较,并展示了它们的差异。

与往常一样,本教程的完整源代码可在GitHub上获得。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章