Java 9 变量句柄-VarHandle

Java 9的发布的新特性除了最主要的模块化之外,在API方面也为开发者们带来了很多有用的特性,本篇我们来探讨一下java 9提供的新的API-VarHandle 对 memory order 的支持,及其在JUC同步类中的应用。在开始本篇之前,你需要对JMM(Java 内存模型)有一定的认知。

VarHandle 的必要性

随着Java中的并发和并行编程的不断扩大,我们经常会需要对某个类的字段进行原子或有序操作,但是 JVM 对Java开发者所开放的权限非常有限。例如:如果要原子性地增加某个字段的值,到目前为止我们可以使用下面三种方式:

  • 使用AtomicInteger来达到这种效果,这种间接管理方式增加了空间开销,还会导致额外的并发问题;
  • 使用原子性的FieldUpdaters,由于利用了反射机制,操作开销也会更大;
  • 使用sun.misc.Unsafe提供的JVM内置函数API,虽然这种方式比较快,但它会损害安全性和可移植性,当然在实际开发中也很少会这么做。

在 VarHandle 出现之前,这些潜在的问题会随着原子API的不断扩大而越来越遭。VarHandle 的出现替代了java.util.concurrent.atomicsun.misc.Unsafe的部分操作。并且提供了一系列标准的内存屏障操作,用于更加细粒度的控制内存排序。在安全性、可用性、性能上都要优于现有的API。VarHandle 可以与任何字段、数组元素或静态变量关联,支持在不同访问模型下对这些类型变量的访问,包括简单的 read/write 访问,volatile 类型的 read/write 访问,和 CAS(compare-and-swap)等。


创建VarHandle

VarHandle通过MethodHandleslookup()方法创建,下面演示了非静态变量和数组的获取方式:

public class VarhandleFoo {
    
    private Point[] points;

    private static final VarHandle QA;//for arrays
    private static final VarHandle X;//for Variables
    static {
        try {
            QA =  MethodHandles.arrayElementVarHandle(Point[].class);
            X = MethodHandles.lookup().
                    findVarHandle(Point.class, "x", int.class); //or
            //X = MethodHandles.lookup().in(Point.class).findVarHandle(Point.class, "x", int.class);
        } catch (ReflectiveOperationException e) {
            throw new Error(e);
        }
    }

    class Point {
        volatile int x;
        // ...
    }
}

Lookup

LookupMethodHandles的内部类,位于java.lang.invoke包中,它是一个用于创建方法和变量句柄的工厂。与我们熟知的核心反射API不同的是,核心反射API在每一次方法被调用时都会进行访问检查,而通过Lookup创建的方法句柄的访问检查是在它被创建时进行的。通过Lookup提供的工厂方法,我们可以访问对象的任何方法、构造函数和参数变量。由工厂方法创建的每个方法句柄都等同于方法的字节码行为(bytecode behavior),也就是说,JVM调用方法句柄与执行和方法句柄相关的字节码行为一致。(有关Lookup的更详细介绍,请参阅官方API-MethodHandles.Lookup

Lookup在Java 9中对变量访问也添加了相应的工厂方法,用于生成变量句柄-VarHandle:

  • findVarHandle:用于创建对象中非静态字段的VarHandle。接收参数有三个,第一个为接收者的class对象,第二个是字段名称,第三个是字段类型。
  • findStaticVarHandle:用于创建对象中静态字段的VarHandle,接收参数与findVarHandle一致。
  • unreflectVarHandle:通过反射字段Field创建VarHandle

获取VarHandle后,接下来就是对变量的访问,下面列举了几种简单的访问形式:

//plain read
int x = (int) X.get(this);
Point p = (Point) QA.get(points,10);

//plain write
X.set(this,1);
QA.set(points,10,new Point());

//CAS
X.compareAndSet(this,0,1);
QA.compareAndSet(points,10,p,new Point());

//Numeric Atomic Update
X.getAndAdd(this,10);

VarHandle中的每个方法都被称为 access mode method,接收的参数都是一个协调表达式,该表达式精确地指示了要访问变量的对象,后续的调用参数表示当前访问模式的值。例如,CAS方法需要两个后续参数:预期值和新值。
需要注意的是,access mode将覆盖在变量声明时指定的任何内存排序效果。 例如,一个VarHandle使用 get 模式访问一个字段时,即使这个字段已经被声明为volatile,也会把这个字段当做方法指定的访问模式进行访问。因此使用时要非常小心。

内存屏障

VarHandle除了支持在各种访问模式下访问变量之外,还提供了一组内存屏障方法,为内存排序提供更细粒度的控制。主要有以下几个方法:

public static void fullFence() {
    UNSAFE.fullFence();
}
public static void acquireFence() {
    UNSAFE.loadFence();
}
public static void releaseFence() {
    UNSAFE.storeFence();
}
public static void loadLoadFence() {
    UNSAFE.loadLoadFence();
}
public static void storeStoreFence() {
    UNSAFE.storeStoreFence();
}

本质上来看,这些内存屏障都是通过Unsafe类的fullFenceloadFencestoreFence来实现,关于Unsafe,大家可以参考我的另外一篇文章:JUC源码分析—CAS和Unsafe

VarHandle的应用

VarHandle出世后,JUC中多数同步类中都使用VarHandle替代了Unsafe,这里我们拿AQS举例说明,直接看代码:

// VarHandle mechanics
private static final VarHandle STATE;
private static final VarHandle HEAD;
private static final VarHandle TAIL;

static {
    try {
        MethodHandles.Lookup l = MethodHandles.lookup();
        STATE = l.findVarHandle(AbstractQueuedSynchronizer.class, "state", int.class);
        HEAD = l.findVarHandle(AbstractQueuedSynchronizer.class, "head", Node.class);
        TAIL = l.findVarHandle(AbstractQueuedSynchronizer.class, "tail", Node.class);
    } catch (ReflectiveOperationException e) {
        throw new ExceptionInInitializerError(e);
    }

    // Reduce the risk of rare disastrous classloading in first call to
    // LockSupport.park: https://bugs.openjdk.java.net/browse/JDK-8074773
    Class<?> ensureLoaded = LockSupport.class;
}

/**
 * 初始化queue
 */
private final void initializeSyncQueue() {
    Node h;
    if (HEAD.compareAndSet(this, null, (h = new Node())))
        tail = h;
}

AQS中对变量state, head, tail的访问都改为了VarHandle方式。例如,如果要用CAS方式修改head节点,只需要调用VarHandlecompareAndSet即可(HEAD.compareAndSet(this, null, new Node()))。

目前,在java.util.concurrent包中对变量的访问基本上都由Unsafe改为了VarHandle,有关VarHandle的更多详细使用方式,请参考JUC源码。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342