5

没有数据竞争顺序一致性的先决条件。数据竞争是由访问冲突引起的。如果至少有一个访问是写入,则对同一变量的两次访问是冲突的。

请参阅下面的 JLS7 引用以供参考。

我理解这种情况的定义,其中一个操作是读访问,另一个操作是写访问。但是,如果对同一个变量有两个写操作,我不明白为什么需要它(对于内存模型)。

问题:在对同一个变量进行两次写入操作的情况下,保证顺序一致性的理由是什么,这不是按发生前关系排序的?


§17.4.1:[..]如果至少有一次访问是写入,则对同一变量的两次访问(读取或写入)称为冲突。

§17.4.5:[..]当一个程序包含两个冲突的访问,这些访问没有按照发生前的关系排序时,就说它包含一个数据竞争。[..]当且仅当所有顺序一致的执行都没有数据竞争时,程序才能正确同步。如果一个程序被正确同步,那么程序的所有执行将看起来是顺序一致的。

4

4 回答 4

3

例如,考虑long32 位架构上的变量。这将需要很长时间的两次写入。如果两个线程同时尝试,则有序列使变量处于不一致状态。

thread1:   write high bits                                  write low bits
thread2:                    write high bits, write low bits

这将导致线程 2 的高位和线程 1 的低位。

于 2013-09-01T15:55:00.670 回答
3

如果两个写访问不处于happens-before 关系中,则未指定哪个将最后发生,即哪个分配获胜。例如,程序

static int x;
static int y;

public static void main(String[] args) {
    Thread t1 = new Thread() {
        @Override public void run() {
            x = 1;
            y = 1;
        }
    };
    Thread t2 = new Thread() {
        @Override public void run() {
            y = 2;
            x = 2;
        }
    };
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(x + "" + y);
}

可能会打印 11、12、21 或 22,即使唯一的数据竞争发生在写入之间,并且无法通过顺序一致的执行获得 12。

于 2013-09-01T15:55:02.983 回答
2

直观地说,顺序一致性意味着多线程程序的执行看起来好像程序是按照原始程序顺序一次执行一条语句,即开发人员看到的代码中语句的实际顺序。顺序一致性是人们直觉地推理并发性的方式。

这里的重点是动词出现。事实上,编译器和 VM 可以自由地在后台执行许多优化,只要它们不破坏顺序一致性。

根据内存模型,只有正确同步的程序才会出现顺序一致。换句话说:如果一个程序没有正确同步,它在运行时的执行可能对应于您无法通过按原始程序顺序一次执行一条语句来实现的执行。

让我们考虑原始代码

T1        T2

a = 3     b = 5
b = 4     a = 6

顺序一致的执行可以是a=3,b=4,b=5,a=6, or a=3,b=5,a=6,b=4, or b=5,a=6,a=3,b=4or or a=3,b=5,b=4,a=6or b=5,a=3,b=4,a=6or b=5,a=3,a=6,b=4(所有可能的交错)

为了保证 JVM 中的顺序执行,您应该将四个分配中的每一个都包装在一个同步块中。否则,编译器和 VM 被授权进行可能破坏直观顺序一致性的优化。例如,他们可以决定将 T1 的语句重新排序为b=4,a=3。代码不会以原始程序顺序运行。通过这种重新排序,可能会发生以下执行:b=4,b=5,a=6,a=3,导致b=5,a=3. 顺序一致性无法达到此状态。

如果程序没有正确同步,可能会破坏顺序一致性的原因是优化考虑了单个线程的一致性,而不是整个程序。在这里,如果孤立地考虑,交换 T1 中的分配不会损害 T1 的逻辑。然而,它损害了线程 T1 和 T2 交错的逻辑,因为它们改变了相同的变量,即它们具有数据竞争。如果分配被包装到同步块中,那么重新排序将是不合法的。

您的观察中确实有一些事实,即如果您不读取堆,您实际上不会注意到发生的竞争。但是,可以安全地假设写入的任何变量也同时被读取,否则它没有任何用途。正如这个小例子应该举例说明的那样,写入不应该竞争,否则它们可能会破坏堆,这可能会在以后产生意想不到的后果。

现在,更糟糕的是,JVM 上的读取和写入并不是原子操作(读取和写入双打需要内存访问)。因此,如果他们竞争,他们可以破坏堆,不仅在它不一致的意义上,而且在它包含从未真正存在的价值的意义上。

最后,赋值表达式的结果是赋值发生后变量的值。所以x = ( y = z ) 是有效的。这假定写入没有与并发竞争竞争并返回写入的值。

简而言之:如果读写没有正确同步,就很难保证它们的效果。

于 2013-09-01T15:56:06.850 回答
1

请参阅http://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.4.5-500

我们希望两次写入具有发生前的关系,以便后面的写入可以影响前面的写入。考虑这个例子

hb(w1, r1), hb(w2, r1), hb(r1, r2), but not hb(w1, w2) or hb(w2, w1)

    w1   w2
     \   / 
      \ /
       |
       r1  // see w1 or w2
       |
       r2  // see w1 or w2

在顺序一致的执行中,r2 和 r1 必须看到相同的值。然而,JMM 被削弱以不保证这一点。因此,该程序没有“正确同步”。

如果hb(w1, w2) or hb(w2, w1)JMM 确实保证 r2 和 r1 看到相同的值。

       w1
       |
       w2
       |
       r1  // see w2
       |
       r2  // see w2

基本思想是将所有写入和读取链接在一个链上,以便每次读取都是确定性的。

PS 数据竞争的定义是错误的;永远不应将两个 volatile 操作视为数据竞争,请参阅Is volatile read occur-before volatile write?

于 2013-09-01T16:07:32.280 回答