Semghh

jdk9 内存顺序模型

2024-12-24

本文谈一下对 Java Memeory Mode 的理解。

1. Java Memory Modes

术语Java Memeory Mode (在jdk9中被 doug lea 提及) ,这个术语通常用于描述 java 并发模型。

在jdk9中,提供了一套 VarHandle API , 为高级的Java程序员提供了一种控制并发的方式。

提供了一些控制原语。 也可以认为是一些“控制保证”。 程序员可以利用这些”保证“来编写出符合业务逻辑的,安全的,效率的工具。

1.1 Varhandle API 的介绍

我们需要使用 MethodHandles.lookup() 来获得 VarHandle变量。

//TODO

1.2 并发的背景

“并发”总是会出现一种令人意想不到的现象,它总是难以捕捉,例如经典的 i++ 问题。

导致这种现象的可能原因有:

任务并行

  • 任务并行。 对于单核处理器来说,两个线程分别执行操作A和B。 那么只有简单的A先于B 或 B先于A 。但是对于多核来说,两个操作可能是无序的(或许是同时进行)

内存并行

  • 内存并行。除了任务并行(多核并行)以外,内存并行仍然可能导致问题。内存可能同时被多个设备代理(通常是缓存), 变量不是由”唯一”的物理设备表示(这将会带来强制刷新到内存、失效缓存行等问题)。

    除此以外,处理器一次性能够操作内存的宽度也会引起问题(例如32位cpu读取long类型),他们不总是原子的操作。

指令并行

  • 指令并行。 (cpu的流水线作业)cpu包含了多个部件,多个部件是协同工作的。cpu指令总是以叠加的方式处理指令。 因此同一时间可能会处理多个cpu指令。

1.3 更多的模式

处理上述 “并行”的概念和技术逐渐成熟, 相同的方法经常出现在不同的编程语言中。

“没有银弹!” 在众多的规则和模型中,没有一个对所有程序中所有代码都有意义。

多核的经验表明,需要更多的模式(mode)来处理常见的并发编码问题。如果没有他们,一些程序员可能会过渡的使用 同步代码,这会让程序变得更慢。

而一些程序员通过使用非标准操作,在特定的JVM和处理上 绕过限制实现了这些行为,虽然可行但会导致移植性较差。

新的 Memeory Order Mode (内存顺序模型),具有积累效果(约束越来越强,性能则递减)。

从最弱到最强:

plain -> Opaque -> Release/Acquire -> Volatile

其中plain和 Volatile 与jdk9之前兼容。

plain

最简单的 =操作,对没有volatile修饰的变量操作,就是plain,可能是 plain read 或 plain write。例如:

public class Foo {
    public int value;
}

public void plainRead(Foo f){
    final int v = f.value; 
}

public void plainWrite(Foo f){
    f.value = 5;
}

plain 没有任何额外的语义,没有任何额外的保证。

JVM 规范保证,在单线程内,无论指令如何重排,总是保证plain的直观表现:

例如 ` d = (a + b) * (c + b);`

所有的load都可能提前启动 , 指令示意图:

 load a, r1 | load b, r2 | load c, r4 | load b, r5
 add r1, r2, r3 | add r4, r5, r6
 mul r3, r6, r7
 store r7, d

但最终仍会表现得正确。

Opaque Mode

ValueHandle 提供了2个api getOpaque() / setOpaque()

比Plain 模式添加了额外的约束, 这些约束提供了 “线程间访问” 中 “变量的”最小感知”。

当使用Opaque(或更强的模式)时,提供保证:

  • 一致性: 保证了变量严格的读写顺序。保证了 依赖于先读然后写的操作, 或 写操作后再读的一致性。

    换句话说就是不会破坏 RMW(读-修改-写)的关系。

  • 持续: 写操作只保证最终可见。

    换句话说,不保证其他线程立即可见。但最终一定会可见。

  • bitwise 原子性 : 如果使用 Opaque (或更强的模式)访问,读取所有的数据类型,包括(long ,double) 在bitwise尺度上都是原子性的(保证一次完整读取所有bit),不会混合读取多次并发写入的bit(不会出现前一半儿bit是A操作写入,后一半儿bit是B写入,导致数据组合在一起是无意义的)。

