线程永远不会停止在Java中

时间:2016-03-05 03:41:26

标签: java multithreading

我正在阅读Effective Java第10章:并发;第66项:同步对共享可变数据的访问,有一些代码如下:

public class StopThread {
private static boolean stopRequested;
public static void main(String[] args) throws InterruptedException {
    // TODO Auto-generated method stub
    System.out.println(stopRequested);
    Thread backgroundThread = new Thread(new Runnable(){

        @Override
        public void run() {
            // TODO Auto-generated method stub
            int i = 0;
            while (!stopRequested){
                i++;
            }

            System.out.println("done");
        }

    });
    backgroundThread.start();
    TimeUnit.SECONDS.sleep(1);
    stopRequested = true;
}

}

首先,我认为线程应运行一秒然后停止,因为之后stopRequested设置为true。但是,该计划永远不会停止。永远不会打印done。作者说

while (!stopRequested)
    i++;

将转变为:

if (!stopRequested)
     while(true)
         i++;

有人可以解释一下吗?

我发现的另一件事是,如果我将程序更改为:

public class StopThread {
private static boolean stopRequested;
public static void main(String[] args) throws InterruptedException {
    // TODO Auto-generated method stub
    System.out.println(stopRequested);
    Thread backgroundThread = new Thread(new Runnable(){

        @Override
        public void run() {
            // TODO Auto-generated method stub
            int i = 0;
            while (!stopRequested){
                i++;
                System.out.println(i);
            }

            System.out.println("done");
        }

    });
    backgroundThread.start();
    TimeUnit.SECONDS.sleep(1);
    stopRequested = true;
}

}

程序运行1秒并按预期停止。这有什么区别?

2 个答案:

答案 0 :(得分:8)

我怀疑作者是否真的这样说过(确切地说)。

但重点是

while (!stopRequested)
    i++;

可以表现得像

if (!stopRequested)
     while(true)
         i++;

因为Java规范允许将stopRequested的初始值缓存在寄存器中,或者从内存缓存中的(可能过时的)副本中获取。一个线程不保证读取由另一个线程进行的内存写入的结果,除非在写入和后续读取之间存在正式的“发生之前”关系。在这种情况下,没有这种关系。这意味着没有指定子线程是否会看到父线程分配给stopRequested的结果。

正如该书的作者所解释的那样,解决方案包括:

  • stopRequested声明为volatile
  • 确保读取和写入stopRequested的代码在同一对象上同步的synchronized块或方法中执行,
  • 使用Lock个对象而不是synchronized
  • 满足“之前发生”要求的其他并发机制。

然后你问为什么你的测试似乎有效。

这可以通过以下事实来解释:尽管孩子不能保证看到父母的任务的影响,但也不能保证不会看到它... ...

或者换句话说。 Java规范没有说明这两种可能性中的哪一种会发生。

现在,对于由特定编译器编译的特定程序,由特定硬件上的特定版本的JVM运行,您可以发现程序以一种方式(或另一种方式)始终如一地运行。也许99.9%的时间。也许甚至100%的时间。但是在不同的上下文中编译和运行的相同程序可以表现不同。 JLS这样说。

为什么程序的两个几乎完全相同的版本表现不同的另一个原因是System.out PrintWriter对象在调用println时正在进行一些内部同步。这可能会给你一个偶然的“发生之前”。

答案 1 :(得分:3)

我认为你正在制作Joshua Bloch(这本伟大着作的作者)说出一些他没说的内容:-)。确切地说,这本书说明了以下内容(仅强调我的):

如果没有同步,虚拟机可以转换此代码

while (!done)
  i++;

进入此代码:

if (!done)
  while (true)
    i++;

要明白他的意思(以比他自己在第261-264页所做的更好的方式来解释它,但我会尝试。抱歉Josh!)你应该首先尝试运行这个逐字编程,看看发生了什么。使用多线程,一切皆有可能,但这就是我所做的:

  1. 按原样编排StopThread
  2. 使用JRE 1.8.0_72在我的Linux计算机上运行它。
  3. 它只是挂了!所以,这种行为并没有改变他所描述的内容。
  4. 然后我采取了'线程转储'看看发生了什么。您只需向运行的JVM pid发送kill -3信号即可查看线程正在执行的操作。这是我观察到的(线程转储的相关部分):
  5. "DestroyJavaVM" #10 prio=5 os_prio=0 tid=0x00007fd678009800 nid=0x1b35 waiting on condition [0x0000000000000000]
       java.lang.Thread.State: RUNNABLE
    
    "Thread-0" #9 prio=5 os_prio=0 tid=0x00007fd6780f6800 nid=0x1b43 runnable [0x00007fd64b5be000]
       java.lang.Thread.State: RUNNABLE
      at StopThread$1.run(StopThread.java:14)
      at java.lang.Thread.run(Thread.java:745)
    
    "Service Thread" #8 daemon prio=9 os_prio=0 tid=0x00007fd6780c9000 nid=0x1b41 runnable [0x0000000000000000]
       java.lang.Thread.State: RUNNABLE
    

    正如您所看到的,我们开始的后台线程还活着,正在执行某些事情。我查看了我的计算机名为top的诊断工具,这里显示的是:

    top cmd output

    你能看到我的一台CPU(它是四核计算机)完全忙(100%!)做某事,而且它是正在做某事的java 进程。难道不是很令人费解吗?好吧有点令人费解。当CPU忙于做一些你不了解的事情时,一个很可能的原因是它正在不知疲倦地检查一些内存位置的内容。在这种情况下,如果您尝试连接点,它是 stopRequested 变量,其值(我们可以预期)会不断读取。所以,实际上,CPU只是读取布尔值,一直发现它是假的,它会回到检查它是否已经改变!再一次,它发现它没有(它仍然挂在我的机器上,因为我写这个:-))。

    你会说......没有main thread(顺便说一下,它已经很久了,因为它没有出现在线程转储中)stopRequested = true

    是的,确实如此!

    当然,你会怀疑,为什么Thread-0 呢?

    其中有线索。在存在数据争用的情况下,线程写入的值不会可见到另一个读取它的线程。

    现在我们看一下该数据的声明,即显示这种特殊行为的变量:

    private static boolean stopRequested;
    

    就是这样!这个特殊的声明是相当缺乏的,只要它涉及各种各种方(编译器,及时编译器及其优化......)的处理。在这种不明确的情况下,任何事情都可能发生。特别是,主线程(认为它)写的值可能永远不会被写入主内存中以便Thread-0读取,从而使其进入无限循环。

    因此,这是一个可见性问题。如果没有足够的同步,则无法保证线程写入的值将被另一个线程看到

    这可以解释一下吗?有关更多详细信息,我们都需要更好地了解现代硬件。 Herlihy和Shavit的优秀资源是The Art of Multiprocessor Programming。本书让软件工程师了解硬件的复杂性,并解释了为什么多线程如此困难。