无法理解Java规范中volatile的示例

时间:2016-12-07 20:09:13

标签: java volatile specifications

我对Java volatile的含义有了一般的了解。但阅读 Java SE Specification 8.3.1.4我在理解某个易变的示例下的文字时遇到了问题。

class Test {
    static volatile int i = 0, j = 0;
    static void one() { i++; j++; }
    static void two() {
        System.out.println("i=" + i + " j=" + j);
    }
}
  

这允许方法一和方法二同时执行,但是   保证访问i和j的共享值   与它们出现的次数完全相同,顺序完全相同   在每个线程执行程序文本期间发生。   因此,j的共享值永远不会大于i的共享值,   因为对i的每次更新都必须反映在i的共享值中   在更新到j之前发生。但是,任何给定的都是可能的   方法二的调用可能会观察到j的值很多   大于i观察到的值,因为方法一可能是   在方法二取出之间执行多次   i的值和方法二取值j的时刻。

怎么样

  

j永远不会超过我

,但同时

  

方法二的任何给定调用都可能会观察到j的值   远远大于我观察到的值

...

看起来像矛盾。

运行示例程序后,我j大于i。为什么要使用volatile呢?如果没有volatile,它也会得到几乎相同的结果(i也可能大于j,这是规范中的前一个示例之一)。为什么此示例可以替代synchronized

7 个答案:

答案 0 :(得分:5)

在任何时候,j都不会超过i

这与两个观察的方法不同,因为它在不同时间访问变量ij。首先访问i,然后稍后访问j

这不是同步版本的直接替代品,因为行为不同。不使用volatile的一个区别是没有volatile,可以始终打印0值。增量不需要显示。

该示例演示了易失性访问的顺序。需要这个的例子可能是:

volatile boolean flag = false;
volatile int value;

// Thread 1
if(!flag) {
    value = ...;
    flag = true;
}

// Thread 2
if(flag) {
    System.out.println(value);
    flag = false;
}

和线程2读取线程1设置的值而不是旧值。

答案 1 :(得分:3)

我认为这个例子的重点是强调在使用volatile时需要注意并确保顺序;这种行为可能是违反直觉的,这个例子证明了这一点。

我同意这里的措辞有点模糊,有可能为多个案例提供更明确,更明确的例子,但没有矛盾。

共享值是同一时刻的值。如果两个线程在完全相同的时刻读取i和j的值,则永远不会观察到j的值大于i。 volatile保证保持读取和更新的顺序,如代码中所示。

但是,在示例中,打印+ i+ j是两个不同的操作,以任意时间分隔;因此,j可以被观察到大于i,因为它可以在读取i之后和读取j之前被更新任意次数。

使用volatile的意义在于,当您以正确的顺序同时更新和访问volatile变量时,您可以做出原则上不可能没有volatile的假设。

在上面的示例中,two()中的访问顺序不允许以置信度结束哪个变量大于或等于。

但是,请考虑样本是否已更改为System.out.println("j=" + j + " i=" + i);

在这里你可以断言,j的打印值永远不会大于i的打印值。由于两个原因,这个假设在没有volatile的情况下保持。

首先,更新i ++和j ++可以由编译器和硬件以任意顺序执行,实际上可以作为j ++; i ++执行。如果从其他主题开始,您可以在j++之后i++之前访问j和i,您可以观察j=1i=0,无论访问顺序如何。 volatile保证不会发生这种情况,并且它将按照源中编写的顺序执行操作。

其次,volatile保证另一个线程将看到另一个线程更改的最新值,只要它在上次更新后的稍后时间点访问它。没有挥发性,就没有关于观察值的假设。从理论上讲,价值可以永远留在另一个线程零。程序可以从过去的更新中打印两个零,零和任意数字等;其他线程中的观察值可能小于更新程序线程在更新后看到的当前值。 volatile保证在第一个更新后你会看到第二个线程中的值。

虽然第二个保证可能看起来是第一个保证(订单保证)的结果,但它们实际上是正交的。

