Loading

JDK9 - VarHandle小记

说在前面

在开始之前,有必要点明一下虽只字未提但贯穿全文的核心,从而知道我们使用某些API的目的是什么:VarHandle/Unsafe提供了比volatile关键字更弱的变量访问方式,合理地利用它们可以让我们程序可以在符合运行预期的话情况下提高性能,这里的“弱”指的是约束更少。

所谓约束,举个例子,我们在多线程共享变量时,完全不加同步/互斥约束的话可能会导致程序出错,但如果加的约束过头了,比如每个线程访问共享变量时都套一把大锁,又会极大降低并发量。

在此引用大神Doug Lea的一句话,摘自参考链接中的视频:

"Some programmers over synchronize code, which can make programs slow, some programmers under synchronize code, which can make programs wrong"

- Doug Lea

四种访问模式 + volatile

四种访问模式从弱到强依次为plain, opaque, release/acuqire。如果加上约束最强的volatile读写的话,应该算是五种访问模式,下面依次介绍。

  • plain:普通的变量访问,不保证可见性也不禁止重排序,在保证单线程最终一致性的情况下允许指令随意的重排序、最多同时32位可以原子访问

    // X为公共变量x的VarHandle对象,x初始化为0
    // Thread1
    while (X.get(this) != 1); // 永远观察不到x被赋值为1
    
    // Thread2
    X.set(this, 1)
    
  • opaque:保证了可见性、并且对于所有类型的访问都是原子的(比如64bits的long类型)。如何理解这个coherent呢,从现象上来说就是线程能观测到别的线程对共享变量的修改,比如上面例子中,opaque模式访问x最终会观测到1。(另外再提一嘴为什么这种模式命名为opaque,与transparent这个词比较来说:transparent表示计算机中存在这样的特性/功能,但是我们感知不到;opaque表示我们能感知到,也能去显式地使用,但是我们不知道内部机制,类似黑盒。)

  • release(write) / acuqire(read):禁止某些类型的重排序,release指的是写,acquire指的是读。在opaque的基础上,release write之前的访问都不会被重排序到release write之后,acquire read之后的访问都不会被排序到acquire read之前。

    // y为普通变量,X为公共变量x的VarHandle对象,x初始化为0
    // Thread1
    y=2;
    X.setRelease(this, 1); // 在运行这句之前,y已经被赋值为2
    
    // Thread2
    if (X.getAcquire(this)==1) {
      r1 = y; // 在运行这句之前,x已经保证为1
    }
    
  • volatile:volatile前后的访问都不能重排序,VarHandle支持volatile读写,与直接使用Java在语法层面提供的volatile关键字效果相同

Fences

相比绑定到某个变量进行访问,VarHandle还支持直接加访问屏障来实现相同效果,更加灵活。VarHandle提供了五种屏障,按从弱到强:

  • LoadLoadFence:fence前后的读不能重排
  • StoreStoreFence:fence前后的写不能重排
  • ReleaseFence:fence前的读写和fence后的写不能重排
  • AcquireFence:fence前的读和fence后的读写不能重排
  • FullFence:fence前后的读写不能重排

下面就举个例子看看访问模式和屏障的应用,以及什么时候该用谁:

// 分别用访问模式和屏障来实现:初始化x并赋值到全局变量,并且保证其它线程不会读到未完全初始化的x,即“赋值到全局变量”不会与“初始化x”重排

// 方案1. 使用访问模式实现
x.a = 1;
x.b = 2;
VarHandle.releaseFence();
globalSharedObject.x = x;

// 方案2. 使用屏障实现(X为globalSharedObject.x的VarHandle)
x.a = 1;
x.b = 2;
X.setRelease(globalSharedObject, x);

只有单个变量需要同步访问的话,使用访问模式API确实更加方便些,但如果是多个变量的话,直接用屏障则看起来更加方便(运行效率哪个更好未知):

// 初始化多个对象:x, y, z

// 方案1. 使用屏障实现
x.a = 1;
y.a = 1;
z.a = 1;
VarHandle.releaseFence();
globalSharedObject.x = x;
globalSharedObject.y = y;
globalSharedObject.z = z;

// 方案2. 使用访问模式实现(X为globalSharedObject.x的VarHandle)
x.a = 1;
X.setRelease(globalSharedObject, x);
y.a = 1;
Y.setRelease(globalSharedObject, y);
z.a = 1;
Z.setRelease(globalSharedObject, z);

回到上面单个变量x的同步访问例子,可以观察到“初始化x”这个动作只是将一堆字面量赋值到x上,相当于都是对x的写操作,并没有读操作。因此在使用屏障的实现中,可以将releaseFence退化为更约束更弱的storeStoreFence:

x.a = 1;
x.b = 2;
VarHandle.storeStoreFence();
globalSharedObject.x = x;

参考链接

「StackOverflow」What's the difference between getVolatile and getAcquire?

「YouTube」Java 9 VarHandles Best practices, and why? by Tobi Ajila

Using JDK 9 Memory Order Modes - Doug Lea

https://stackoverflow.com/questions/24565540/how-to-understand-acquire-and-release-semantics

posted @ 2024-01-19 15:43  NOSAE  阅读(77)  评论(0编辑  收藏  举报