Java内存模型:混合不同步和同步

时间:2015-02-05 14:28:05

标签: java multithreading java-memory-model

假设有一个这样的类:

public void MyClass {
    private boolean someoneTouchedMeWhenIWasWorking;

    public void process() {
        someoneTouchedMeWhenIWasWorking = false;
        doStuff();
        synchronized(this) {
            if (someoneTouchedMeWhenIWasWorking) {
                System.out.println("Hey!");
            }
        }
    }

    synchronized public void touch() {
        someoneTouchedMeWhenIWasWorking = true;
    }
}

一个线程调用process,另一个调用touch。请注意process在启动时如何在没有同步的情况下清除标志。

使用Java内存模型,即使process稍后发生,运行touch的线程是否有可能看到其本地非同步写入的效果?

也就是说,如果线程按此顺序执行:

T1: someoneTouchedMeWhenIWasWorking = false; // Unsynchronized
T2: someoneTouchedMeWhenIWasWorking = true; // Synchronized
T1: if (someoneTouchedMeWhenIWasWorking) // Synchronized

...... T1是否有可能从本地缓存中看到值?它可以首先刷新它对内存的所有内容(覆盖T2编写的内容),然后从内存中重新加载该值吗?

是否需要同步第一次写入或使变量变为volatile?

如果答案得到文档或一些可敬的消息来源的支持,我将非常感激。

4 个答案:

答案 0 :(得分:3)

发生在关系( hb )之前的JLS原因。让我们使用JLS约定注释您提议的执行的读写:

T1: someoneTouchedMeWhenIWasWorking = false;   // w
T2: someoneTouchedMeWhenIWasWorking = true;    // w'
T1: if (someoneTouchedMeWhenIWasWorking)       // r

我们有以下发生在之前的关系:

  • hb(w,r),由于程序订单规则
  • hb(w',r),由于synchronized keyword(监视器上的解锁发生在该监视器上的每个后续锁定之前):如果您的程序在 r (看着你的手表)之前执行了 w',那么 hb(w',r)

您的问题是 r 是否可以阅读 w 。这在the last section of #17.4.5

中得到了解决
  

我们说允许变量 v 的读取 r 观察写 w v 如果,在执行跟踪的发生 - 部分顺序之前:

     
      在 w 之前没有订购
  • r (即,不是 hb(r,w)的情况),并且
  •   
  • 没有干预写 w' v (即没有写 w' v 这样 hb(w,w') hb(w',r))。
  •   
     

非正式地,如果在排序之前没有发生,则允许读取 r 查看写入 w 的结果以防止读取。

  • 明确满足第一个条件:我们确定 hb(w,r)
  • 第二个条件也是如此,因为 w w'
  • 之间没有发生在之前的关系

因此,除非我遗漏了某些内容,否则 r 似乎理论上可以观察 w w'

实际上,正如其他答案所指出的那样,典型的JVM以更严格的方式实现,JMM和我认为任何当前的JVM都不允许 r 观察者 w ,但这不是保证。

无论如何,使用volatile或在synchronized块中包含 w ,问题很容易解决。

答案 1 :(得分:3)

  

使用Java内存模型,即使稍后触摸,线程运行进程是否有可能看到其本地,非同步写入的效果?

我在这里看到一个微妙但重要的过度简化:你认为你会如何确定第二个线程的写操作恰好发生在第一个线程的写入和读取操作之间?为了能够说明这一点,您需要假设写入操作是时间线上的理想化点,并且所有写入都按顺序发生。在实际硬件上,写入是一个非常复杂的过程,涉及CPU流水线,存储缓冲区,L1缓存,L2缓存,前端总线,最后是RAM。在所有CPU内核上同时进行相同类型的过程。那么,一个写“发生在另一个之后”究竟是什么意思呢?

此外,考虑"Roach Motel"范例,这似乎可以帮助许多人作为Java内存模型后果的“心理捷径”:您的代码可以合法地转换为

public void process() {
    doStuff();
    synchronized(this) {
        someoneTouchedMeWhenIWasWorking = false;
        if (someoneTouchedMeWhenIWasWorking) {
            System.out.println("Hey!");
        }
    }
}

这是一种完全不同的方法,可以利用Java内存模型提供的自由来获得性能。 if子句中的读取确实需要读取实际的内存地址,而不是直接内联false的值并因此删除整个if-block(这实际上会在没有synchronized的情况下发生),但是false的写入不一定会写入RAM。在推理的下一步中,优化编译器可以决定完全删除对false的赋值。根据代码的所有其他部分的具体情况,这是可能发生的事情之一,但还有许多其他可能性。

要取消上述内容的主要信息应该是:不要假装你可以使用一些简化的“本地缓存失效”概念来推断Java代码;而是坚持官方的Java内存模型并考虑它实际提供的所有自由。

  

是否需要同步第一次写入或使变量变为volatile?

考虑到上述讨论,我希望你意识到这个问题实际上没有实质内容。您将观察另一个线程的写入,从而保证观察它在写入之前所做的所有其他操作,或者您将不会观察它并且不会有保证。如果您的代码没有数据竞争,那么任何结果都适合您。如果它有数据竞争,那么最好修复它而不是试图修复你的习语。

最后,想象一下你的变量是不稳定的。那会给你的是什么,你现在没有?就JMM形式主义而言,两个写入将在它们之间有一个明确的顺序(由总同步顺序强加),但是你将处于与现在完全相同的位置:明确的顺序将是任意在任何特定的执行中。

答案 2 :(得分:1)

process()中的synchronized块确保拾取主存储器的最新值。因此代码是安全的。

这是volatile变量的一个很好的用例。 " someoneTouchedMeWhenIWasWorking"至少应该是变数。

在您的示例代码中,synchronized用于保护" someoneTouchedMeWhenIWasWorking"这只不过是一面旗帜。如果它被设为易失性,那么"同步"块/方法可以删除。

答案 3 :(得分:1)

要解释更新值的时间,请让我引用此JSR-133 FAQ

  

但是,除了相互排斥之外,还有更多的同步。同步确保线程在同步块之前或期间的内存写入以可预测的方式显示给在同一监视器上同步的其他线程。在我们退出synchronized块之后,我们释放了监视器,它具有将缓存刷新到主内存的效果,因此该线程所做的写操作对其他线程是可见的。在我们进入同步块之前,我们获取监视器,它具有使本地处理器高速缓存无效的效果,以便从主存储器重新加载变量。然后,我们将能够看到前一版本中显示的所有写入。

因此,当一个线程退出someoneTouchedMeWhenIWasWorking块时,synchonized的值将被发布到主内存。然后在输入synchronized块时从主存储器读取已发布的值。

synchronized块之外,可以缓存该值。但是当进入同步块时,它将从主内存中读取,并在退出时将被发布,即使它未被声明为volatile


这里有一个关于这个问题的有趣答案:What can force a non-volatile variable to be refreshed?

接下来是对话:http://chat.stackoverflow.com/rooms/46867/discussion-between-gray-and-voo

请注意,在最后一个示例中,@ ThoririosDelimanolis尝试执行:synchronized(new Object()) {}在while(!stop)块中,但它没有停止。他补充说,synchronized(this)确实如此。我认为这种行为在FAQ中说明了:

  

同步确保线程在同步块之前或期间的内存写入以可预测的方式显示给在同一监视器上同步的其他线程