提问人:omar santaella 提问时间:4/17/2023 最后编辑:Charliefaceomar santaella 更新时间:4/18/2023 访问量:85
关于 java 中的 volatile 关键字的问题,csharp。为什么大多数来源都说对主内存而不是共享缓存进行易失性更新?
Question about volatile keyword in java, csharp. Why do most sources say volatile updates to main memory rather than the shared cache?
问:
根据消息来源的不同,我听到了不同的事情。有人说易失性更新到共享缓存,但大多数人说它更新到 ram(主内存)并从 ram 读取。显然,如果您正在从 ram 编写和读取,这将严重破坏性能。鉴于现代处理器具有共享的多核缓存,写入变量是否只更新本地缓存,而不更新共享缓存?voltile 是否在缓存级别纠正了这个问题,或者正如大多数人所说的那样,它是否依赖于主内存?
如果变量未在共享缓存级别更新,是否有技术原因导致这种情况发生?从共享缓存中更新和读取似乎比从主内存中更新和读取具有更高的性能。
我正在尝试理解更好的多线程代码和性能影响。没有遇到任何问题。
答:
volatile
主要是用于编译 ASM 的语言功能。它所做的几乎只是强制写入内存地址而不是寄存器,它不决定是否缓存该内存地址或缓存在何处。实际写入可能是全局或本地缓存行,也可能是主内存。
它还可能引入写入屏障,以防止指令重新排序。它还强制写入是原子的。
它不执行任何涉及缓存行的操作。CPU 内核管理其本地缓存线,并确保它们始终与其他本地缓存线、全局缓存线和主内存保持一致。这里一致并不意味着它们都是一样的。这意味着在任何时候,内核都会知道特定位置是否有效,可以读取或写入,因此特定内存地址的真实来源在哪里。
评论
这不是java语言的工作方式。Java-the-Language 有一个官方规范,该规范不是针对特定处理器编写的。
控制文档是 Java 内存模型。
JMM在保证方面起作用。通常,JMM的工作原理如下:
- 如果您执行某些操作,则 JVM 会保证这些可靠性。
- 否则。。。任何事情都可能发生。该规范根本没有解释或做出任何承诺。
这些东西被故意保持得非常抽象。因此,JVM 实现只需要确保第一个项目符号是有保证的,并且可以做任何它想做的事情。因此,像这样的东西只是保证不再可能从一个线程而不是另一个线程观察更新,以及 JVM 如何确保这种保证取决于 JVM。volatile
这就是 java 可以高效的原因。
下面是一个简单的示例:
JMM保证以下几点:
有一个“发生之前”原因的枚举列表,每个条目都高度规范。任何 2 行(从技术上讲,任何 2 字节码指令)要么具有 HB 关系,要么没有 HB 关系,您可以确定,因为它是指定的。假设我们有 Bytecode1 和 Bytecode2,并且 bc1 与 bc2 具有 Happens-Before 关系。
JVM 保证在该上下文中,当 BC2 执行时,不可能观察到 BC1 完成执行之前的状态。
这就是 JVM 所保证的全部内容。
例如,指定为相对于该线程方法中的第一行的 HB。因此,鉴于:someThread.start()
run()
class Test {
static long v = 10;
void test() {
v = -1;
new Thread(() -> {
System.out.println(v);
}).start();
}
}
上面的代码必须打印 -1。如果它打印 20,则该 JVM 有问题;如果你提交了关于它的错误,它将被接受。这是因为 是相对于 之前发生的,因此,能够观察到以前的状态是 JVM 规范保证不会发生的事情。v = -1;
.start()
v = -1;
相比之下:
class Test {
static long v = 10;
void test() {
new Thread(() -> {
Thread.sleep(1000);
System.out.println(v);
}).start();
v = -1;
}
}
这里的 JVM 规范说,打印“10”的 JVM 是合法的。打印“-1”的 JVM 也是合法的。抛硬币来确定这一点的 JVM 是合法的。一个 JVM,它决定每天在今天的每个单元测试中打印 -1,即使你运行它 10000 次,然后当你向那个重要客户演示时,它开始返回 10...那是合法的。
事实上,JVM 规范甚至说进行拆分读取是合法的(前 32 位都是 1 位,因为都是 1 位,而后 32 位仍然是 20,导致一些大的负数)。这是一个有趣的案例:你今天可能运行的 JVM 几乎不会被强制去实际生成拆分读取,因为谁仍然有完整的 32 位支出,即便如此,你通常也无法捕捉到“介于两者之间”的另一个线程。-1
但是,如果您编写的代码在发生拆分读取条件时会中断,则该代码有问题。如果一棵树倒在森林里......——如果你编写了一个不可能再触发的错误,这是一个错误吗?我不知道,问一个哲学家:)
整天提交关于这个的错误,它只会被拒绝:JVM工作正常,因为它遵守规范。
即使你把休眠时间延长到“14天”,让JVM运行那么长时间,它也可以打印出“10”,尽管这看起来很疯狂(14天前它被设置为-1!),但有些JVM确实可以做到这一点。
volatile
在确切的保证方面有点复杂,它也适合这个 Happens-Before 框架(本质上,任何与易失性字段交互的 2 字节码指令,其中至少一个是写入指令,建立 HB,但要知道哪一个是 Happens-Before 可能有点棘手)。
JVM 是如何实现这一点的?这取决于 JVM。如果有一种有效的方法来提供这种保证,它就会做有效的事情。
这正是 JVM 被如此抽象地指定的原因。想象一下,规范从字面上阐明:确保对字段的任何写入都将立即刷新到主 RAM。volatile
然后,如果有一种更快的方法来确保线程间的一致性,例如,仅将页面从一个内核直接刷新到另一个内核(想象一下存在一个处理器可以做这样的事情),或者所有内核共享一个缓存级别,而您只能刷新到那里 - 那么 JVM 无法使用这些功能,因为一些善意的白痴决定对 Java 语言规范中的某些显式 CPU 过于具体公文。
有关大多数 JVM 当前如何实现在使用时保证某些行为的要求的细分,请参阅 @Charlieface 的回答。但要意识到,这个答案实际上并不是 java lang 规范以任何方式保证的;JLS 甚至没有提到“缓存行”这个词。volatile
评论
volatile
volatile
Java 语言规范 (JLS) 几乎没有提到“主内存”或“缓存”。它严格地描述了变量的行为,即它如何限制不同线程中事件的顺序。
JLS的描述有点枯燥和学术。(在第 17 章中出现的任何位置都搜索“挥发性”。但大多数情况下,它告诉你的是;无论线程 A 在写入某个字段之前执行什么操作,都必须在线程 B 读取同一字段并获取 A 写入的值时对另一个线程 B 可见。volatile
volatile
f
f
评论
volatile