1. 程式人生 > >java進階之unsafe與CAS操作簡介

java進階之unsafe與CAS操作簡介

簡介

Unsafe

Unsafe類屬於在sun.misc包下,不屬於Java標準包。
但是很多Java的基礎類庫,比如Netty、Cassandra、Hadoop、Kafka等都使用此類。
Unsafe類因為是native方法,直接呼叫底層,所以效率很高,在增強Java語言底層操
作能力方面起了很大的作用。當然,因為其直接操作記憶體,有點不受JVM管束,所以
官方建議除了JAVA自帶的API內部使用,是不建議開發人員直接使用的。

CAS操作

Compare And Swap(CAS)簡單的說就是比較並交換。
CAS 操作包含三個運算元 —— 記憶體位置(V)、預期原值(A)和新值(B)。如果記憶體位置的值與預期原值相匹配,那麼處理器會自動將該位置值更新為新值。否則,處理器不做任何操作。無論哪種情況,它都會在 CAS 指令之前返回該位置的值。Java併發包(java.util.concurrent)中大量使用了CAS操作,涉及到併發的地方都呼叫了sun.misc.Unsafe類中的Comparexxx方法進行CAS操作。

Volatile修飾的成員變數在每次被執行緒訪問時,都強迫從主記憶體中重讀該成員變數的值。並且,當成員變數發生變化時,執行緒將變化值回寫到主記憶體。這樣不同的執行緒看到的變數的值總是相同的,或者說volatile修飾的變數值發生變化時對於另外的執行緒是可見的。

通過volatile和CAS機制,我們可以實現樂觀鎖的功能。
程式設計中的鎖可以分為兩類:悲觀鎖和樂觀鎖。悲觀鎖的主要觀點是限制入口,每次只能有一個執行緒訪問,好像真的有一把鎖,誰得到了誰才能夠執行自己的操作,但是這樣會導致大量的執行緒阻塞,比較耗費資源,也適用於同步程式碼快本身就佔用大量資源或者會長時間執行的情況。
而樂觀鎖則認為衝突是很少的,所以開放入口,讓每個執行緒自己去檢查當前是否有其餘的執行緒在執行,如果沒有則自己執行,如果有則等待,這樣在衝突較少或者同步程式碼塊佔用資源較少的的情況下,樂觀鎖的效率會非常的高


而通過volatile和CAS機制就是實現此樂觀鎖的一種方式,當然也有人覺得這種方式並沒有真正意義上的阻塞執行緒,並不存在鎖操作,所以也被認為是無鎖同步的一種方式。

因為java的CAS實現是呼叫的Java本地方法(JNI),是直接由cpu硬體支援的原子操作,這使得java程式通過CAS來執行的效率會非常高。

方法說明

Unsafe類使用了單例模式,需要通過一個靜態方法getUnsafe()來獲取。但Unsafe類做了限制,如果是直接呼叫的話,它會丟擲一個SecurityException異常;
在JDk1.8或之前只有由主類載入器載入的類才能呼叫這個方法。其原始碼如下:

public
static Unsafe getUnsafe() { Class var0 = Reflection.getCallerClass(); if(!VM.isSystemDomainLoader(var0.getClassLoader())) { throw new SecurityException("Unsafe"); } else { return theUnsafe; } }

當然,可以使用反射的方法來使用:

 Field f = Unsafe.class.getDeclaredField("theUnsafe");
 f.setAccessible(true);
 Unsafe unsafe = (Unsafe) f.get(null);

在JDK1.9之後此類已經開放使用了:
java9中,Unsafe變成了jdk.internal.misc下的包,同時包含了一個靜態方法,可以直接拿到theUnsafe物件。

package jdk.internal.misc;

public static Unsafe getUnsafe() {
    return theUnsafe;
}

通過unsafe來構建java物件:
通過unsafe構建時,無論構造方法是不是private的類都可以例項化。

//unsafe = Unsafe的例項物件
Singleton singleton= (Singleton) unsafe.allocateInstance(Singleton.class);