release/acqiure 模式

通过 VarHandle.setRelease() VarHandle.getAcquire() api来实现 release/acquire 模式。并且比Opaque提供了积累(拥有Opaque的全部约束保证)的约束:

  • 在线程T内, 如果一个访问操作A 源码上在 Release(或更强的模型)写操作W 之前。那么在本地线程内,A操作在W之前。

  • 在线程T内,如果 acquire(或更强的模式)读操作R 在源码上在访问操作A之前。 那么acquire R操作先发生,A访问后发生。

RA(release/acquire)模式是因果一致性系统的主要思想。 因果性在大多数的通信形式上是必不可少的。例如,如果我做好了晚饭,我告诉你已经做好了晚饭,你听到了我的声音,那么你可以确定晚餐一定存在。 在听到ready 时,你一定能访问到正确的dinner

例如:

 volatile int ready; // 初始化的值是0 ,使用 VarHandle REDAY 引用该变量
 int dinner;         // mode does not matter here  
 
 
 Thread 1                   |  Thread 2 
 dinner = 17;               |  if (READY.getAcquire(this) == 1)  //如果看到了 ready =1
 READY.setRelease(this, 1); |    int d = dinner; // 那么一定保证看到dinner=17

如果看到了Release操作做出的改变,那么在Release之前的编码的操作一定执行完毕。

在生产者消费者设计、消息传递设计中,需要 RA模式的保证。

RA栅栏

你可以使用api风格更明确的 RA模式:

先调用 VarHandle.releaseFence() ,然后使用 VarHandle.setOpaque()来代替setRelease() 。如果你的变量是 bitwise原子性的,那么甚至可以用 plain 来代替。

同样的,可以使用 VarHandle.acquireFence() 插入acquire屏障,然后逐步用opaque、plain 代替acquire

例如:

//Thread 1:

dinner = 17;
VarHandle.releaseFence();//R屏障
ready = 1;

Thread 2:
VarHandle.acquireFence();
if(ready==1)
    int d = dinner;  //此处一定能看到dinner=17

但是 releaseFence() 可能比 setRelease() 提供更多的额外约束 :

releaseFence是一个栅栏,将前面的所有访问和 后面的所有写入分开, ` releaseFence()` 保证Fence之前的所有访问都完成,再执行后续的写入操作。

提供一个简单的验证程序

public class MainForFences {

    public volatile int ready;

    public int dinner;

    public static VarHandle varHandle;
    static {

        try {
            varHandle = MethodHandles.lookup()
                    .findVarHandle(MainForFences.class,"ready",int.class);
        } catch (NoSuchFieldException | IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }

    public static void main(String[] args) throws ClassNotFoundException, InterruptedException {


        int a=0;
        int b=0;

        for (int i = 0; i < 10000000; i++) {
            if (i%10000==0){
                System.out.println(a);
                System.out.println(b);
            }
            int callable = callable();
            if (callable==0) a++;
            if (callable==19) b++;

        }
        System.out.println(a);
        System.out.println(b);
    }

    private static int callable() throws InterruptedException {
        MainForFences mainForFences = new MainForFences();

        AtomicInteger res = new AtomicInteger();


        Thread t1 = new Thread(() -> {
            mainForFences.dinner = 19;
            VarHandle.releaseFence();
            mainForFences.ready=1;
        });


        Thread t2 = new Thread(() -> {
            while (true) {
                VarHandle.acquireFence();

                if (mainForFences.ready == 1) {
                    res.set(mainForFences.dinner);
                    break;
                }
            }
        });

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        return res.get();
    }
}

参考

https://www.lenshood.dev/2021/01/27/java-varhandle/#opaque

https://gee.cs.oswego.edu/dl/html/j9mm.html#summarysec


Similar Posts

下一篇 定时器的实现

Comments