我正在阅读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秒并按预期停止。这有什么区别?
答案 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!)你应该首先尝试运行这个逐字编程,看看发生了什么。使用多线程,一切皆有可能,但这就是我所做的:
StopThread
。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
的诊断工具,这里显示的是:
你能看到我的一台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。本书让软件工程师了解硬件的复杂性,并解释了为什么多线程如此困难。