Java - 发生在监视器解锁之前的关系

时间:2014-02-18 13:02:53

标签: java multithreading memory-model jls happens-before

我最近阅读了http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html,它清楚地描述了许多Java内存模型的内在函数。一个特别的摘录引起了我的注意:

The rule for a monitorexit (i.e., releasing synchronization) is that 
actions before the monitorexit must be performed before the monitor is released.

对我来说很明显,但是在阅读http://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html并且发生之前 - 在定义之前,我发现关于监视器解锁的所有内容都是当一个线程解锁监视器发生 - 之前另一个线程再次锁定它(这也很有意义)。有人可以解释JLS如何解释在解锁操作之前必须发生同步块内所有操作的明显情况吗?

进一步的评论:

基于几个回复,我想写下你们所说的话的进一步评论:

  1. 在单线程中重新加载
  2. 我引用来源的几个“真理”:

    a = new A()
    

    如果new A()涉及一百个操作,然后将堆上的地址分配给a,编译器可以简单地重新排序那些以将堆地址分配给a,然后按照惯例初始化(双重检查锁定的问题)

    synchronized{
        a = 5;
    }
    print a;
    

    可以更改为

    synchronized{
        a = 5;
        print a;
    }
    

    我们使用print语句重新排序monitorexit(根据JLS也有效)

    现在,我提到了一个简单的案例:

    x = 1;
    y = 2;
    c = x + y;
    print c;
    

    我认为没有理由阻止编译器先分配x或先分配x。完全没有什么能阻止它,因为不管x是先分配还是y分配,最终输出都不变。所以重新排序是完全可能的。

    1. monitor.unlock
    2. 基于打印语句被“拉入”同步块的示例,让我们尝试将其反转,即启动

      synchronized{
          a = 5;
          print a;
      }
      

      我可以期待编译器这样做:

      synchronized{
          a = 5;
      }
       print a;
      

      在单线程世界中似乎完全合理, YET 这绝对是无效的,并且针对JLS(根据引用的来源)。现在为什么会这样,如果我在JLS中找不到任何关于此的内容?显然,关于“程序顺序”的动机现在无关紧要,因为编译器可以进行重新排序,例如将语句“拉入”同步块。

2 个答案:

答案 0 :(得分:2)

这不仅仅是synchronized块内执行的所有操作,它还指的是monitorexit之前该线程的所有操作。

  

有人可以解释JLS如何解释所有的明显条件   同步块内的动作必须在 - 之前发生   解锁操作?

对于特定线程(并且只有一个线程),无论synchronized如何,所有操作都会维护程序顺序,因此看起来好像所有读取和写入都按顺序发生(我们不需要在发生之前进行排序)单线程案例)。

before-before关系考虑了多个线程,即在monitorexit之前的一个线程中发生的所有操作在连续monitorenter 之后对所有线程都可见。

编辑以解决您的更新问题。

编译器必须遵循特定规则才能重新排序。在这种情况下的具体问题在可以重新排序网格中进行了演示here

特别有用的条目是

  • 第一个操作:正常加载(加载a;打印a)
  • 第二项操作:监控退出

此处的值为,这意味着编译器无法重新排序两个操作,其中第一个是正常加载,第二个是monitorexit,因此在您的情况下,此重新排序将违反JLS。

有一个称为roach-motel排序的规则,即读/写可以重新排序到同步块中,但不能从中排除。

答案 1 :(得分:1)

也许你错过了this(§17.4.5):

  

如果x和y是同一个线程的动作,并且x在程序顺序中位于y之前,那么hb(x,y)。

结合您已经知道的发生在之前的事情,应该清楚这意味着解锁操作之前的所有操作都将对其他线程可见。

关于你对这个问题的补充,如果你这样写:

synchronized {
    a = 5;
    b = 3;
}

并且编译器发出:

synchronized{
    a = 5;
}
b = 3;

然后违反了上面引用的规定:现在b = 3锁定释放之前不会发生。这就是为什么它是非法的。 (请注意,使用print a的示例并不具有指导性,因为它只涉及读取+副作用,而这些副作用不易用简单变量描述。)