当前位置: 首页 > 知识库问答 >
问题:

易失性变量和刷新/读取主存

孔和畅
2023-03-14

官方记录说

写入易失性字段与监视器释放具有相同的记忆效果,从易失性字段读取与监视器获取具有相同的记忆效果。

有效地,挥发性的语义学得到了实质性的加强,几乎达到了同步的水平。为了可见性的目的,挥发性字段的每次读取或写入都像“半”次同步。

从这里开始。

这是否意味着,对volatile变量的任何写入都会使执行线程将其缓存刷新到主存中,而每次从volatile字段读取都会使线程从主存重新读取其变量?

我之所以这样问,是因为同一文本包含了这一声明

重要提示:请注意,两个线程访问同一个volatile变量非常重要,以便正确设置发生之前的关系。线程A写入易失性字段f时对其可见的所有内容在读取易失性字段g后对线程B可见,这种情况并非如此。释放和获取必须“匹配”(即,在同一易失性字段上执行)才能具有正确的语义。

这句话让我很困惑。我确信,对于使用synchronized语句的常规锁获取和释放,这是不正确的-如果某个线程释放了任何监视器,那么它所做的所有更改对所有其他线程都是可见的(更新:实际上不是真的-查看最佳答案)。在stackoverflow上甚至有一个关于它的问题。然而,有人指出,无论出于何种原因,挥发性字段的情况并非如此。我无法想象的是,在保证之前的任何实现都不会使更改对其他线程可见,这些线程不读取相同的易失性变量。至少想象一个实现,它与前两个引号不矛盾。

此外,在发布这个问题之前,我做了一些研究,比如这篇文章,其中包含这句话

执行这些指令后,所有写操作都可以通过缓存系统或主内存对所有其他线程可见。

所提到的指令是在写入volatile字段时发生的指令。

那么这个重要的音符是什么意思呢?还是我错过了什么?或者那张便条完全错了?

答复

在做了更多的研究之后,我只能在官方文件中找到关于挥发性字段及其对非挥发性字段变化的影响的声明:

使用易失性变量降低了内存一致性错误的风险,因为对易失性变量的任何写入都与同一变量的后续读取建立了先发生后发生的关系。这意味着对易失性变量的更改始终对其他线程可见。更重要的是,这还意味着当线程读取易失性变量时,它不仅会看到对易失性的最新更改,还会看到导致更改的代码的副作用。

从这里开始。

我不知道这是否足以得出结论,在关系被保证之前,只有读取相同volatile的线程才会发生这种情况。所以现在我只能总结一下,结果是不确定的。

但在实践中,我建议考虑线程A在写入易失性字段时所做的更改,只有在线程B读取相同的易失性字段时,才保证线程B可见。上面引用的官方来源强烈暗示了这一点。

共有2个答案

程和畅
2023-03-14

这是否意味着,对volatile变量的任何写入都会使执行线程将其缓存刷新到主存中,而每次从volatile字段读取都会使线程从主存重新读取其变量?

不,这并不意味着。这样想是一个常见的错误。这意味着Java内存模型中指定了什么。

在英特尔CPU上,有刷新缓存线的指令:clflush和clflushopt,并且在任何时候发生易失性写操作时,对整个缓存线进行这种刷新都是非常低效的。

为了提供一个示例,让我们看看volatile变量是如何通过

Java(TM) SE Runtime Environment (build 1.8.0_171-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.171-b11, mixed mode)

为了我的哈斯韦尔。让我们写一个简单的例子:

public static volatile long a = 0;

