一、VarHandle簡介
變量句柄(VarHandle)是對於一個變量的強類型引用,或者是一組參數化定義的變量族,包括了靜態字段、非靜態字段、數組元素等,VarHandle支持不同訪問模型下對於變量的訪問,包括簡單的read/write訪問,volatile read/write訪問,以及CAS訪問。
VarHandle相比於傳統的對於變量的併發操作具有巨大的優勢,在JDK9引入了VarHandle之後,JUC包中對於變量的訪問基本上都使用VarHandle,比如AQS中的CLH隊列中使用到的變量等。
二、VarHandle的作用與優勢
在開始討論VarHandle之前,我們先來回憶一下併發操作裏面的三大特性:原子性、可見性、有序性。
volatile變量可以保證可見性、有序性(防止指令重拍),加鎖或者原子操作、CAS等可以保證原子性,只有同時滿足這三個特性才能夠保證對於一個變量的併發操作是合乎預期的。
加鎖的話需要對線程進行同步,而線程上下文切換之間帶來的開銷是很大的,所以這裏不予考慮,考慮一下幾種方式:
- 使用Atomic包下的原子類進行間接管理,但增加了開銷,也可能導致額外的問題如ABA問題;
- 使用原子性的FieldUpdaters,利用反射機制,開銷也會增大;
- 使用sun.misc.Unsafe提供的JVM內置函數,但直接操作JVM可能會損害安全性和可移植性;
針對以上的問題,VarHandle就是用來替代上述方式的一種方案,它提供了一系列標準的內存屏障操作,用於更細粒度的控制指令排序,在安全性、可用性、性能等方面都要優於現有的AIP,且基本上能夠和任何類型的變量相關聯。
三、創建VarHandle
VarHandle通過MethodHandles類中的內部類Lookup來創建:
public class VarHandleTest {
private String plainStr; //普通變量
private static String staticStr; //靜態變量
private String reflectStr; //通過反射生成句柄的變量
private String[] arrayStr = new String[10]; //數組變量
private static final VarHandle plainVar; //普通變量句柄
private static final VarHandle staticVar; //靜態變量句柄
private static final VarHandle reflectVar; //反射字段句柄
private static final VarHandle arrayVar; //數組句柄
static {
try {
MethodHandles.Lookup l = MethodHandles.lookup();
plainVar = l.findVarHandle(VarHandleTest.class, "plainStr", String.class);
staticVar = l.findStaticVarHandle(VarHandleTest.class, "staticStr", String.class);
reflectVar = l.unreflectVarHandle(VarHandleTest.class.getDeclaredField("reflectStr"));
arrayVar = MethodHandles.arrayElementVarHandle(String[].class);
} catch (ReflectiveOperationException e) {
throw new ExceptionInInitializerError(e);
}
}
}
來分析一下上述創建VarHandle的代碼:
1、通過MethodHandles類裏面的lookup()函數創建一個Lookup類,這個Lookup類是MethodHandles的內部類,作用就是用於創建方法句柄和變量句柄的一個工廠類,源碼如下:
public static Lookup lookup() {
return new Lookup(Reflection.getCallerClass());
}
2、通過Lookup裏面的工廠方法生成不同類型的VarHandle,拿生成普通變量的findVarHandle方法來看:
public VarHandle findVarHandle(Class<?> recv, String name, Class<?> type)
throws NoSuchFieldException, IllegalAccessException {
MemberName getField = resolveOrFail(REF_getField, recv, name, type);
MemberName putField = resolveOrFail(REF_putField, recv, name, type);
return getFieldVarHandle(REF_getField, REF_putField, recv, getField, putField);
}
代碼很簡單,就是根據傳入的參數來生成字段的訪問對象MemberName,MemberName是用來描述一個方法或字段引用的數據結構,再根據MemberName生成句柄,熟悉JVM的同學看到REF_getField應該能夠聯想到JVM字節碼,這個就是對應着訪問字段的字節碼。
對於findStaticVarHandle、unreflectVarHandle方法的實現其實也很類似,大致就是將REF_getField改爲REF_getStatic的過程。
四、VarHandle的訪問
一開始我們就講過,VarHandle支持不同訪問模型下對於變量的訪問,包括簡單的read/write訪問,volatile read/write訪問,以及CAS訪問等,那麼分別來看一下VarHandle是如何支持這些訪問模型下對於變量的訪問的。
1、簡單的read/write訪問
plainVar.set(this, "I am a plain string"); //實例變量的普通write操作
staticVar.set("I am a static string"); // 靜態變量的普通write操作
reflectVar.set(this, "I am a string created by reflection"); //反射字段的普通write操作
arrayVar.set(arrayStr, 0, "I am a string array element"); //數組變量的普通write操作
String plainString = (String) plainVar.get(this); //實例變量的普通read操作
String staticString = (String) staticVar.get(); // 靜態變量的普通read操作
String reflectString = (String) staticVar.get(this); //反射字段的普通read操作
String arrayStrElem = (String) arrayVar.get(arrayStr, 0); //數組變量的普通read操作, 第二個參數是指數組下標,即第0個元素
2、volatile read/write訪問
對於不同類型的變量的訪問方法跟上面其實大同小異,下面就以普通變量來舉例:
volatileVar.setVolatile(this, "I am volatile string"); //volatile write
String volatileString = (String) volatileVar.get(this); //volatile read
3、CAS訪問
String casString = (String) casVar.get(this); //先讀取當前值作爲cas的預期值
casVar.compareAndSet(this, casString, "I am a new cas string"); //第二個參數爲預期值,第三個參數爲修改值
五、VarHandle中的指令重排序影響
VarHandle中定義了多種不同的訪問模式,定義在VarHandle內部的枚舉類裏面:
public enum AccessMode {
GET("get", AccessType.GET),
SET("set", AccessType.SET),
GET_VOLATILE("getVolatile", AccessType.GET),
SET_VOLATILE("setVolatile", AccessType.SET),
GET_ACQUIRE("getAcquire", AccessType.GET),
SET_RELEASE("setRelease", AccessType.SET),
...
}
上面只是VarHandle中定義的訪問模式中的一小部分,實際上還有很多,不同的訪問模式對於指令重排的效果都可能不一樣,因此需要慎重選擇訪問模式。
其次,在創建VarHandle的過程中,VarHandle內部的訪問模式會覆蓋變量聲明時的任何指令排序效果。比如說一個變量被聲明爲volatile類型,但是調用varHandle.get()方法時,其訪問模式就是get模式,即簡單讀取方法,因此需要多加註意。