JUC整理筆記一之細說Unsafe

JFound發表於2020-05-23

JUC(java.util.concurrent)的開始,可以說是從Unsafe類開始。

Unsafe 簡介

Unsafe在sun.misc 下,顧名思義,這是一個不安全的類,因為Unsafe類所操作的並不屬於Java標準,Java的一系列記憶體操作都是交給jvm的,而Unsafe類卻能有像C語言的指標一樣直接操作記憶體的能力,同時也會帶來了指標的問題。過度使用Unsafe類的話,會使出錯率變得更大,因此官方才命名為Unsafe,並且不建議使用,連註釋的沒有。

而為了安全使用Unsafe,Unsafe類只允許jdk自帶的類使用,從下面的程式碼中可以看出

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

如果當前Class是非系統載入的(也就是caller.getClassLoader()不為空),直接丟擲SecurityException

在java9之後,又出現了一個jdk.internal.misc.Unsafe類,其功能與sun.misc.Unsafe類是一樣的,唯一不一樣的是在 getSafe() 的時候,jdk.internal.misc.Unsafe是沒有做校驗的,但是jdk包下的程式碼,應用開發時是不能直接呼叫的,而且在java9之後,兩個Unsafe類都有充足的註釋。

  • 獲取Unsafe

Unsafe類裡有這樣的一個field。

private static final Unsafe theUnsafe = new Unsafe();

也就是說雖然不能直接拿到Unsafe物件,但是還是可以通過反射去獲取的。

private static Unsafe getUnsafe() throws NoSuchFieldException, IllegalAccessException {
     Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe");
     theUnsafeField.setAccessible(true);
     return (Unsafe) theUnsafeField.get(Unsafe.class);
}

而 JUC緊密使用了Unsafe的功能

功能簡介

Unsafe類的功能主要分為記憶體操作、CAS、Class相關、物件操作、陣列相關、記憶體屏障、系統相關、執行緒排程等功能。

記憶體操作

  • 堆外(native memory)記憶體操作
//分配記憶體,並返回記憶體地址
public native long allocateMemory(long bytes);
//擴充記憶體,address可以是allocateMemory方法返回的地址,bytes是擴充的大小
public native long reallocateMemory(long address, long bytes);
//釋放記憶體
public native void freeMemory(long address);
//在給定的記憶體塊設定預設值
public native void setMemory(long address, long bytes, byte value);
//獲取指定地址值的byte型別
public native byte getByte(long address);
//設定堆外指定值的byte型別的值
public native void putByte(long address, byte x);
  • 堆內記憶體操作
//在給定的記憶體塊設定預設值
public native void setMemory(Object o, long offset, long bytes, byte value);
//記憶體拷貝
public native void copyMemory(Object srcBase, long srcOffset, Object destBase, long destOffset, long bytes);
//獲取指定地址的值,offset為o物件某個field的偏移量,類似有: getInt,getDouble,getLong,getChar等
public native Object getObject(Object o, long offset);
//設定值,offset為o物件某個field的偏移量,類似的還有 putInt,putDouble,putLong,putChar等
public native void putObject(Object o, long offset, Object x);

通常,在java建立的物件都是在堆記憶體中的,堆記憶體是有JVM所託管,並且都遵循JVM記憶體管理機制。與之相對的是JAVA管核之外的堆外記憶體,是依賴Unsafe提供的操作堆外記憶體的native方法處理。

java帶的NIO中的java.nio.DirectByteBuffer中就是用Unsafe的堆外記憶體函式來操作堆外記憶體。使用方法

ByteBuffer.allocateDirect(1024);

最終在 DirectByteBuffer 的建構函式中呼叫 UNSAFE 來分配並初始化記憶體。

long base = 0;
try {
    base = UNSAFE.allocateMemory(size);
} catch (OutOfMemoryError x) {
    Bits.unreserveMemory(size, cap);
    throw x;
}
UNSAFE.setMemory(base, size, (byte) 0);
  • 使用案例

