Java UnSafe 類使用介紹

1. UnSafe 簡介

UnsafeJava 整個併發包底層實現的核心,它具有像 C++ 的指針一樣直接操作內存的能力,而這也就意味着其越過了 JVM 的限制。Unsafe 的特性可歸結如下,它雖然在一定程度上提升了效率,但是也帶來了指針的不安全性

  1. Unsafe 不受 JVM 管理,也就無法被自動 GC,需要手動 GC,容易出現內存泄漏
  2. Unsafe 的大部分方法中必須提供原始地址(內存地址)和被替換對象的地址,偏移量需自行計算,一旦出現問題必然是 JVM 崩潰級別的異常,會導致整個應用程序直接 crash
  3. 直接操作內存,也意味着其速度更快,在高併發的條件之下能夠很好地提高效率

2. UnSafe 功能與使用示例

Unsafe 類的構造函數是私有的,而對外提供的靜態方法Unsafe#getUnsafe() 又對調用者的 ClassLoader 有限制 ,如果這個方法的調用者不是由 Boot ClassLoader 加載的,則會報錯

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

因爲 Java 源碼中由開發者自定義的類都是由 Appliaction ClassLoader 加載的,所以正常情況下我們無法直接使用Unsafe ,如果需要使用它,則需要利用反射

