Java 中的线程间通信

Inter-thread communication in Java

为什么在下面的Java代码中:

public class WaitForOther {

private volatile boolean in = false;// need volatile for transitive closure
                                    // to occur



public void stoppingViaMonitorWait() throws Exception {

    final Object monitor = new Object();

    new Thread(new Runnable() {

        @Override
        public void run() {
            synchronized (monitor) {
                in = true;
                try {
                    monitor.wait(); // this releases monitor
                    //Thread.sleep(8000); //No-op
                    System.out.println("Resumed in "
                            + Thread.currentThread().getName());

                } catch (InterruptedException ignore) {/**/
                }
            }

        }
    }).start();

    System.out.println("Ready!");
    while (!in); // spin lock / busy waiting
    System.out.println("Set!");
    synchronized (monitor) {
        System.out.println("Go!");
        monitor.notifyAll();
    }

}

取消对 Thread.sleep(8000); //No-op 的注释会缩短输出:

Ready! Set! Go!

否则会在中断的线程 0 中正确恢复:

Ready!
Set!
Go!
Resumed in Thread-0

这是调用上述行为的 JUnit 测试,正如评论中所要求的那样:

public class WaitForOtherTest {

    WaitForOther cut  = new WaitForOther();


    @Test
    public void testStoppingViaMonitorWait() throws Exception {
        cut.stoppingViaMonitorWait();
    }



}

谢谢!

此代码滥用了 Java 的低级线程通信结构,如 waitnotify。不清楚你想用这段代码建立什么。

以下是我对您的程序行为的观察(这些与您的不同。我 运行 它在 IDE 和命令行中使用服务器和客户端编译器):

sleep() //注释掉//:

Ready!
thread is daemon? : false
Set!
Go!
Resumed in Thread-0

随着 sleep() 未注释:

Ready!
thread is daemon? : false
Set!
Go!
<< 8 seconds pass >>
Resumed in Thread-0

因此,它按照您的设计运行:main 线程启动一个非守护线程(比如 thread-0)。 main 线程和 thread-0 然后争夺锁 (monitor),但 thread-0 总是赢,因为你想要它。

如果thread-0抢到了锁,它会在in上设置volatile write并立即放弃锁让main线程通过wait()ing通知它在上面。现在 main 线程看到易失更新(visibility gua运行tee),跳出忙等待,获取锁,打印 "Go" 并通知 thread-0,根据其睡眠状态得到通知。

main 线程由于忙等待而无法赢得锁。

我添加了一行来说明你的线程是否是 daemon 并且由于 JVM 没有退出,因为你的线程是一个非守护线程,它保持足够长的时间从睡眠中唤醒并打印其行的线程。

据说 wait() 应该只用于等待 在循环中 条件变量。

我已经在 J​​Unit 中尝试过你的测试,但我得到的结果与你的经历相反:

  1. Thread.sleep 被注释掉时,测试运行正常并打印 "Resumed in <>"
  2. Thread.sleep 在代码中(实际执行)时,JVM 终止并且 "Resumed in ..." 打印。

原因是 JUnit 在测试完成后终止了 VM。它做了 System.exit();。您很幸运在案例 1 中获得了完整的输出,因为它是在单独的线程中打印的,而 JUnit 并不等待该线程。

如果你想确保线程在测试方法结束之前完成,要么你需要让你的 API 等待线程,要么你需要让测试等待线程。

如果你的stoppingViaMonitorWait方法returns它创建的线程,你可以在测试中等待。

@Test
public void testStoppingViaMonitorWait() throws Exception {
    Thread x = cut.stoppingViaMonitorWait();
    x.join();
}

另一种选择是将线程池(ExecutorService 的一个实例)注入到您正在测试的 class 中,让它在池中安排线程(这在任何情况下都更好) case), 在你的测试方法中你可以调用 ExecutorService.awaitTermination.

您的观察与 Java 内存模型无关。尝试 运行 使用以下代码:

new Thread() {
  @Override
  public void run() {
    try {
      Thread.sleep(1000);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    System.out.println("Will not be printed when running JUnit");
  }
}.start();

如果您 运行 它在 main 方法中,线程 - 不是 a deamon thread - 保持进程活动直到睡眠期结束,以便打印最后一行,之后进程结束。

由于 JUnit 正在测试可能损坏的代码,因此它需要假设从单元测试启动的线程可能永远不会结束。因此,框架不会等到所有启动的线程终止,而是在所有测试方法返回后显式终止 Java 进程。 (假设您没有为您的测试设置超时,这个超时额外适用。)这样,JUnit 测试套件就不容易受到测试中损坏代码的影响。

但是,如果您的测试套件执行时间比启动线程的 运行时间长,您仍然可以看到打印的语句。例如通过添加另一个测试:

public class WaitForOtherTest {

  WaitForOther cut = new WaitForOther();

  @Test
  public void testStoppingViaMonitorWait1() throws Exception {
    cut.stoppingViaMonitorWait();
  }

  @Test
  public void testStoppingViaMonitorWait2() throws Exception {
    Thread.sleep(9000);
  }
}

您仍然可以看到打印的行。

但是请注意,此打印结果相当不稳定,因为它依赖于特定的、不确定的测试执行顺序和非等待代码的短 运行 时间。 (然而,它通常适用于 运行 这个示例,足以用于演示目的)。