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

时间:2016-02-22 16:42:29

标签: java concurrency

Effective Java :第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++),则会成功终止!

我无法弄清楚它是如何发生的!

4 个答案:

答案 0 :(得分:5)

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

此变量未定义为volatile

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

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

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

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

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

同样使用关键字synchronized将解决此问题,因为在同步块中使用的任何变量都将被采用并更新主存储器。

没有volatile的内存模型(主线程使用处理器1,显式线程使用处理器2)

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

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

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

答案 1 :(得分:1)

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

首先,要意识到线程不是保证不完成;这是因为它不能保证完成。换句话说,stopRequested上的竞争条件 - 由一个线程正在写入它,另一个线程正在从中读取,并且两个线程之间没有同步 - 这意味着JVM 可能,但不是必须的,让读者看看作者做了什么。

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

答案 2 :(得分:1)

由于您没有告诉线程stopRequested是一个可以从该线程外部修改的值,因此无法保证while将评估该变量的最新值。< / p>

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

还有一些考虑因素,实际上从线程的角度来看,stopRequested是一个循环不变量,因为它永远不会只被读取,所以也应该考虑优化选择:如果一个值被认为是不进行修改,然后没有理由在每次迭代时对其进行评估。

答案 3 :(得分:0)

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

因此,backgroundThread()将意识到布尔值的变化并停止执行,从而终止守护进程。