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

piztneat  于 2023-02-28  发布在  Java
关注(0)|答案(8)|浏览(121)

我大致理解了volatile在Java中的含义,但是阅读Java SE规范www.example.com时,我在理解某个volatile示例下面的文本时遇到了问题。8.3.1.4 I have a problem understanding the text beneath that certain volatile example.

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的共享值。因为在对j的更新发生之前,对i的每次更新必须反映在i的共享值中。然而,有可能的是,方法二的任何给定调用可能观察到比对i观察到的值大得多的j的值,因为方法一可能在方法二获取i的值的时刻和方法二获取j的值的时刻之间被执行多次。
怎么样
j不大于i
,但同时
方法二的任何给定调用可能观测到远大于观测到的i的值的j的值
??
看起来很矛盾。
运行示例程序后,我得到j大于i。那么为什么要使用volatile呢?没有volatile时,它给出的结果几乎相同(i也可以大于j,这是规范中前面的示例之一)。为什么在这里用这个示例来替代synchronized

jxct1oxe

jxct1oxe1#

在任一时刻,j都不大于i
这与方法2观察到的不同,因为它在不同的时间访问变量ij。首先访问i,然后稍晚访问j
这不是同步版本的直接替代方案,因为行为不同。与不使用volatile的一个区别是,如果不使用volatile,0的值总是可以打印出来。增量不需要是可见的。
这个例子演示了volatile访问的顺序。一个需要这样做的例子可能是这样的:

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设置的值而不是旧值。

bsxbgnwa

bsxbgnwa2#

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

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

第一示例的新奇在于,由于更新重排序,即使在首先观察到j * 时,j也可以大于i *。
最后一个例子现在非常有意义,只是对解释做了一些小的修改(括号中的修改和注解):
这允许方法one和方法two被并发执行,但是保证对ij的共享值的访问发生的次数和顺序与它们在每个线程执行程序文本期间出现的次数和顺序完全相同。j的共享值从未[观察到]大于i的共享值,因为在对j的更新发生之前,对i的每次更新必须反映在i的共享值中。然而,方法two的任何给定调用可能观测到比观测到的[j]的值大得多的[ i]的值,因为在方法two获取[ j ]的值的时刻和方法two获取[ j]的值的时刻之间,方法one可能被执行多次x1米20英寸1x英寸]。
这里的关键点是,当使用volatile时,第二次更新永远不会在第一次更新之前被观察到。关于两次读取之间差距的最后一句完全是括号,并且ij被交换以符合错误的示例。

s2j5cfk0

s2j5cfk03#

我认为该示例的重点是强调在使用volatile时需要小心并确保顺序;该行为可能是违反直觉的,并且该示例证明了这一点。
我同意有关的措辞有点含糊,可以就多个个案提供更明确和清晰的例子,但这并不矛盾。
共享值是同一时刻的值。如果两个线程在完全相同的时刻读取i和j的值,则j的值永远不会大于i。volatile保证读取和更新的顺序与代码中相同。
然而,在该示例中,打印+ i+ j是两个不同的操作,这两个操作被任意时间量分隔开;因此,由于在读取i之后和读取j之前,j可以被更新任意次数,所以可以观察到j大于i。
使用volatile的意义在于,当您以正确的顺序并发地更新和访问volatile变量时,您可以做出没有volatile原则上不可能实现的假设。
在上面的示例中,two()中的访问顺序不允许有把握地得出哪个变量大于或等于。
但是,请考虑样本是否更改为System.out.println("j=" + j + " i=" + i);
在这里,你可以自信地Assertj的打印值永远不会大于i的打印值。没有volatile,这个假设成立,原因有二。
首先,更新i和j可以由编译器和硬件以任意顺序执行,并且实际上可以作为j执行;i。如果您从其他线程在j++之后i++之前访问j和i,则可以观察到j=1i=0,而不管访问顺序如何。volatile保证不会发生这种情况,并且它将按照源代码中写入的顺序执行操作。
第二,volatile保证另一个线程可以看到另一个线程更改的最新值,只要它在上次更新后的稍后时间点访问它。没有volatile,就无法假设观察到的值。理论上,该值可以永远保留给另一个线程零。程序可以打印两个零,零和一个任意数字,等等,从过去的更新;在其他线程中观察到的值可能小于更新器线程在更新后看到的当前值。volatile保证您将在第一个线程更新后在第二个线程中看到该值。
虽然第二种保证看起来是第一种保证(顺序保证)的结果,但它们实际上是正交的。
关于synchronized,它允许执行一系列非原子操作,如作为原子操作的i++;j++,例如,如果一个线程同步i++;j++,另一个线程同步System.out.println("i=" + i + " j=" + j);,则第一个线程可以不执行递增序列,而第二个线程打印,结果将是正确的。
但这是有代价的。首先,synhronized本身就有性能损失。其次,更重要的是,并不总是需要这样的行为,被阻塞的线程浪费时间,降低了系统吞吐量(例如,你可以做这么多的i++;j++;系统输出期间)。