关于synchronized,它允许执行一系列非原子操作,例如i++;j++作为原子操作,例如如果一个线程同步i++;j++而另一个线程同步System.out.println("i=" + i + " j=" + j);,则第一个线程可能不会执行增量序列,而第二个线程打印并且结果将是正确的。

但这需要付出代价。首先,同步化本身具有性能损失。其次,更重要的是,并不总是需要这样的行为,并且被阻塞的线程浪费时间,降低了系统吞吐量(例如,您可以在System.out期间执行许多i ++; j ++;)。

答案 2 :(得分:3)

我想提出这是一个错误,示例应该在j之前打印i

static void two() {
    System.out.println("j=" + j + " i=" + i);
}

第一个示例中的新颖性是,由于更新重新排序,j即使在首次观察时也可能大于i

最后的例子现在很有意义,对解释进行了一些小的修改(括号中的编辑和评论):

  

这允许方法one和方法two同时执行,但保证对ij的共享值的访问次数完全相同,并且以完全相同的顺序,因为它们似乎在每个线程执行程序文本期间发生。因此,j的共享值永远不会[观察到]大于i的共享值,因为i的每次更新都必须反映在i的共享值中在更新j之前发生。但是,方法two的任何给定调用都可能会观察到[i]的值远远大于[j]的值,因为方法{{ 1}}可能会在方法one获取[two]的值与方法j获取[two]值的时刻之间执行多次

这里的关键点是,在使用i时,在第一次更新之前永远不会观察到第二次更新。关于两次读取之间差距的最后一句话完全是括号,并且volatilei被交换以符合错误的例子。

答案 3 :(得分:2)

  

j怎么永远不比我大?

假设您只执行一次one()。在执行此方法期间,i总是在j之前递增,因为递增操作一个接一个地发生。 如果同时执行one(),则每个单独的方法调用将等待执行队列中的其他方法完成将其值写入i或j,具体取决于当前正在执行的方法尝试递增的变量。因此,对i的所有写入都是一个接一个地发生,并且对j的所有写入都是一个接一个地发生。并且因为在方法体本身内,我在j之前递增,在给定的时刻,j将永远不会大于i。

  

方法二的任何给定调用可能会观察到j的值远远大于为i观察到的值,如何?

如果您在调用one()时在后台执行方法two(),则在读取i然后读取j的时间之间,方法可以被执行多次。因此,当读取i的值时,可能是在时间t = 0时发生one()的某些调用的结果,并且当读取j的值时,它可能是调用稍后发生的one()的结果,例如在t = 10时。因此,在j语句中,i在此情况下可能大于println

  

为什么使用volatile来代替synchronized?

我不会列出任何人应该使用volatile而不是synchronized块的所有原因。但请记住,volatile仅保证对该特定字段的原子访问,并且不能确保未标记为synchronized的代码块的原子执行。在这个例子中,对i和j的访问是同步的,但整体操作{i ++; j ++}没有同步,因此它显然是(我明显使用它,因为它不完全相同,但看起来相似)与不使用volatile关键字的结果相同。

答案 4 :(得分:2)

  

怎么样

     
    

j永远不会超过i

  
     

,但同时

     
    

方法二的任何给定调用都可能会观察到j的值大于>>大于i

的值   
     

...

在程序执行的任何给定的时刻中,第一个语句始终为true;对于程序执行中的任何给定 interval ,第二个语句可能为true。

当写入一个volatile变量时,在它必须对其他线程可见之前写入它和所有内容(至少在Java 5+中。对于之前的Java版本,解释并没有太大变化,尽管)。因此,i 的增量必须在j递增时显示,这意味着j永远不会超过i到其他i线程。

ji的读取不能保证在程序执行中的某个时刻发生。 jtwo()的读取似乎与执行two()的线程彼此非常接近,但实际上在读取之间可能已经过了一些任意的时间。例如,i可能会在i = 5j = 5时读取i,但在其他线程执行时会被“冻结”,从而更改j和{的值{1}}分别代表2019。当two()恢复时,它会从中断处继续读取j,其值现为19two()没有重新阅读i,因为就其而言,执行没有中断,因此无需进行额外的工作。

  

为什么要使用volatile?

