Java中的线程永远不会停止。

4

我正在阅读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秒钟并按预期停止。这里有什么不同之处?


实际上,在第二段代码中,你做了什么不同的事情?除了一个println之外,我看不到任何其他的东西。 - Aajan
第一个程序从未停止。第二个程序将在一秒钟内停止。您可以在您的桌面上尝试它。 - chrisTina
@Aajan,是的,我也只能在while循环中发现额外的println。 - stuXnet
类似于https://dev59.com/am445IYBdhLWcg3wTIdf?rq=1 - stuXnet
2个回答

8
我怀疑作者并没有确切地这么说。
但是重点在于,
while (!stopRequested)
    i++;

could可以像

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

由于Java规范允许将stopRequested的初始值缓存在寄存器中,或从(可能过期的)内存缓存中获取。除非写入和后续读取之间存在正式的“先于发生”关系,否则一个线程不能保证读取另一个线程所做的内存写入的结果。在这种情况下,不存在这样的关系。这意味着未指定子线程是否会看到父线程对stopRequested的赋值结果。
正如该书的作者所解释的那样,解决方案包括:
  • stopRequested声明为volatile
  • 确保读取和写入stopRequested的代码在同步块或同步方法中执行,该同步块或同步方法与相同对象同步,
  • 使用Lock对象而不是synchronized,或
  • 使用其他满足“先于发生”要求的并发机制。
然后你问为什么你的测试似乎有效。
这是因为虽然不能保证子级将看到父级的赋值效果,但也不能保证不会看到...两者之一。
换句话说,Java规范没有说明哪种情况会发生。
现在,对于由特定编译器编译、由特定版本的JVM运行在特定硬件上的特定程序,你可能会发现该程序一致地表现出一种方式(或另一种方式)。也许99.9%的时间。甚至可能是100%的时间。但在不同的上下文中编译和运行相同的程序可能会表现不同。JLS就是这么说的。
另一个解释为什么两个几乎相同版本的程序表现不同是System.out PrintWriter对象在调用println时进行了一些内部同步。这可能给您带来幸运的“先于发生”。

如果没有一秒钟的“睡眠”,我同意你的观点。但这不像是一个监听器吗?子线程一直在监听主线程所做的更改,如果有更改,它将执行其他操作? - chrisTina
@byteBiter - 不需要检查是否有“happens before”时,也不需要检查更改。这就是JLS所说的。 - Stephen C

3
我认为你在让这位伟大书籍的作者Joshua Bloch说他没有说过的话 :-).准确来说,这本书的意思是(仅强调我的部分):
没有同步的情况下,虚拟机转换这段代码是完全可接受的
while (!done)
  i++;

将此代码转换为:

这段代码:

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

为了理解他的意思(很难用比他自己在第261-264页更好的方式解释,但我会尝试。对不起,Josh!),您应该首先尝试逐字运行此程序并观察发生了什么。使用多线程,一切皆有可能,但这是我做的事情:
1. 编写 StopThread 如所示。 2. 在我的Linux计算机上使用JRE 1.8.0_72运行它。 3. 它只是挂起了! 因此,行为与他描述的相同。 4. 然后我获取了“线程转储”以查看正在发生什么。 您可以向运行中的JVM pid发送kill -3信号以查看线程正在执行的任务。 这是我观察到的(线程转储的相关部分):
"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(它是四核电脑)正在全力以赴地做 某些 事情,而且,是java进程在做这件事。这不令人困惑吗?在CPU繁忙地执行着您不理解的操作时,有一个非常可能的原因是它无休止地检查某个内存位置的内容。在这种情况下,如果您尝试连接这些点,那么它就是被不断读取值的布尔变量stopRequested。因此,实际上,CPU只是一直读取boolean的值,并发现它为false,然后返回检查其是否更改!同样,它发现它没有,因此它仍然挂在我的计算机上(当我写这篇文章时: -))。
你会说...主线程(顺便提一下,它已经消失了,因为它不出现在线程转储中)stopRequested = true,对吗?
是的,它做到了!
自然而然地,您会怀疑为什么Thread-0 线程没有看到它呢?
问题就在这里。在存在数据竞争的情况下,一个线程写入的值对于读取它的另一个线程来说是不可见的。
现在我们看一下这个数据的声明,这个变量展现了这种奇特的行为:
private static boolean stopRequested;

就是这样!在涉及的各方(编译器、即时编译器及其优化等)处理上,这种声明太少具体规定。在这种不够明确的情况下,任何事都可能发生。特别是,主线程写入的数值可能永远不会实际写入到主存中供“Thread-0”读取,使其进入无限循环。

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

这解释清楚了吗?要了解更多细节,我们都需要更好地了解现代硬件。Herlihy和Shavit的The Art of Multiprocessor Programming是一个极好的资源。这本书让软件工程师了解硬件的复杂性,并解释为什么多线程如此困难。


好的。假设 stopRequested 只是一个指针,它指向一个存储值的内存块。在最开始,这个值为 false。然后主线程和另一个子线程开始运行。之后,主线程在 1 秒后把 stopRequested 的值改为 true。请问您的意思是说子线程没有读取到相同的内存块? - chrisTina
在我提供的演示之后,你推断“子线程没有读取相同的内存块”有点奇怪。我们可以这样说:主线程试图将其写入Thread-0读取的相同位置。只是各个层面允许采取的自由最终导致主线程的更新未出现在主内存(RAM)中。例如,没有任何要求L1 / L2 / ..缓存的内容(主线程写入的内容)必须反映在RAM中。 - Kedar Mhaswade

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接