Unsafe類提供了以下這些功能:

  • 記憶體管理

包括分配記憶體、釋放記憶體等
該部分包括了allocateMemory(分配記憶體)、reallocateMemory(重新分配記憶體)、copyMemory(拷貝記憶體)、freeMemory(釋放記憶體 )、getAddress(獲取記憶體地址)、addressSize、pageSize、getInt(獲取記憶體地址指向的整數)、getIntVolatile(獲取記憶體地址指向的整數,並支援volatile語義)、putInt(將整數寫入指定記憶體地址)、putIntVolatile(將整數寫入指定記憶體地址,並支援volatile語義)、putOrderedInt(將整數寫入指定記憶體地址、有序或者有延遲的方法)等方法。

getXXX和putXXX中是對記憶體中各種資料的讀寫方法,其中getXXXVolatile和getXXX,前者支援Volatile語義,後者不支援,而普通操作也get類似。
get操作同時可以獲取某一個類中某一個屬性在非堆記憶體中的位置,因為在非堆中,這個屬性需要滿足下面兩個屬性之一:
這個屬性是一個靜態屬性 。
這個屬性是一個volatile修飾的屬性。
(非堆記憶體又被稱為方法區,一般都儲存靜態屬性和常量池和JVM內部處理或優化所需的記憶體等)

而在寫方法中,putXXX,putOrderXXX,putXXXVolatile的效能也是依次從高到低。
分配記憶體和讀寫值:

Unsafe unsafe = getUnsafe();
final int size = 100;
//分配記憶體,並返回地址,size表示位元組數,即分配了100個位元組的記憶體
final long addr = unsafe.allocateMemory( size );
try{
    System.out.println( "當前分配的記憶體首地址: " + addr );
    //由於一個Int型別佔用4個位元組,所以100個位元組長度實際上最多表示一個25個長度的Int陣列
    for ( int i = 0; i < size / 4; i++ )
    {
        unsafe.putInt( addr + i * 4, 123);
        if ( unsafe.getInt( addr + i * 4 ) != 123 ) {
            System.out.println("記憶體值寫入失敗,地址偏移量是:" + i);
        }
    }
    //迴圈列印資料前3個數
    for ( long i = 0; i < 3; ++i ){
        int result = unsafe.getInt( addr + i * 4 );
        System.out.println( " i = " + i  + ",result is:" + result);
    }
}finally{
    //必須在finally中釋放記憶體
    unsafe.freeMemory( addr );
}

輸出:

當前分配的記憶體首地址: 428555056
 i = 0,result is:123
 i = 1,result is:123
 i = 2,result is:123
  • 類似反射、物件、變數操作
    Unsafe提供了很多類似反射的操作:
    allocateInstance() 可以直接生成物件例項,且無需呼叫構造方法和其它初始化方法。
    這在物件反序列化的時候會很有用,能夠重建和設定final欄位,而不需要呼叫構造方法。
    staticFieldOffset(靜態域偏移)、defineClass(定義類)、defineAnonymousClass(定義匿名類)、ensureClassInitialized(確保類初始化)、objectFieldOffset(物件域偏移)等方法。
    通過這些方法我們可以獲取物件的指標,通過對指標進行偏移,我們可以直接修改指標指向的資料(即使它們是私有的),包括JVM已經認定為垃圾、可以進行回收的物件。

  • 陣列操作
    這部分包括了arrayBaseOffset(獲取陣列第一個元素的偏移地址)、arrayIndexScale(獲取陣列中元素的增量地址)等方法。arrayBaseOffset與arrayIndexScale配合起來使用,就可以定位陣列中每個元素在記憶體中的位置。
    java中預設的陣列下標是Integer型別的,所以最大陣列長度是Integer.MAX_VALUE,而使用Unsafe類的記憶體分配方法可以實現超大陣列。當然這樣的陣列實際上風險很大,記憶體和分配和回收都有可能會發生異常。