虽然volatilesynchronized都提供了可见性保证,但精确的语义略有不同。 volatile保证对变量所做的更改将立即对所有线程可见,而synchronized保证其块中所做的更改将对所有线程可见,只要它们在同一线程上同步锁定即可。 synchronized还提供了volatile没有的其他原子性保证。

  

为什么这个例子可以替代同步?

仅当volatile由单个线程执行时,

synchronized才是one()的可行替代方案,这就是这里的情况。在这种情况下,只有一个线程写入ij,因此不需要synchronized提供的原子性保证。如果one()由多个线程执行,volatile将不起作用,因为组成增量的read-add-store操作必须以原子方式发生,volatile不能保证。< / p>

答案 5 :(得分:2)

此程序确保方法two()观察j >= i-1(不考虑溢出)。

如果没有volatilei,j的观察值可能会到处都是。

声明

  

j的共享值永远不会大于i的共享值

非常非正式,因为它意味着&#34;同时&#34;,这不是JMM中定义的概念。

JMM的核心原则是关于顺序一致性&#34;。 JMM的动力是

  

JLS#17 - 如果程序正确同步,那么程序的所有执行都将显示为顺序一致

在以下程序中

void f()
{
    int x=0, y=0;
    x++;
    print( x>y );
    y++
}
始终会将{p> x>y视为true。必须是,如果我们遵循一系列行动。否则,我们真的无法推理任何(命令性)代码。这是&#34;顺序一致性&#34;。

&#34;顺序一致性&#34;是观察到的属性,它不必与&#34;真实&#34;行动(无论那意味着什么)。在x>y实际递增(或根本不加)之前,JVM完全有可能将true评估为x。只要JVM可以保证观察到的顺序一致性,它就可以优化实际执行,例如,不按顺序执行代码。

但这是针对单线程的。如果多个线程正在读/写共享变量,那么这样的优化当然会完全破坏顺序一致性。我们不能通过考虑来自多个线程的交错动作来解释程序行为(在线程内序列之后的同一线程中的动作)。

如果我们想保证任何多线程代码的线程间顺序一致性,我们必须放弃为单线程开发的优化技术。对于大多数程序来说,这将会导致严重的性能损失。它也是不必要的 - 线程之间的数据交换相当罕见。

因此,创建特殊指令只是为了在需要时建立线程间顺序一致性。易失性读写就是这样的行为。所有易失性读写都遵循线程间顺序一致性。在这种情况下,它保证j >= i-1中的two()

答案 6 :(得分:-1)

所有这些都取决于您如何使用它。 Java中的volatile关键字用作Java编译器和Thread的指示符,它不缓存此变量的值并始终从主内存中读取它。因此,如果您想通过实现共享任何读取和写入操作都是原子的变量,例如读取和写入int或boolean变量,然后您可以将它们声明为volatile变量。

从Java 5以及Autoboxing,Enum,Generics和Variable参数等主要更改中,Java引入了Java内存模型(JMM)的一些变化,它保证了从一个线程到另一个线程所做的更改的可见性,同时也发生了#34; -before&#34;它解决了在一个线程中发生的内存写入问题,可以通过&#34;泄漏并被另一个线程看到。

Java volatile关键字不能与方法或类一起使用,它只能与变量一起使用。 Java volatile关键字还保证可见性和排序,在Java 5写入任何volatile变量之后发生任何读入volatile变量之前。顺便说一句,使用volatile关键字也会阻止编译器或JVM重新排序代码或将它们从同步障碍中移开。

Java中的Volatile关键字的重点

  1. Java中的volatile关键字只是对变量的应用,而使用带有类和方法的volatile关键字是非法的。

  2. Java中的volatile关键字保证volatile变量的值将始终从主内存中读取,而不是从Thread的本地缓存中读取。

  3. 在Java中,读取和写入对于使用Java volatile关键字声明的所有变量都是原子的(包括长变量和双变量)。

  4. 在变量中使用Java中的volatile关键字可以降低内存一致性错误的风险,因为在Java中对volatile变量的任何写入都会建立与之后读取同一变量的先发生关系。