从另一个线程读取共享变量(有效Java#66)

从另一个线程读取共享变量(有效Java#66),java,concurrency,Java,Concurrency,在有效的Java:item 66中,Joshua Bloch给出了一个关于生命失败的例子: // Broken! - How long would you expect this program to run class StopThread { private static boolean stopRequested = false; public static void main(String[] args) throws InterruptedE

在有效的Java:item 66中,Joshua Bloch给出了一个关于生命失败的例子:

// Broken! - How long would you expect this program to run
class StopThread {
    private static boolean stopRequested = false;

    public static void main(String[] args) 
            throws InterruptedException {

        Thread backgroundThread = new Thread(new Runnable() {
            public void run() {
                int i = 0;
                while (!stopRequested) {
                    i++;
                }
           }
        });
        backgroundThread.start();

        TimeUnit.SECONDS.sleep(1);
        stopRequested = true;
    }   
}
正如约书亚·布洛赫所说,这项计划不会终止。 但是,如果我将
I++
更改为
System.out.println(I++)
,它将成功终止


我不知道这是怎么发生的

问题与变量
stopRequest
的内存值有关

此变量未定义为
易失性

如果有两个处理器,则内部线程检查从其注册表获取的
stopRequest

主线程改变另一个处理器注册表中的
stopRequest

因此,主线程修改了
stopRequest
的值,但线程只看到它的一个副本,它永远不会改变

查看
PrintStream
的源代码后修改(感谢ΔλЛ的推荐):使用
System.out.print
命令将使用显式
synchronized
块打印传递给它的值。这将授予
stopRequest
的值是从主存而不是从处理器的注册表获取的

添加一个
volatile
关键字将通知JVM从主内存而不是从处理器的注册表中获取值,从而解决问题

同时使用关键字
synchronized
将解决此问题,因为同步块中使用的任何变量都将被获取并更新主内存

    Processor 1         Processor 2     Main memory
-----------         -----------     -----------
       NA                   NA           false
       NA                   NA           true       // After setting 
                                                  //stopRequest to true
不带volatile的内存模型(主线程使用处理器1,显式线程使用处理器2)

stopRequest
定义为
volatile
,其中所有线程都从主存读取
stopRequest

    Processor 1         Processor 2     Main memory
-----------         -----------     -----------
       NA                   NA           false
       NA                   NA           true       // After setting 
                                                  //stopRequest to true

tl;dr:这很可能是
println
同步的“意外”副作用

首先,要意识到,并不是保证线程不会完成;问题是它不能保证它会完成。换句话说,
stopRequested
上的竞态条件——由一个线程正在向其写入,另一个线程正在从中读取,并且两个线程之间没有同步——意味着JVM可以(但不需要)让读卡器看到写入器做了什么


那么,为什么
System.out.println
会改变这一点呢?因为这是一个同步的方法。就JLS而言,这实际上并没有为您提供任何关于
stopRequested
的保证,但它确实意味着JVM必须做一些事情,比如获取(n个不相关的)锁,并在边缘之前建立(不相关的)锁,这使得对
stopRequested
的写入更可能在多个线程中看到。

由于您没有告诉线程
stopRequested
是一个可以从该线程外部修改的值,因此不能保证
while
将计算为变量的最新值

这就是为什么
volatile
关键字在这种情况下很有用,因为它将明确强制执行
stopRequested
值在读取时将是任何线程设置的最新值

实际上,从线程的角度来看,还有更多的考虑因素,
stopRequested
是一个循环不变量,因为它从来都不是由只读设置的,所以也应该考虑优化选择:如果认为某个值没有被修改,那么就没有理由在每次迭代中对其进行评估。

System.out.println()
请求一个资源在控制台上写入,这本身就是一种阻塞方法……这意味着,它将阻塞控制台上的
backgroundThread()
print()
。这类似于向其发送中断


这样,<代码> BeaBeaTeRead()/Coe>将意识到布尔值的变化和停止执行,从而终止守护进程。

独立于答案,您还需要考虑该书是10年和3个主要版本旧的。事情有时会改变。@SeanPatrickFloyd有时不会,不过:项目中报告的行为仍然会发生。@AndyTurner我知道。这就是为什么我将此作为评论提供,而不是作为answer@SeanPatrickFloyd你说的话是在转移视线。10年前适用于Java的原则在今天仍然基本正确。在这个特殊的案例中,并发行为仍然是完全合法的,并且符合规范。我从来没有说过别的。我自己也是高效Java的拥护者。@flkes:最好使用中断来实现这一点,而不是制作一个重复现有功能的标志。应该提到println获取了一个锁,它最终强制变量的更新值变为可见。是的。。。。但我认为约书亚的努力是为了解释一个细微的错误也会产生非常奇怪的错误,但可以肯定的是,原子布尔运算也会解决这个问题。@NathanHughes true;只能调用
Thread::interrupt
。我只是更一般地考虑从多个线程访问布尔值。我认为需要注意的是,仅仅添加
synchronized
并不能保证解决问题,除非两个线程在同一个确切的对象上同步。否则,同步使写入更有可能被看到,但不能保证。@ΔλЛ你是对的。这不是一条新的线索。仅是将导致从主存读取的synchronized关键字。ThanksIt没有在本文中解释
System.out.println()
的效果。但是
System.out.println()
的效果并不确定,因为在某些机器上,它可以解决其他机器上的问题,但在另一些机器上它无法解决,所以理解原因是不相关的,因为问题在其他地方。根据CPU上线程的分配方式,谁负责buf