public static void main(String[] args){
    Thread t1 = new Thread(() -> {
        while(true){
            //to avoid DCE
            if(String.valueOf(String.valueOf(a).hashCode()).equals(String.valueOf(System.nanoTime()))){
                System.out.print(a);
            }
        }
    });

    Thread t2 = new Thread(() -> {
        while(true){
            inc();
        }
    });

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

public static void inc(){
    a++;
}

我禁用了分层编译,并使用C2编译器运行它,如下所示:

java -server -XX:-TieredCompilation -XX:+UnlockDiagnosticVMOptions -XX:CompileCommand=print,*Volatile.inc -jar target/test-0.0.1.jar

输出如下:

  # {method} {0x00007f87d87c6620} 'inc' '()V' in 'com/test/Volatlee'
  #           [sp+0x20]  (sp of caller)
  0x00007f87d1085860: sub     $0x18,%rsp
  0x00007f87d1085867: mov     %rbp,0x10(%rsp)   ;*synchronization entry
                                                ; - com.test.Volatlee::inc@-1 (line 26)

  0x00007f87d108586c: movabs  $0x7191fab68,%r10  ;   {oop(a 'java/lang/Class' = 'com/test/Volatlee')}
  0x00007f87d1085876: mov     0x68(%r10),%r11
  0x00007f87d108587a: add     $0x1,%r11
  0x00007f87d108587e: mov     %r11,0x68(%r10)
  0x00007f87d1085882: lock addl $0x0,(%rsp)     ;*putstatic a
                                                ; - com.test.Volatlee::inc@5 (line 26)

  0x00007f87d1085887: add     $0x10,%rsp
  0x00007f87d108588b: pop     %rbp
  0x00007f87d108588c: test    %eax,0xca8376e(%rip)  ;   {poll_return}
  0x00007f87d1085892: retq
  ;tons of hlt ommited

因此,在这个简单的示例中,易失性编译为locked指令,要求缓存行具有要执行的独占状态(可能会发送读取无效信号到其他内核,如果不是)。

闻人修平
2023-03-14

您从一个完全错误的角度看待这个问题。首先,您引用了JLS,而不是谈论flush,这将是该规范的实现细节。您绝对唯一需要依赖的是JLS,其他任何东西都不是坏事,但不能证明任何形状或形式的规范是对是错。

你犯错误的根本原因是:

我可以肯定的是,这不是真的为定期锁获取。。。

实际上,在x86上,您可能是对的,但JLS和oracle官方教程要求:

当线程释放内在锁时,该操作与同一锁的任何后续获取之间建立了先发生后发生的关系。

为后续操作建立了之前发生的操作(如果需要,请阅读两个更简单的操作)。一个线程释放锁,另一个线程获取锁-这是后续的(release-acquire-semantics)。

相同的事情发生在易失性-一些线程写入它,当一些其他线程观察到通过后续读取写入时,会发生在之前。

 类似资料:
  • 我已经阅读了许多相互矛盾的信息(msdn,SO等),关于易失性和VoletleRead(ReadAcquireFence)。 我理解这些限制的内存访问重新排序含义——我仍然完全搞不清楚的是新鲜度保证——这对我来说非常重要。 msdn doc用于挥发性提及: (…)这样可以确保字段中始终存在最新的值。 挥发性字段的msdn文档提到: 对易失性字段的读取称为易失性读取。易失性读取具有“获取语义”;也就

  • 在阅读了这个问题和这个(尤其是第二个答案)之后,我对volatile及其关于记忆障碍的语义感到非常困惑。 在上面的例子中,我们写入一个易失性变量,这会导致一个mitch,这反过来会将所有挂起的存储缓冲区/加载缓冲区刷新到主缓存,使其他缓存行无效。 然而,非易失性字段可以优化并存储在寄存器中,例如?那么,我们如何才能确保给定一个写入易失性变量之前的所有状态变化都是可见的呢?如果我们有1000件东西呢

  • 所有的中断函数都能正常工作,但是过程函数却让我很生气。 我会感激任何我没注意的把戏。

  • 它几乎是线程安全的单例的正确实现。我看到的唯一问题是: 初始化字段的可以在完全初始化之前发布。现在,第二个线程可以在不一致的状态下读取。 但是,在我看来,这只是问题。这是唯一的问题吗?(并且我们可以使不稳定)。

  • 问题内容: 我尝试了解为什么此示例是正确同步的程序: 由于存在冲突的访问(存在对a的写入和读取),因此在每个顺序一致性中,必须在访问之间的关系之前执行。假设顺序执行之一: 是1发生-在2之前发生,为什么? 问题答案: 不,在相同变量的易失性写入之前(以同步顺序),在易失性写入 之前 不一定 会发生 易失性读取。 这意味着它们可能处于“数据争用”中,因为它们“冲突的访问未按先发生后关系进行排序”。如

  • 我想澄清发生前关系是如何与不稳定变量一起工作的。让我们有以下变量: 和线程A: 和线程B: 根据Java内存模型(JMM),以下语句是否正确?如果不是,正确的解释是什么? 总是发生-之前 在JMM中发生在之前,只有当它实际发生在时间之前 在JMM中发生在之前(并且将被可预测地分配)如果实际发生在