11-Java中CAS操作
1. Java中CAS操作
-
在Java中使用鎖不好的地方就是當一個執行緒沒有獲取到鎖時會被阻塞掛起,這會導致執行緒上下文重新排程與開銷。Java提供了非阻塞的volatile關鍵字來解決共享變數的可見性問題。但是volatile只能保證共享變數的可見性,不能解決讀-改-寫的原子性問題。CAS即為Compare and Swap,是JDK提供的非阻塞的原子性操作。它通過硬體方式保證了比較-更新操作的原子性。
-
經典ABA問題:假如執行緒1使用CAS修改初始值為A的變數X(X=A),那麼執行緒1首先會獲取當前變數X的值(A),然後使用CAS操作嘗試修改X的值為B,如果使用CAS修改成功了,那麼程式執行一定是正常的嗎?其實未必,這是因為有可能線上程1獲取到變數X的值A後,在執行CAS之前,執行緒2使用了CAS修改了變數X值為B,然後又使用了CAS操作使得變數X值為A,雖然執行緒A執行了CAS操作時X=A,但是這個A已經不是執行緒1獲取到的A了。這就是ABA問題。ABA問題的產生是因為變數的狀態值產生了環形轉換,就是變數值可以從A到B,也可以B到A,如果變數的值只能朝著一個方向轉換,例如A到B,B到C,不構成環路,就不會存在這個問題。JDK中的AtomicStampedReference類給每個變數的狀態值都配備了一個時間戳,從而避免了ABA問題。
-
2. Unsafe類
2.1 Unsafe類中重要的方法
JDK的rt.jar包中的Unsafe類提供了硬體級別的原子性操作,Unsafe類中的方法都是native方法,他們使用JIN的方法訪問本地C++實現庫,下面我們介紹一下Unsafe類能做的事情:
-
long objectFieldOffset(Field field)方法:返回指定的變數在所屬類中的記憶體偏移地址,該偏移地址僅僅在該Unsafe函式中訪問指定欄位時候使用。如下程式碼就是使用Unsafe類來獲取變數value在AtmoicLong物件中的記憶體偏移。
-
static{ try{ valueOffset=unsafe.objectFieldOffset(AtomicLong.class.getDeclaredField("value")); } catch(Exception e){ e.printStack(); } }
-
-
int arrayBaseOffset(Class arrayClass)方法:獲取陣列中第一個元素的地址。
-
int arrayIndexScale(Class arrayClass)方法:獲取陣列中第一個元素佔用的位元組數。
-
boolean comapreAndSwapLong(Object obj,long offset,long expect,long update)方法:比較物件obj的偏移量為offset的變數值是否為expect,如果不是,則修改為update,然後返回true,否則返回false。
-
native long getLongvolatile(Object obj,long offset)方法:獲取物件obj中偏移量為offset的變數對應的volatile語義的值。
-
void park()方法:阻塞當前執行緒。
-
void unpark()方法:喚醒呼叫park後阻塞的執行緒。
-
。。。。。。
2.2使用Unsafe類
public class TestUnsafe {
//獲取Unsafe例項
static final Unsafe unsafe = Unsafe.getUnsafe();
//記錄變數state在類TestUnsafe中的偏移值
static long stateOffset = -1L;
//變數state
private volatile long state = 0;
static {
try {
//獲取state變數在TestUnsafe中的偏移值
stateOffset = unsafe.objectFieldOffset(TestUnsafe.class.getDeclaredField("state"));
} catch (NoSuchFieldException e) {
System.out.println(e.getLocalizedMessage());
e.printStackTrace();
}
}
public static void main(String[] args) {
//建立例項,並設定state為1
TestUnsafe test = new TestUnsafe();
Boolean success = unsafe.compareAndSwapInt(test, stateOffset, 0, 1);
System.out.println(success);
}
}
Exception in thread "main" java.lang.ExceptionInInitializerError
Caused by: java.lang.SecurityException: Unsafe
at sun.misc.Unsafe.getUnsafe(Unsafe.java:90)
at com.heiye.temp.TestUnsafe.<clinit>(TestUnsafe.java:7)
為了找出原因,我們需要檢視getUnsafe程式碼
@CallerSensitive
public static Unsafe getUnsafe() {
//2.2.7
Class var0 = Reflection.getCallerClass();
//2.2.8
if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
throw new SecurityException("Unsafe");
} else {
return theUnsafe;
}
}
//2.2.9
public static boolean isSystemDomainLoader(ClassLoader var0) {
return var0 == null;
}
程式碼(2.2.7)獲取呼叫getUnsafe方法的Class物件,這裡這個物件是TestUnsafe.class。
程式碼(2.2.8)判斷是不是Bootsrap類載入器載入的localClass,在這裡看是不是Bootsrap載入器載入了TestUnsafe.class,很明顯檢視是由AppClassLoader載入的,所以這裡就直接丟擲了異常。
為什麼要進行判斷呢?我們知道Unsafe類是rt.jat提供的,rt.jat包裡面的類是Bootsrap類載入器載入的,而我們啟動main方法的時候是AppClassLoader類載入器載入的,所以main方法在啟動的時候,根據委託機制,會委託給Bootsrap載入器載入Unsafe類。如果沒有程式碼(2.2.8)的限制,我們會可以隨意的使用Unsafe類做事情了。而Unsafe類是可以直接操作記憶體的,是不安全的。所以JDK開發組特意做了這個限制,不讓開發人員正規渠道使用Unsafe類,而是在rt.jar包的核心類中使用Unsafe類。
如果我們想用,我們可以通過反射的角度來獲取Unsafe例項。
public class TestUnsafe1 {
static Unsafe unsafe;
static Long stateOffset;
private volatile long state = 0;
static {
try {
//使用反射來獲取Unsafe成員變數theUnsafe
Field field = Unsafe.class.getDeclaredField("theUnsafe");
//設定為可存取
field.setAccessible(true);
//獲取該變數的值
unsafe = (Unsafe) field.get(null);
//獲取state在TestUnsafe1中的偏移量
stateOffset = unsafe.objectFieldOffset(TestUnsafe1.class.getDeclaredField("state"));
} catch (NoSuchFieldException | IllegalAccessException e) {
System.out.println(e.getLocalizedMessage());
e.printStackTrace();
}
}
public static void main(String[] args) {
TestUnsafe1 testUnsafe1 = new TestUnsafe1();
Boolean success = unsafe.compareAndSwapInt(testUnsafe1, stateOffset, 0, 1);
System.out.println(success);
}
}
true