堆外記憶體

Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) theUnsafeField.get(Unsafe.class);
long address = unsafe.allocateMemory(1024);
unsafe.setMemory(address, 1024, (byte) 0);
unsafe.putInt(address, 1);
System.out.println(unsafe.getInt(address));
unsafe.freeMemory(address);

堆內記憶體

public class Demo {
    private static String name = "jfound";
    private int age = 10;
}
//-----------
Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) theUnsafeField.get(Unsafe.class);

Demo demo = new Demo();
Field ageField = Demo.class.getDeclaredField("age");
ageField.setAccessible(true);
long ageFieldAddress = unsafe.objectFieldOffset(ageField);
System.out.println(unsafe.getInt(demo, ageFieldAddress));
Field nameField = Demo.class.getDeclaredField("name");
nameField.setAccessible(true);

unsafe.ensureClassInitialized(Demo.class); //初始化,否則name為null
long nameAddress = unsafe.staticFieldOffset(nameField);
System.out.println(unsafe.getObject(Demo.class, nameAddress)); //輸出jfound

CAS

CAS,較並替換,CAS操作包含三個運算元——記憶體位置、預期原值及新值。執行CAS操作的時候,將記憶體位置的值與預期原值比較,如果相匹配,那麼處理器會自動將該位置值更新為新值,否則,處理器不做任何操作。

此操作是CPU指令cmpxchg,是屬於指令級別的,具有原子性 ,典型的 atomic下的類,AQS系列的鎖都是借用Unsafe下的CAS的api來實現的。

  • api
//cas改變值,o為改變的物件,可以是class,offset是指定的field偏移量,expected是期望的值,expected修改的值
public final native boolean compareAndSwapObject(Object o, long offset,  Object expected, Object update);
public final native boolean compareAndSwapInt(Object o, long offset, int expected,int update);
public final native boolean compareAndSwapLong(Object o, long offset, long expected, long update);
  • 例子
public class Demo {
    private static String name = "jfound";
}
//--------
Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) theUnsafeField.get(Unsafe.class);

Field nameField = Demo.class.getDeclaredField("name");
nameField.setAccessible(true);
long nameAddress = unsafe.staticFieldOffset(nameField);
unsafe.ensureClassInitialized(Demo.class); //初始化,否則name為null
unsafe.compareAndSwapObject(Demo.class, nameAddress, "jfound", "jfound-plus");
System.out.println(unsafe.getObject(Demo.class, nameAddress)); //輸出 jfound-plus

Class相關

  • api
//獲取給定靜態欄位的記憶體地址偏移量,這個值對於給定的欄位是唯一且固定不變的
public native long staticFieldOffset(Field f);
//獲取一個靜態類中給定欄位的物件指標
public native Object staticFieldBase(Field f);
//判斷是否需要初始化一個類,通常在獲取一個類的靜態屬性的時候(因為一個類如果沒初始化,它的靜態屬性也不會初始化)使用。 當且僅當ensureClassInitialized方法不生效時返回false。
public native boolean shouldBeInitialized(Class<?> c);
//檢測給定的類是否已經初始化。通常在獲取一個類的靜態屬性的時候(因為一個類如果沒初始化,它的靜態屬性也不會初始化)使用。
public native void ensureClassInitialized(Class<?> c);
//定義一個類,此方法會跳過JVM的所有安全檢查,預設情況下,ClassLoader(類載入器)和ProtectionDomain(保護域)例項來源於呼叫者
public native Class<?> defineClass(String name, byte[] b, int off, int len, ClassLoader loader, ProtectionDomain protectionDomain);
//定義一個匿名類
public native Class<?> defineAnonymousClass(Class<?> hostClass, byte[] data, Object[] cpPatches);
  • 例子
public static class Demo {
    private static String name = "jfound";
}
// -----------   
Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) theUnsafeField.get(Unsafe.class);

System.out.println(unsafe.shouldBeInitialized(Demo.class)); //true,為初始化
unsafe.ensureClassInitialized(Demo.class); //初始化
System.out.println(unsafe.shouldBeInitialized(Demo.class)); //fale,已經初始化