oxiaedzo

oxiaedzo4#

为什么j永远不会大于i?
假设你只执行one()一次,在执行这个方法的过程中,i总是在j之前递增,因为递增操作是一个接一个地发生的。如果你并发执行one(),每个单独的方法调用都将等待执行队列中的其他方法完成向i或j写入它们的值。这取决于当前执行的方法试图递增的变量。因此,所有对i的写入一个接一个地发生,并且所有对j的写入一个接一个地发生。并且由于在方法主体本身内i在j之前递增,在给定时刻,j永远不会大于i。
方法二的任何给定调用可能观察到j的值比观察到的i的值大得多,这是怎么做到的?
如果在后台执行one()方法,同时调用two(),在读取i和读取j之间,方法1可能会执行多次。因此,当读取i的值时,可能是在t=0时调用one()的结果。然后,当读取j的值时,它可能是稍后发生的one()调用的结果,例如在t=10时。因此,在println语句中,j可能大于i
为什么使用volatile代替synchronized?
我不会列出所有应该使用volatile而不是synchronized块的原因,但请记住volatile只保证对特定字段的原子访问,而不保证对未标记为synchronized的代码块的原子执行。j++}不同步,因此它 * appearly *(我使用appearly是因为它不完全相同,但看起来很相似)给出了与不使用volatile关键字相同的结果。

ve7v8dk2

ve7v8dk25#

怎么样
j不得大于i
,但同时
方法二的任何给定调用可能观察到j的值远大于i的观察值
??
第一条语句在程序执行的任何给定时刻总是为真,第二条语句可能在程序执行的任何给定间隔为真。
写入volatile变量时,在它必须对其他线程可见之前,会同时写入它和所有内容(至少在Java5+中是这样的。不过,对于在此之前的Java版本,解释并没有太大变化)因此,i的增量 * 必须 * 在j递增时可见,这意味着对于其它线程,j永远不会显得大于i
然而,不保证ij的读取在程序执行中的单个时刻发生。ij的读取可能看起来与执行two()的线程彼此非常接近地发生,但实际上在读取之间可能已经经过了某个任意量的时间。例如,当i = 5j = 5时,two()可以读取i,但是随后在其他线程执行时被"冻结",将ij的值分别改变为例如2019。当two()恢复时,它从停止的地方开始读取jj现在的值为19two()不重新读取i,因为就它而言,执行中没有中断,因此不需要进行额外的工作。
那为什么要用volatile呢?
虽然volatilesynchronized都提供了可见性保证,但精确的语义略有不同。volatile保证对变量所做的更改将立即对所有线程可见,而synchronized保证在其块中所做的改变将对所有线程可见**,只要它们在同一锁上同步**。synchronized还提供了volatile不提供的额外原子性保证。
为什么在这里使用此示例作为synchronized的替代方法?
只有当one()由单个线程执行时,volatile才是synchronized的可行替代方案,这里就是这种情况。在这种情况下,只有单个线程写入ij,因此不需要synchronized提供的原子性保证。如果one()由多个线程执行,volatile无法正常工作,因为构成增量的读取-添加-存储操作必须以原子方式发生,而volatile无法保证这一点。

nuypyhwy

nuypyhwy6#

