如何理解JDK9内存模型?

问题描述

我正在学习 JDK9 内存模型。

看完演讲后 Java Memory Model Unlearning Experience 和阅读论文 Using JDK 9 Memory Order Modes

我对一些概念感到困惑。

  1. 不透明是否能立即保证可见性

  2. 如何理解论文中的偏序全序

对于第一个问题,论文说

使用等待变量值的空自旋几乎从来都不是一个好主意。使用 Thread.onSpinWait、Thread.yield 和/或阻塞同步来更好地应对“最终”可能是很长时间的事实,尤其是当系统上的线程数多于内核数时。

所以如果我写代码

// shared variable I and VarHandle I_HANDLE which referred to I
public static int I = 0;

public static final VarHandle I_HANDLE;

// Thread-1
I_HANDLE.setopaque(1);

// Thread-2
while((int) I_HANDLE.getopaque() == 0){
}

线程 2 最终终止,但可能会在很长时间后终止?

如果是这样,是否有任何最小方法可以保证线程 2 立即看到线程 1 的修改? (发布/获取?易变?)

解决方法

没有“立即”更新之类的东西。甚至电力也以有限的速度移动。通常,要求在特定时间跨度内可感知的效果就像要求操作的特定执行时间。两者都无法保证,因为它们是底层架构的属性,JVM 无法更改。

实际上,JVM 开发人员当然会尝试尽可能快地进行操作,而对您而言,作为程序员,最重要的是,关于更新的线程间可见性,没有比不透明写入更快的替代方法。更强的访问模式不会改变更新可见的速度,它们会为读取和写入的重新排序添加额外的约束。

因此,在您的示例中,只要架构和系统负载允许1,更新就会变得可见,但不要询问实际数字。没有人能说需要多长时间。如果您需要时间量方面的保证,则需要一种特殊的(“实时”)实现,它可以为您提供 Java 内存模型之外的额外保证。


1 举一个实际场景:线程 1 和 2 可能会竞争同一个 CPU。线程 1 写入值并在任务切换之前继续运行操作系统特定的时间(甚至不能保证线程 2 是下一个)。这意味着在写入后,挂钟时间和线程 1 的进度可能会流逝相当长的时间。当然,其他线程也可能同时在其他 CPU 核上取得很大进展。但也有可能在线程 1 提交写入之前线程 2 的轮询是线程 1 没有机会写入新值的原因。这就是为什么你应该用 onSpinWaityield 标记这样的轮询循环,让执行环境有机会防止这种情况发生。请参阅 this Q&A 以了解有关两者之间差异的讨论。

,

简单来说,不透明意味着读取或写入将会发生。所以它没有被编译器优化掉。

它不提供关于其他变量的任何排序保证。

因此,它非常适合用于性能计数器,其中 1 个线程执行更新,其他线程读取它。

但是如果你会做以下(伪)

// global
final IntReference a = new IntReference();
final IntReference b = new IntReference();

void thread1(){
    a.setPlain(1);
    b.setOpaque(1);
}

void thread2(){
    int r1 = b.getOpaque();
    int r2 = a.getPlain();
    if(r1 == 1 && r2 == 0) println("violation");
}

那么可能是因为:

  • a,b 的商店重新排序
  • 来自 a 和 b 的负载被重新排序。

但是,如果您使用存储释放和加载获取,则不会发生重新排序,因为释放和获取提供了相对于其他变量的排序约束。

void thread1(){
    a.setPlain(1);
    [StoreStore] <--
    [LoadStore]
    b.setRelease(1);
}

void thread2(){
    int r1 = b.getAcquire();
    [LoadLoad] <---
    [LoadStore]
    int r2 = a.getPlain();
    if(r1 == 1 && r2 == 0) println("violation");
}