Field nameField = Demo.class.getDeclaredField("name");
nameField.setAccessible(true);
long nameAddress = unsafe.staticFieldOffset(nameField);
System.out.println(unsafe.getObject(unsafe.staticFieldBase(nameField), nameAddress));

物件操作

  • api
//返回物件成員屬性在記憶體地址相對於此物件的記憶體地址的偏移量
public native long objectFieldOffset(Field f);
//獲得給定物件的指定地址偏移量的值,與此類似操作還有:getInt,getDouble,getLong,getChar等
public native Object getObject(Object o, long offset);
//給定物件的指定地址偏移量設值,與此類似操作還有:putInt,putDouble,putLong,putChar等
public native void putObject(Object o, long offset, Object x);
//從物件的指定偏移量處獲取變數的引用,使用volatile的載入語義
public native Object getObjectVolatile(Object o, long offset);
//儲存變數的引用到物件的指定的偏移量處,使用volatile的儲存語義
public native void putObjectVolatile(Object o, long offset, Object x);
//有序、延遲版本的putObjectVolatile方法,不保證值的改變被其他執行緒立即看到。只有在field被volatile修飾符修飾時有效
public native void putOrderedObject(Object o, long offset, Object x);
//繞過構造方法、初始化程式碼來建立物件,這個厲害吧
public native Object allocateInstance(Class<?> cls) throws InstantiationException;
  • demo
public static class Demo {
    private String name = "jfound";
    public Demo() {
        System.out.println("建構函式");
    }
}
//--------
Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) theUnsafeField.get(Unsafe.class);

Field nameField = Demo.class.getDeclaredField("name");

Demo demo = (Demo) unsafe.allocateInstance(Demo.class); //建構函式沒呼叫
long offset = unsafe.objectFieldOffset(nameField);
System.out.println(unsafe.getObject(demo, offset)); //列印null,allocateInstance出來的物件,屬性也沒有初始化
System.out.println(unsafe.getObject(new Demo(), offset)); //jfound

有意思的是,allocateInstance 建立出來的物件是沒有呼叫建構函式的,而且物件的屬性也沒有初始化。

陣列相關

  • api
//返回陣列中第一個元素的偏移地址
public native int arrayBaseOffset(Class<?> arrayClass);
//返回陣列中一個元素佔用的大小
public native int arrayIndexScale(Class<?> arrayClass);
  • demo
Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) theUnsafeField.get(Unsafe.class);

System.out.println(unsafe.arrayBaseOffset(int[].class));
System.out.println(unsafe.arrayIndexScale(int[].class));

記憶體屏障

  • api
//記憶體屏障,禁止load操作重排序。屏障前的load操作不能被重排序到屏障後,屏障後的load操作不能被重排序到屏障前
public native void loadFence();
//記憶體屏障,禁止store操作重排序。屏障前的store操作不能被重排序到屏障後,屏障後的store操作不能被重排序到屏障前
public native void storeFence();
//記憶體屏障,禁止load、store操作重排序
public native void fullFence();

系統相關

//返回系統指標的大小。返回值為4(32位系統)或 8(64位系統)。
public native int addressSize();  
//記憶體頁的大小,此值為2的冪次方。
public native int pageSize();

執行緒排程

這部分包括了執行緒的掛起、回覆、鎖等方法,

  • api
//阻塞執行緒
public native void park(boolean isAbsolute, long time);
//取消阻塞執行緒
public native void unpark(Object thread);

park

阻塞當前執行緒直到一個unpark方法出現(被呼叫)、一個用於unpark方法已經出現過(在此park方法呼叫之前已經呼叫過)、執行緒被中斷或者time時間到期(也就是阻塞超時)。在time非零的情況下,如果isAbsolute為true,time是相對於新紀元之後的毫秒,否則time表示納秒

unpark

釋放被park建立的在一個執行緒上的阻塞。這個方法也可以被使用來終止一個先前呼叫park導致的阻塞。這個操作是不安全的,因此必須保證執行緒是存活的(thread has not been destroyed)。

微信搜尋 JFound
回覆 unsafe 獲取 unsafe 的思維導圖
回覆 juc 獲取 juc 的思維導圖

相關文章