Java volatile 是不是会阻止缓存或强制执行直写缓存?

Posted

技术标签:

【中文标题】Java volatile 是不是会阻止缓存或强制执行直写缓存?【英文标题】:Does Java volatile prevent caching or enforce write-through caching?Java volatile 是否会阻止缓存或强制执行直写缓存? 【发布时间】:2016-08-19 15:15:36 【问题描述】:

我正在尝试理解 Java 的 volatile 关键字与 写入 到具有 CPU 缓存的多线程程序中的易失性原子变量有关的内容。

我已经阅读了一些教程和 Java 语言规范,尤其是 section 17.4.5 on "happens-before ordering"。我的理解是,当一个线程将新值写入 volatile 变量时,更新的值必须对读取该变量的其他线程可见。对我来说,这些语义可以通过以下两种方式之一实现:

    线程可以在 CPU 缓存中缓存 volatile 变量,但对缓存中变量的写入必须立即刷新到主内存。也就是说,缓存是write-through。

    线程永远不能缓存 volatile 变量,并且必须在主内存中读取和写入此类变量。

tutorial (http://tutorials.jenkov.com) 中提到了方法 1,它说:

通过声明计数器变量 volatile 所有写入计数器 变量将立即写回主存。

在 *** 问题“Volatile variable in Java”中提到了方法 2,还有这个 tutorial 说:

这个变量的值永远不会被线程本地缓存:all 读写将直接进入“主内存”

Java 中使用的正确方法是哪一种?

回答我的问题的相关 *** 问题:

Volatile variable in Java

Does Java volatile read flush writes, and does volatile write update reads

Java volatile and cache coherence

【问题讨论】:

两者都不是。它在分配点强制设置内存栅栏,这意味着对 volatile 变量的写入 和所有先前的写入 对其他线程可见,如果这些线程首先读取相同的 volatile 变量。如果没有读取相同的 volatile,则无法保证效果。 您在“方法 2”中引用的 SO 答案似乎是正确的:““Java 中的可变变量”看起来是正确的(晚餐开始了,但我只是快速阅读了一下)。您链接的教程在“方法 2”中似乎是废话。 【参考方案1】:

保证只是您在语言规范中看到的。从理论上讲,写入 volatile 变量可能会强制将缓存刷新到主内存,也可能不会,可能是随后的读取会强制缓存刷新或以某种方式导致在没有缓存刷新的情况下在缓存之间传输数据。这种模糊是故意的,因为它允许潜在的未来优化,如果更详细地说明 volatile 变量的机制,这些优化可能是不可能的。

在实践中,对于当前的硬件,这可能意味着在没有一致缓存的情况下,写入 volatile 变量会强制将缓存刷新到主内存。当然,有了一致的缓存,就不需要这样的刷新了。

【讨论】:

谢谢。所以这个答案是错误的,对吧? ***.com/a/6259755/4561314 "声明一个 volatile Java 变量意味着:这个变量的值永远不会被线程本地缓存:所有的读写都将直接进入'主内存'。"我想你的回答是说 volatile 变量可以被缓存。 嗯,我的“写回主存”部分有问题,@KookieMonster。我认为 Warren 是在说,在缓存一致的架构中,volatile 可能不会真正写入主内存(我同意)。 其实我也有同样的问题,所以问了英特尔。 According to them,“相干流量通过 QPI 链路传输 [QPI 链路] 使用 QPI 带宽的一小部分。”所以实际上并不总是涉及主存储器。 QPI is described on Wikipedia, fyi. @KookieMonster 你明白这意味着你错了,对吧?不需要涉及主内存,如果这样做,现代 CPU 会慢得多,因为主内存通常比内核之间的互连慢一个数量级。 缓存没有被“刷新”。在易失性写入的情况下发生的主要事情是 CPU 停止执行加载,直到存储缓冲区被耗尽。如果包含写入的缓存线未处于独占或修改状态,则会完成所有权请求,这将使任何其他 CPU 上的缓存线无效,并且一旦被授予,来自存储缓冲区的存储就可以提交给 L1D。没有刷新任何缓存。【参考方案2】:

在 Java 中,最准确的说法是,所有线程都会看到最近对 volatile 字段的写入,以及在该 volatile 读/写之前的任何写入。

在 Java 抽象中,这在功能上等同于从共享内存读取/写入的 volatile 字段(但这在较低级别上并不严格准确)。


比与 Java 相关的级别低得多;在现代硬件中,any and allany and all 内存地址的读取/写入总是发生在 L1 中并首先注册。话虽如此,Java 旨在向程序员隐藏这种低级行为,因此这仅在概念上与讨论相关。

当我们在 Java 中的字段上使用 volatile 关键字时,这只是告诉编译器在对该字段的读/写操作中插入称为内存屏障的东西。内存屏障有效地确保了两件事;

    读取此地址的任何线程都将使用最新的值(屏障使它们等待直到最近的写入使其返回共享内存,并且在此更新的值使到他们的 L1 缓存)。

    对任何字段的读/写都不能越过屏障(也就是说,它们总是在其他线程可以继续之前被写回,并且编译器/OOO 无法将它们移动到屏障之后的某个点)。

举一个简单的Java例子;

//on one thread
counter += 1; //normal int field
flag = true; //flag is volatile

//on another thread
if (flag) foo(counter); //will see the incremented value

本质上,当将flag 设置为true 时,我们创建了一个内存屏障。当 Thread #2 尝试读取该字段时,它会遇到我们的屏障并等待新值到达。同时,CPU 确保counter += 1 在新值到达之前被写回。因此,如果 flag == truecounter 将增加。


总结一下;

    所有线程都会看到 volatile 字段的最新值(可以粗略地描述为“通过共享内存进行读取/写入”)。

    对易失性字段的读取/写入与之前对一个线程上的任何字段的读取/写入建立之前发生的关系。

【讨论】:

什么内存操作返回一个old值?在什么 CPU 上?

以上是关于Java volatile 是不是会阻止缓存或强制执行直写缓存?的主要内容,如果未能解决你的问题,请参考以下文章

[UE4]缓存选项 Is volatile

C# volatile 变量:内存栅栏 VS。缓存

volatile修饰符

volatile和synchronized的区别

volatile 对可见性的保证并不是那么简单

volatile&synchronized&diff