此程序确实保证方法two()遵守j >= i-1(不考虑溢出)。
如果没有volatilei,j的观测值可能到处都是。
该声明
j的共享值永远不会大于i的共享值
是非常非正式的,因为它的意思是"同时",这在JMM中不是一个定义的概念。
JMM的核心原则是关于"顺序一致性"的,JMM的驱动动机是
JLS#17-如果程序已正确同步,则该程序的所有执行将显示为顺序一致
在下面的程序中

void f()
{
    int x=0, y=0;
    x++;
    print( x>y );
    y++
}

x>y总是被看作true。如果我们遵循动作的顺序,它必须是这样。否则,我们真的没有办法对任何(命令式)代码进行推理。这就是"顺序一致性"。
"顺序一致性"是一个 * 观察到的 * 属性,它不必与"实际"操作一致(不管这意味着什么)。在x实际递增之前,JVM完全有可能将x>y计算为true只要JVM能够保证观察到的顺序一致性,它就能够以任何方式优化实际执行,例如乱序执行代码。
但这是针对单线程的,如果多个线程读写共享变量,这样的优化当然会完全破坏顺序一致性,我们不能通过考虑多个线程的交叉操作(同一线程中的操作遵循线程内的顺序)来推断程序行为。
如果我们想保证任何多线程代码的线程间顺序一致性,我们必须放弃为单线程开发的优化技术。这将对大多数程序造成严重的性能损失。而且这也是不必要的--线程间的数据交换相当少。
因此,创建了专门的指令,只是为了在 * 需要 * 时建立线程间顺序一致性,volatile读写就是这样的动作,所有volatile读写都遵守线程间顺序一致性,这样就保证了j >= i-1two()中。

sbdsn5lh

sbdsn5lh7#

这允许方法一和方法二被并发执行,但是保证对i和j的共享值的访问发生的次数和顺序与它们在每个线程执行程序文本期间出现的次数和顺序完全相同。因此,j的共享值永远不会大于i的共享值。因为对i的每次更新必须在对j的更新发生之前反映在i的共享值中。

  • 这似乎说明了方法一(),如果i和j没有被volatile修改,它假设i ++在CPU-1中执行,但是值没有更新回物理内存单元,而j ++在CPU-2中执行,并且值立即更新回物理内存单元,然后我们在方法二中从内存中获得这两个值(),我们将看到i小于j
  • 但如果使用volatile修改,则i & update memory)就像捆绑在一个操作中,如果++发生,CPU的下一条指令一定是将i的值更新回物理内存的操作,在这种情况下,我们假设方法一()不会在**"i ="+ i +"j ="+ j之间的间隙执行,那么我们尝试执行方法二,并从物理内存中检索i和j的值,我们将看到j的值永远不会大于i**。
cwdobuhd

cwdobuhd8#

所有的都取决于你如何使用它。在Java中volatile关键字被用作Java编译器和线程的一个指示符,它们不缓存这个变量的值,而是总是从主内存中读取它。所以如果你想共享任何一个变量,其中的读写操作是原子的,例如在一个int或布尔变量中读写,那么你可以将它们声明为volatile变量。
从Java 5开始,随着自动装箱、枚举、泛型和变量参数等重大变化,Java在Java内存模型(Java Memory Model,JMM)中引入了一些变化,这保证了从一个线程到另一个线程所做更改的可见性,也保证了“发生之前”的可见性,这解决了发生在一个线程中的内存写入可能“泄漏”并被另一个线程看到的问题。
Java volatile关键字不能与方法或类一起使用,它只能与变量一起使用。Java volatile关键字还保证了可见性和顺序,在Java 5写入任何volatile变量之后,在任何读取volatile变量之前。顺便说一下,使用volatile关键字还可以防止编译器或JVM重新排序代码或将它们从同步屏障中移走。
关于Java中Volatile关键字的要点
1.在Java中volatile关键字只能应用于变量,在类和方法中使用volatile关键字是非法的。

  1. Java中的volatile关键字保证volatile变量的值总是从主内存读取,而不是从Thread的本地缓存读取。
    1.在Java中,对于所有使用Java volatile关键字声明的变量(包括long和double变量),读写都是原子的。
    1.在Java中对变量使用volatile关键字可以降低内存一致性错误的风险,因为在Java中对volatile变量的任何写入都会与同一变量的后续读取建立happens-before关系。

相关问题