【问题标题】:Volatile example from Java Language Specification returning surprising behaviorJava 语言规范中的可变示例返回令人惊讶的行为
【发布时间】:2013-11-14 12:18:36
【问题描述】:

我想亲自尝试Java lang spec example,但显然有些东西我不明白。我的理解是 volatile 计数器的递增顺序应该与代码中显示的顺序相同。 “令人惊讶的是”我得到一个随机计数器值,因为一个计数器有时小于、等于和大于另一个。 有没有人可以解释我错过了什么?

下面的代码和输出:

public class C {

private static volatile int i = 0;
private static volatile int j = 0;

static void one() {
    i++;
    j++;
}

static void two() {
    int a = i;
    int b = j;
    if(a < b)
        System.out.println(a + " < " + b);
    if(a > b)
        System.out.println(a + " > " + b);
    if(a == b)
        System.out.println(a + " = " + b);
}

public static void main(String[] args) throws Exception {
    Thread t1 = new Thread(new Runnable() {
        @Override
        public void run() {
            while(true)
                one();
        }
    });
    Thread t2 = new Thread(new Runnable() {
        @Override
        public void run() {
            while(true)
                two();
        }
    });

    t1.start();
    t2.start();

    Thread.sleep(5000);

    System.exit(0);
}
}

输出

214559700 > 214559699
214559807 > 214559806
214559917 > 214559916
214560019 = 214560019
214560137 > 214560136
214560247 = 214560247
214560349 > 214560348
214560455 = 214560455
214560561 > 214560560
214560670 = 214560670
214560776 = 214560776
214560886 > 214560885
214560995 = 214560995
214561097 < 214561098

【问题讨论】:

  • 有什么令人惊讶的?递增总是按顺序发生,但这并不意味着将值复制到ab 或打印总是在这两个值都递增之后发生。你能澄清一下你预计会发生什么吗?
  • 要记住的另一件事......即使增量也不是“原子的”。如果你有两个线程同时调用 i++ 1000 次,你可能会发现执行完成时 i 不是 2000,而是少了一些值。
  • 感谢您指出这一点!

标签: java volatile jls


【解决方案1】:

对于所有这三种情况,假设我们从i = 0j = 0 开始... one() 和 two() 都可以执行两个操作,但是 1 和 2 之间的顺序是不确定的:

a == b

two() loads i (a = 0)
two() loads j (b = 0)
one() increments i (i = 1)
one() increments j (j = 1)

a > b

one() increments i (i = 1)
two() loads i (a = 1)
two() loads j (b = 0)
one() increments j (j = 1)

a (较罕见)

two() loads i (a = 0)
one() increments i (i = 1)
one() increments j (j = 1)
two() loads j (b = 1)

【讨论】:

  • 谢谢格伦!这是有道理的!
【解决方案2】:

对 i 和 j 的访问是不同步的,所以,以下所有情况都可能发生:

  • t2 读取 i,t2 读取 j,t1 写入 i,t1 写入 j
  • t2 读取 i,t1 写入 i,t2 读取 j,t1 写入 j
  • t1 写入 i,t2 读取 i,t2 读取 j,t1 写入 j
  • t1 写入 i,t2 读取 i,t2 读取 j,t2 读取 i,t2 读取 j,t1 写入 j
  • 等。等

【讨论】:

    【解决方案3】:

    当其中一个线程刚刚完成递增i并想要开始递增j时,另一个线程可能已经设置了int的ab,从而导致不同的值。

    volatile 与我认为您认为的用途不同。本质上,volatile 用于指示变量的值将被不同的线程修改。 volatile 修饰符保证任何读取字段的线程都会看到最近写入的值

    声明一个 volatile Java 变量意味着:

    • 这个变量的值永远不会被缓存在线程本地:所有的读写都将直接进入“主内存”;
    • 对变量的访问就像它被包含在 synchronized 块中一样,在自身上同步。

    Source and more explanation.

    当您想从不同的线程而不是不同的成员访问相同的 volatile 成员时,volatile 的使用变得更加清晰。

    【讨论】:

      【解决方案4】:

      请务必注意,线程可以以多种方式交错。由于您将ij 复制到ab,您可以对ij 进行修改 复制iaj 变成 b。例如,这种跟踪可以解释您的输出:

      一些初始状态,然后复制

      • 背景:i == 214559700, j == 214559699
      • T2: a = i, b = j

        214559700 > 214559699
        

      递增 7 次,然后递增——复制! ——精神

      • T1: (i++ j++) (x7)
      • T1: i++
      • T2: a = i, b = j

          214559807 > 214559806
        
      • T1: j++

      递增 9 次,然后递增——复制! ——精神

      • T1: (i++ j++) (x9)
      • T1: i++
      • T2: a = i, b = j

        214559917 > 214559916
        
      • T1: j++

      递增2次,然后复制

      • T1: (i++ j++) (x2)
      • T2: a = i, b = j

        214560019 = 214560019
        

      【讨论】:

      • 感谢 Joshua 的准确/实用回答!我可能没有正确地问我的问题。 “令人惊讶”的是,我可以观察到 a 的值小于 b 的值。规范中有:cite"
      • 规范:“这允许方法一和方法二同时执行,但保证访问 i 和 j 的共享值的次数和顺序完全相同,因为它们似乎发生在每个线程执行程序文本期间。因此,j 的共享值永远不会大于 i 的共享值,因为对 i 的每次更新都必须在更新 j 发生之前反映在 i 的共享值中。然而,任何给定的方法 2 调用都可能观察到更大的 j 值......"
      • @user2991939 是的,ij 的值总是使得i ≥ j,但是,正如你所引用的,“任何给定的方法二调用都可能观察到 j 那要大得多”。文本明确指出方法二可能会看到一个更大j(并因此存储在b中的值),这意味着a的值更小。我希望上面的解释/记录显示了这是如何发生的,即递增和复制是如何交错的。
      【解决方案5】:

      如果我们先阅读j

      static void two() {
          int b = j;
          int a = i;
      

      那么保证a>=b。

      【讨论】:

        最近更新 更多