Java语言规范中返回惊人行为的易失性示例

Java语言规范中返回惊人行为的易失性示例,java,volatile,jls,Java,Volatile,Jls,我想亲自尝试一下,但显然有些东西我不明白。我的理解是,易失性计数器的递增顺序应该与代码中显示的顺序相同。“令人惊讶的是”我得到了一个随机计数器值,其中一个计数器有时比另一个计数器小、相等、大。 有人能解释我遗漏了什么吗 代码和输出如下所示: public class C { private static volatile int i = 0; private static volatile int j = 0; static void one() { i++; j++; }

我想亲自尝试一下,但显然有些东西我不明白。我的理解是,易失性计数器的递增顺序应该与代码中显示的顺序相同。“令人惊讶的是”我得到了一个随机计数器值,其中一个计数器有时比另一个计数器小、相等、大。 有人能解释我遗漏了什么吗

代码和输出如下所示:

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);
}
}
公共C类{
私有静态volatile int i=0;
私有静态volatile int j=0;
静态void one(){
i++;
j++;
}
静态空二(){
int a=i;
int b=j;
if(ab)
系统输出打印项次(a+“>”+b);
如果(a==b)
系统输出打印项次(a+“=”+b);
}
公共静态void main(字符串[]args)引发异常{
线程t1=新线程(新的可运行线程(){
@凌驾
公开募捐{
while(true)
一个();
}
});
线程t2=新线程(新可运行(){
@凌驾
公开募捐{
while(true)
二();
}
});
t1.start();
t2.start();
睡眠(5000);
系统出口(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
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

需要注意的是,线程可以以多种方式交错。由于您正在将
i
j
复制到
a
b
中,因此可以在将
i
复制到
a
j
复制到
b
之间对
i
j
进行修改。例如,这种跟踪可以解释您的输出:

一些初始状态,然后复制
  • 背景:i==214559700,j==214559699
  • T2:a=i,b=j

    214559700 > 214559699
    
      214559807 > 214559806
    
    214559917 > 214559916
    
    214560019 = 214560019
    
递增7次,然后递增-复制-装饰
  • T1:(i++j++)(x7)
  • T1:i++
  • T2:a=i,b=j

    214559700 > 214559699
    
      214559807 > 214559806
    
    214559917 > 214559916
    
    214560019 = 214560019
    
  • T1:j++
递增9次,然后递增-复制-装饰
  • T1:(i++j++)(x9)
  • T1:i++
  • T2:a=i,b=j

    214559700 > 214559699
    
      214559807 > 214559806
    
    214559917 > 214559916
    
    214560019 = 214560019
    
  • T1:j++
增加2倍,然后复制
  • T1:(i++j++)(x2)
  • T2:a=i,b=j

    214559700 > 214559699
    
      214559807 > 214559806
    
    214559917 > 214559916
    
    214560019 = 214560019
    

对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
  • 等等等等

当其中一个线程刚刚完成增量
i
并希望开始增量
j
时,另一个线程可能已经设置了int的
a
b
,从而导致不同的值

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

声明易失性Java变量意味着:

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


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

对于这三种情况,假设我们从
i=0
j=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)
one() increments i (i = 1)
two() loads i (a = 1)
two() loads j (b = 0)
one() increments j (j = 1)
a>b

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


如果我们先读
j

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

那么保证a>=b.

什么是令人惊讶的?增量总是按顺序进行,但这并不意味着将值复制到
a
b
或打印总是在这两个值都递增之后进行。你能澄清一下你到底期望发生什么吗?还有一件事要记住。。。即使增量也不是“原子的”。如果您有两个线程同时调用i++1000次,您可能会发现,当执行完成时,i不是2000,而是更少的值。感谢您指出这一点!感谢Joshua给出的准确/实用的答案!我可能没有问对我的问题。“令人惊讶”的是,我可以观察到a的值小于b的值。在规范中有:cite“Spec:”这允许方法1和方法2同时执行,但保证访问i和j的共享值的次数和顺序与每个线程在执行程序文本期间出现的次数相同。因此,j的共享值永远不会大于i的共享值,因为对i的每次更新都必须在对j的更新发生之前反映在i的共享值中。然而,方法2的任何给定调用都可能观察到一个更大的j值……”@user2991939对,
i
j
的值总是这样的
i≥ j
,但是,正如您所引用的,“方法2的任何给定调用可能会观察到一个更大的j值”。文本明确指出,方法2可能会看到a
j
(并且