public static Unsafe getUnsafe() {
        Unsafe unsafe = null;
        try {
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            unsafe = (Unsafe) field.get(null);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return unsafe;
    }

2.1 操作對象屬性

方法 功能
public native Object getObject(Object o, long offset) 獲取一個Java 對象中偏移地址爲 offset 的屬性的值,此方法可以突破修飾符的限制,類似的方法有 getInt()、getDouble()等,同理還有 putObject() 方法
public native Object getObjectVolatile(Object o, long offset) 強制從主存中獲取目標對象指定偏移量的屬性值,類似的方法有getIntVolatile()、getDoubleVolatile()等,同理還有putObjectVolatile()
public native void putOrderedObject(Object o, long offset, Object x) 設置目標對象中偏移地址 offset 對應的對象類型屬性的值爲指定值。這是一個有序或者有延遲的 putObjectVolatile()方法,並且不保證值的改變被其他線程立即看到。只有在屬性被volatile修飾並且期望被修改的時候使用纔會生效,類似的方法有 putOrderedInt() 和 putOrderedLong()
public native long objectFieldOffset(Field f) 返回給定的非靜態屬性在它的類的存儲分配中的位置(偏移地址),然後可根據偏移地址直接對屬性進行修改,可突破屬性的訪問修飾符限制

private String name;

@Test
public void test() {
        Unsafe unsafe = getUnsafe();
        try {
            DirectMemory directMemory = (DirectMemory) unsafe.allocateInstance(DirectMemory.class);
            long nameOffset = unsafe.objectFieldOffset(DirectMemory.class.getDeclaredField("name"));
            unsafe.putObject(directMemory, nameOffset, "luck");
            System.out.println(directMemory.getName());
        } catch (Exception e) {
            e.printStackTrace();
        }
}

2.2 操作數組元素

方法 功能
public native int arrayBaseOffset(Class arrayClass) 返回數組類型的第一個元素的偏移地址(基礎偏移地址)
public native int arrayIndexScale(Class arrayClass) 返回數組中元素與元素之間的偏移地址的增量,配合 arrayBaseOffset() 使用就可以定位到任何一個元素的地址

private String[] names = {"nathan", "goog", "luck"};
  
@Test
public void test() {
        Unsafe unsafe = getUnsafe();
        try {
             Class<?> a = String[].class;
             int base = unsafe.arrayBaseOffset(a);
             int scale = unsafe.arrayIndexScale(a);
             // base + i * scale 即爲字符串數組下標 i 在對象的內存中的偏移地址
             System.out.println(unsafe.getObject(names, (long) base + 2 * scale));
        } catch (Exception e) {
            e.printStackTrace();
        }
}

ForkJoinPool 中有一種新的元素定位算法,這種方法要求 scale 也就是元素的內存大小爲 2 的次冪。假如 scale 等於 4,那麼 ASHIFT 值爲 2,乘以 4 和向左移 2 位結果是一樣的,但是移位操作顯然更高效

ASHIFT = 31 - Integer.numberOfLeadingZeros(scale)
offset = (i << ASHIFT) + ABASE

2.3 內存地址操作

方法 功能
public native int addressSize() 獲取本地指針的大小(單位是byte),通常值爲 4 或者 8
public native int pageSize() 獲取本地內存的頁數,此值爲2的冪次方
public native long allocateMemory(long bytes) 分配一塊新的本地內存,通過bytes指定內存塊的大小(單位是byte),返回新開闢的內存的地址
public native long reallocateMemory(long address, long bytes) 通過指定的內存地址 address 重新調整本地內存塊的大小,調整後的內存塊大小通過bytes指定(單位爲byte)
public native void setMemory(Object o, long offset, long bytes, byte value) 將給定內存塊中的所有字節設置爲固定值(通常是0)
public class DirectMemoryBuffer {
    private final static int BYTE = 1;
    private long size;
    private long address;
    private Unsafe unsafe;

    public DirectMemoryBuffer(long size, Unsafe unsafe) {
        this.size = size;
        this.unsafe = unsafe;
        address = unsafe.allocateMemory(size * BYTE);
    }

    public void set(long i, byte value) {
        unsafe.putByte(address + i * BYTE, value);
    }

    public int get(long idx) {
        return unsafe.getByte(address + idx * BYTE);
    }

    public long size() {
        return size;
    }

    public void freeMemory() {
        unsafe.freeMemory(address);
    }
}

2.4 CAS 操作

CAS 是一種樂觀鎖機制,它不需要搶佔鎖,能有效地提高效率,依賴於硬件的原子操作實現

方法 功能
public final native boolean compareAndSwapObject(Object target, long offset, Object exceptData, Object targetData) 其作用爲比較目標對象指定偏移量的屬性的期望值與主存中的值,如果二者相等,則將主存中的值更新爲目標值。同樣還有 compareAndSwapInt() 和 compareAndSwapLong()
public class CASCounter {
    private Unsafe unsafe;
    // count 需要聲明爲 volatile 來保證對所有線程可見
    private volatile long count = 0;
    private long offset;

    public CASCounter(Unsafe unsafe) {
        this.unsafe = unsafe;
        try {
            offset = unsafe.objectFieldOffset(CASCounter.class.getDeclaredField("count"));
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
    }

    // 關鍵的 increment 方法,其在 while 循環裏不斷嘗試調用 compareAndSwapLong,在方法內部累加count的同時
    // 檢查其值有沒有被其他線程改變。如沒有,就提交更改,如果不一致,那麼繼續嘗試提交更改
    public void increment() {
        long before = count;
        while (!unsafe.compareAndSwapLong(this, offset, before, before + 1)) {
            before = count;
        }
    }

    public long getCount() {
        return count;
    }
}

2.5 線程掛起與恢復

方法 功能
public native void park(boolean isAbsolute, long time) 阻塞當前線程,一直等到 unpark()方法被調用或者超時,和 Object.await()非常類似,但是 park 是操作系統調用,因此在某些操作系統架構上這會帶來更好的性能
public native void unpark(Object thread) 喚醒被 park 阻塞的線程,由於其不安全性,因此必須保證線程是存活的

2.6 內存屏障

方法 功能
public native void loadFence() 如果不要volatile去增加內存屏障,即可用該方法手動增加屏障。在該方法之前的所有讀操作,一定在load屏障之前執行完成
public native void storeFence() 在該方法之前的所有寫操作,一定在store屏障之前執行完成
public native void fullFence() 在該方法之前的所有讀寫操作,一定在 full 屏障之前執行完成,這個內存屏障相當於上面兩個(load屏障和store屏障)的合體功能

2.7 Class 相關

方法 功能
public native boolean shouldBeInitialized(Class<?> c) 判斷是否需要初始化一個類,通常在獲取一個類的靜態屬性的時候使用(一個類如果沒初始化,它的靜態屬性也不會初始化)。 當且僅當ensureClassInitialized方法不生效時返回false
public native void ensureClassInitialized(Class<?> c) 檢測給定的類是否已經初始化,通常在獲取一個類的靜態屬性的時候使用(因爲一個類如果沒初始化,它的靜態屬性也不會初始化)
public native Class<?> defineClass(String name, byte[] b, int off, int len, ClassLoader loader, ProtectionDomain protectionDomain) 定義一個類,此方法會跳過JVM的所有安全檢查,默認情況下 ClassLoader(類加載器)和 ProtectionDomain(保護域)實例來源於調用者
public native Class<?> defineAnonymousClass(Class<?> hostClass, byte[] data, Object[] cpPatches) 定義一個匿名類
public native long staticFieldOffset(Field f) 返回給定的靜態屬性在它的類的存儲分配中的位置(偏移地址)
public native Object staticFieldBase(Field f) 返回給定的靜態屬性的位置,配合 staticFieldOffset()方法使用
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章