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.atomic
和sun.misc.Unsafe
的部分操作。并且提供了一系列标准的内存屏障操作,用于更加细粒度的控制内存排序。在安全性、可用性、性能上都要优于现有的API。VarHandle 可以与任何字段、数组元素或静态变量关联,支持在不同访问模型下对这些类型变量的访问,包括简单的 read/write 访问,volatile 类型的 read/write 访问,和 CAS(compare-and-swap)等。
创建VarHandle
VarHandle通过MethodHandles
的lookup()
方法创建,下面演示了非静态变量和数组的获取方式:
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
Lookup
是MethodHandles
的内部类,位于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
类的fullFence
、loadFence
和storeFence
来实现,关于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
节点,只需要调用VarHandle
的compareAndSet
即可(HEAD.compareAndSet(this, null, new Node())
)。
目前,在java.util.concurrent
包中对变量的访问基本上都由Unsafe
改为了VarHandle
,有关VarHandle的更多详细使用方式,请参考JUC源码。