@Test
    public void test003(){
        Unsafe unsafe = getUnsafe();
        long longArrayOffset = unsafe.arrayBaseOffset(long[].class);
        final long[] ar = new long[ 1000 ];
        final int index = ar.length - 1;
        ar[ index ] = -1; //FFFF FFFF FFFF FFFF
        System.out.println( "Before change = " + Long.toHexString( ar[ index ] ));
        for ( long i = 0; i < 8; ++i )
        { //long型別佔用8個位元組,這裡每一次迴圈向ar[ index ]這個long物件中的一個位元組中寫入內容0
            unsafe.putByte( ar, longArrayOffset + 8L * index + i, (byte) 0);
            System.out.println( "After change: i = " + i + ", val = " + Long.toHexString( ar[ index ] ));
        }
    }

輸出:

Before change = ffffffffffffffff
After change: i = 0, val = ffffffffffffff00
After change: i = 1, val = ffffffffffff0000
After change: i = 2, val = ffffffffff000000
After change: i = 3, val = ffffffff00000000
After change: i = 4, val = ffffff0000000000
After change: i = 5, val = ffff000000000000
After change: i = 6, val = ff00000000000000
After change: i = 7, val = 0
  • 鎖功能、CAS操作
    類似與compareAndSwapXXX的操作都是一個原子操作,也就是說cpu在執行cas時,對這一塊記憶體是獨佔排他的。在併發包中很多操作真正執行的也是cas,併發包中的類併發效能比使用 synchronized 關鍵字好也在於此:鎖的粒度小了許多並且少了執行緒上下文切換。(synchronized 操作需要切換執行緒的上下文,所以比較耗費資源)
    java的很多AtomicXXX原子類都是通過CAS操作來實現的,比如AtomicInteger。
    同時Unsafe中還包括了monitorEnter、tryMonitorEnter、monitorExit、compareAndSwapInt、compareAndSwap等方法。
    其中monitorEnter、tryMonitorEnter、monitorExit已經被標記為deprecated,不建議使用。
    compareAndSwapInt方法:
/**
* 比較obj的offset處記憶體位置中的值和期望的值,如果相同則更新。此更新是不可中斷的,
  即使原子性的。
* @param obj 需要更新的物件
* @param offset obj中整型field的偏移量
* @param expect 希望field中存在的值
* @param update 如果期望值expect與field的當前值相同,設定filed的值為這個新值
* @return 如果field的值被更改返回true
*/
public native boolean compareAndSwapInt(Object obj, long offset, int expect, int update);
  • 執行緒的阻塞與喚醒:park、unpark
    park、unpark方法是併發包中鎖的基礎,通過park將執行緒一直阻塞直到超時或者中斷等條件出現。unpark可以喚醒一個park的執行緒,使其恢復正常。
    而LockSupport類中的park與unpark方法對unsafe中的park與unpark方法做了封裝,LockSupport類中有各種版本pack方法,但最終都呼叫了Unsafe.park()方法。

  • 記憶體屏障
    記憶體屏障,也稱記憶體柵欄,記憶體柵障,屏障指令等, 是一類同步屏障指令,是CPU或編譯器在對記憶體隨機訪問的操作中的一個同步點,使得此點之前的所有讀寫操作都執行後才可以開始執行此點之後的操作。之所以這樣,實際上是因為cpu的快取中的變數值和記憶體中的變數值在多執行緒情況下可能不一致。
    Unsafe中包括了loadFence、storeFence、fullFence來進行記憶體屏障操作。而Unsafe. putOrderedObject,Unsafe. putOrderedInt,Unsafe. putOrderedLong這三個方法,JDK會在執行這三個方法時插入StoreStore記憶體屏障,避免發生寫操作重排序。
    記憶體屏障是在Java 8新引入的,用於定義記憶體屏障,避免程式碼重排序。
    loadFence() 表示該方法之前的所有load操作在記憶體屏障之前完成。
    storeFence()表示該方法之前的所有store操作在記憶體屏障之前完成。
    fullFence()表示該方法之前的所有load、store操作在記憶體屏障之前完成。

參考