有 True 和 False 共享,允许处理器交换高速缓存行。这怎么会有可见性问题?
如果共享允许核心查看彼此的缓存,那么它解决的问题是什么volatile?
或者你可以这样改写:为了防止在受控竞争状态下的数据泄漏,共享机制缺少什么?
共享可能有效,但它只适用于缓存中已经有这个变量的那些处理器,并且招募线程可以从内存中读取不再相关的数据,因为那些线程已经使用这个变量很长时间了时间变量 - 在最后一次卸载到内存后你是否设法改变了它的值?
也就是说,在第一次从内存中读取变量到第一次更改之间的间隔内,共享不起作用?(作为假设)。
更新: “如果缓存一致性协议要求处理器缓存将内存位置保持在一致状态,那么为什么我们需要 volatile,它的作用相同?”
虚假分享
False Sharing 是一个术语,用于描述当不同线程修改恰好在同一缓存行上的独立变量时出现不良性能下降的机制。
阅读Habr的精彩文章,其中描述了这些机制。简而言之,这是引用:
易挥发的
volatile 修饰符是引入该语言的 java 关键字,用于支持安全的多线程编程。它对读/写变量施加了一些额外的条件。了解有关 volatile 变量的三件事很重要:
volatile 变量的读/写操作是原子的。
一个线程将值写入 volatile 变量的操作结果对于使用该变量从中读取值的所有其他线程都是可见的。
volatile 关键字禁用处理器和/或编译器中的某些优化/排列。
那些。比较这些概念并不完全正确,因为 volatile 是执行安全多线程程序的词,而 false sharing 是描述性能下降的词。
观看 Alexey Shipilev 关于Java 内存模型(不仅如此)的精彩讲座,他在其中将所有内容都安排得井井有条。
如果您有任何疑问,我可以尝试通过更新我的答案来公开。
UPD:以下问题的答案。
链接:Oracle Essentials,规范
了解线程安全有两个方面很重要:(1) 执行控制和 (2) 内存可见性。第一个负责控制代码的执行(包括指令的顺序)和允许/禁止程序的某些块并发(concurrently/simultaneously)执行。第二个是什么内存操作对其他线程可见或不可见。这是因为每个处理器在处理器本身和共享内存之间都有多级缓存,所以运行在不同处理器内核上的线程由于处理器的本地缓存可以同时看到“不同的内存”。
同步的
使用
synchronized可以防止另一个进程获取同一对象上的监视器(或锁) ,从而防止并发(同时)执行包含在同步块中的代码。重要的是要注意同步会创建所谓的先行关系。这种关系允许已获取监视器的线程在获取和释放监视器之前“查看”另一个线程所做的所有更改。实际上,这将(大致)对应于处理器将在捕获监视器时更新缓存并在释放后写入内存的事实。这些操作相当长(相对)。易挥发的
volatile 的使用迫使人们使用程序的内存对变量执行操作,“绕过”处理器的缓存。当我们需要这个变量在不同线程中的可见性,但我们不关心访问这个变量的顺序时,这会很有用。同样在 32 位 java 上,当将变量声明为 volatile 时,long & double entry 变为 atomic。在新的 JSR-133 规范中(在 Java5 中),volatile 的语义得到了加强。对它施加了可见性规则和禁止某些编译器/jvm 优化的规则。
例子
挥发性 - 会有帮助
假设我们有某种不可变对象,许多线程都可以引用它,并且它们在计算中不断使用它。Volatile 非常适合这种情况。有必要让其他线程在声明新对象后立即开始使用它(此时我的意思是我们会将引用从现有对象更改为新构造的对象)。同时,我们不需要专门同步这个更新,重置缓存。
挥发性 - 无济于事
让我们以常规计数器为例:
自增操作是非原子的,由三个操作组成:读取、自增、写入。在此示例中,可能会出现以下情况:
结果,计数器中存储的不是值“2”,而是值“1”。在这种情况下,方法
update()或使用的同步AtomicInteger等将有所帮助。这超出了这个问题的范围。总结以上所有内容 - 当对象发生的所有操作都是“原子”时使用 volatile 变量,如第一个示例(对完全形成的对象的引用发生变化,记录来自一个线程)并且没有竞争对象的状态。
我会发布一些说明。
据我所知,这个问题本身听起来有点不同:“如果缓存一致性协议要求处理器缓存以一致的状态存储内存位置,那么为什么我们需要 volatile,它做同样的事情?”
首先,有两个不同层次的讨论。JLS 在 JVM 内部运行,缓存一致性协议仅存在于特定的处理器架构中。编译和运行 JVM 的体系结构上不必存在缓存一致性。,因此 JLS 使可选特性成为强制性的(事实上,该特性只不过是一致性,更多内容见下文)。我很确定 99% 以上的多核处理器现在都有这个协议,但是 Java 不能依赖任何不能保证的东西——所有 Java 应用程序都应该在所有架构上运行相同(除了与例如,可能存在不同路径的操作系统)。因此,JLS 几乎不得不引入这样一个概念,即使它存在于大多数系统上,因为即使 JVM 是用某种 python 实现的,它仍然必须按照与 on 相同的方式执行代码任何其他系统。
其次,如果我们采用维基百科的定义:
那么在这里你应该注意“内存位置”。Java 中有一些数据类型可以占用处理器运行的多个字 - 至少,当在 32 位操作系统上运行时,double 和 long 将分别占用两个字。如果我理解正确,那么在这样的系统上可能会出现以下情况:
在这种情况下,即使在严格缓存一致性的条件下,处理器也有权更新 double 的一半,因此线程有权看到垃圾而不是实际值。volatile 禁止这种情况,保证任何变量记录的原子性。
第三,除了直接“打铁”的问题外,编译器(间接)参与了代码的执行。我不知道这对现代 Java 有多适用,但是一个积极的编译器可以自由地应用以下优化:
寄存器永远不会更新——它与缓存完整性协议无关。同样,我不知道现有 Java 编译器的实际行为如何,但这个特定示例在 JLS 中列为不安全。
最后,volatile 的语义会干扰程序执行的顺序。JLS 需要满足以下条件:
只要满足这些条件,编译器、JVM 和处理器就可以自由移动表达式。如果我们采用以下代码
那么他完全有权变成
因为所有后续表达式仍然会看到相同的结果。在这个例子中,另一个已经看到
done = true的线程仍然可以从 中读取 0result。但是,如果声明done为volatile,那么写入result必须发生在写入之前true,done读取发生在写入true之后,这样可以保证监听线程的变化可见。这并没有消除在此期间结果中出现多个条目的可能性,它仅保证在读取结果时,它将具有与完成更新同时的值或较晚的值。更新
除了以上所有,还有一个搞笑的案例。坦率地说,Java 的臀部很大,更准确地说,是在这样的臀部上的 GC 执行时间。自然地,他们尝试在与应用程序并行工作的 GC 的帮助下处理这个问题。在这种情况下,其中一种策略是从正在清除的区域中疏散活动对象,以便简单地声明它是免费的以进行完全覆盖。在这种情况下,对象的两个副本(一个在旧地址,另一个 - 被疏散)可以同时存在于 JVM 中,这需要同步记录和读取。对于实施者来说幸运的是,JMM 不对常规读取做出任何承诺,因此大多数操作都可以从同步中释放出来,并且在某一时刻可能会发生所有写入都转到一个对象,而读取是从另一个对象完成的情况- 只要访问不同步。与上述所有示例一样,这与缓存一致性完全一致,但在应用程序运行时允许异常(出于相同的原因 - 缓存一致性在各个内存块、JVM - 对象和字段级别工作)。本段指的是 Shenandoah GC,这在第十次 java 中是预期的,但在其他情况下可以安全地预期这种射击腿的方式。
关于虚假分享,他们上面写的非常正确。
一般来说,有一个简单的规则,如果有一个作者和许多读者,volatile 非常适合。
如果有很多编写器,则需要原子操作或其他同步原语。