为什么类 jsr166e.Striped64.Cell 中的值字段需要额外填充?

sil*_*nce 6 java concurrency jvm

在作为 JSR166 的一部分引入的类中,作者使用所谓的填充来填充 Striped64.Cell 类的单值字段。

以下是该课程的摘录:

/**
 * Padded variant of AtomicLong supporting only raw accesses plus CAS. The value field is placed
 * between pads, hoping that the JVM doesn't reorder them.
 * <p/>
 * JVM intrinsics note: It would be possible to use a release-only form of CAS here, if it were
 * provided.
 */
static final class Cell {
  volatile long p0, p1, p2, p3, p4, p5, p6;
  volatile long value;
  volatile long q0, q1, q2, q3, q4, q5, q6;

  ...
Run Code Online (Sandbox Code Playgroud)

然后作者使用 CAS 原子地修改该值。

在 Striped64 类中,作者还使用 Unsafe 来访问其他两个字段,但没有应用任何此类填充。

我的问题是:为什么需要做这样的事情,引入 14 个冗余字段来填充单个值字段?

ass*_*ias 6

填充的目的是防止value共享字段的缓存行 - 否则可能必须从内存中重新获取该值,因为缓存行上的其他内容需要使整行无效。所以目标是提高性能。

为了让事情变得更简单,Java 8 引入了注释@Contended它在幕后做同样的事情,只不过它是由 JVM 本身处理的。

  • @silkentrance它将使用2个缓存线。如果只有 p0、p1...、p6、value,则很可能会在一个缓存行上得到 p0-p6,并带有一些其他数据,而 value 则位于另一行,并带有其他数据。 (2认同)

Eug*_*ene 6

虽然我确实同意阿西利亚斯的回答,但我认为这需要一些解释。

为什么缓存未命中很重要?

因为从主存读取比从缓存读取要慢得多。如果您有一个需要经常使用的变量,将其缓存很重要。此外,如果此变量与其他变量共享相同的缓存,则可能会出现整个缓存行无效的情况。

考虑变量 1 与变量 2 位于同一缓存中的示例。变量1由线程1使用,变量2由线程2使用。因为它们位于同一缓存行上,所以如果变量 2 有更新并且线程 1 需要使用变量 1,则需要删除缓存行(即使它不使用该变量!)并从主内存读取。这就是所谓的虚假分享

为什么实际上还有 7 个多头?

如果仅当 JVM 不决定重新排序内存时,那么从哪里开始读取此变量并不重要(您可以从 8 个“缓存行”中的第三个读取它) - 您将最终在缓存行中仍然只有一个值。因此,无论您从何处开始读取,缓存行中都只有一个对您重要的值,因此不可能出现“错误共享”的“缓存未命中”。

PS 这就是为什么 Java 对象的大小可以被 8 整除。