天天看點

Unsafe類的源碼解讀以及使用場景

掃描下方二維碼或者微信搜尋公衆号

菜鳥飛呀飛

,即可關注微信公衆号,閱讀更多

Spring源碼分析

Java并發程式設計

文章。
Unsafe類的源碼解讀以及使用場景

文章目錄

      • 1. Unsafe類簡介
      • 2. 如何擷取Unsafe類的執行個體
      • 3. Unsafe功能介紹以及實際應用
        • 3.1 CAS操作
        • 3.2 記憶體操作
        • 3.3 線程排程相關
        • 3.4 數組相關
        • 3.5 對象相關操作
        • 3.6 Class相關操作
        • 3.7 記憶體屏障相關
        • 3.8 系統相關
      • 4. 總結

  在上一篇文章《初始CAS的實作原理》中,提到了Unsafe類相關方法,今天這篇文章将詳細介紹Unsafe類的源碼。

  為什麼要單獨用一篇文章介紹Unsafe類呢?這是因為在看源碼過程中,經常會碰到它,例如JUC包下的原子類、AQS、Netty等源碼中,最終都會看見Unsafe類的使用。搞清楚Unsafe類的使用,對以後看源碼會有很大的幫助。

1. Unsafe類簡介

  • Unsafe類是

    rt.jar

    sun.misc

    包下的類,從類名就能看出來,這個類是不安全的,但是它的功能十分強大。相比C和C++的開發人員,作為一名Java開發人員是十分幸福的,因為在Java中程式員在開發時不需要關注記憶體的管理,對象的回收,因為JVM全部都幫助我們完成了。如果Java開發人員需要自己手動去操作記憶體,那麼可以通過Unsafe類去進行申請,這也是Unsafe類被定義為

    不安全

    的類的原因,因為一不小心就容易出現

    忘記釋放記憶體

    等問題。
  • Unsafe類中方法很多,但大緻可以分為8大類。CAS操作、記憶體操作、線程排程、數組相關、對象相關操作、Class相關操作、記憶體屏障相關、系統相關。

    筆者畫了一張腦圖,因為圖檔占用空間較大,為了不影響閱讀,我把這張圖放在了文章末尾,以供參考。

2. 如何擷取Unsafe類的執行個體

  • Unsafe類被final修飾了,表示Unsafe不能被繼承;同時Unsafe的構造方法用private修飾,表示外部無法直接通過構造方法去建立執行個體。實際上Unsafe是一個單例對象,下面是Unsafe類的部分源碼。
// 類被final修飾,表示不能被繼承
public final class Unsafe {

	// 構造器被私有化
    private Unsafe() {}

    private static final Unsafe theUnsafe = new Unsafe();

    public static Unsafe getUnsafe() {
        Class<?> caller = Reflection.getCallerClass();
        if (!VM.isSystemDomainLoader(caller.getClassLoader()))
            throw new SecurityException("Unsafe");
        return theUnsafe;
    }
}
           
  • 雖然Unsafe是一個單例,但是我們在自己開發的類中無法通過Unsafe.getUnsafe()擷取到Unsafe的執行個體,在程式運作時會抛出

    SecurityException

    異常。例如如下示例:
public class Demo {

    public static void main(String[] args) {
        Unsafe unsafe = Unsafe.getUnsafe();
    }
}
           
  • 運作main()方法,最終在控制台出現如下運作時異常:
Exception in thread "main" java.lang.SecurityException: Unsafe
    at sun.misc.Unsafe.getUnsafe(Unsafe.java:90)
    at com.tiantang.study.Demo.main(Demo.java:14)
           
  • 為什麼會出現

    SecurityException

    異常呢?這是因為在Unsafe類的

    getUnsafe()

    方法中,它做了一層校驗,判斷目前類(Demo)的類加載器(ClassLoader)

    是不是啟動類加載器(Bootstrap ClassLoader)

    ,如果不是,則會抛出

    SecurityException

    異常。在JVM的類加載機制中,自定義的類使用的類加載器是

    應用程式類加載器(Application ClassLoader)

    ,是以這個時候校驗失敗,會抛出異常。
  • 那麼如何才能擷取到Unsafe類的執行個體呢?有兩種方案。
  • 第一方案:将我們自定義的類(如Demo類)所在的jar包所在的路徑通過-Xbootclasspath參數添加到Java指令中,這樣當程式啟動時,Bootstrap ClassLoader會加載Demo類,這樣校驗就通過了。顯然這種方式比較麻煩,而且不太實用,因為在項目中,可能需要在很多地方都使用Unsafe類,如果通過Java指令行這種方式去指定,就會很麻煩,而且容易出現纰漏。
  • 第二種方案:通過反射來建立Unsafe類的執行個體(

    反射反射,程式員的快樂

    )。反射的代碼可以參考如下示例:
public static void main(String[] args) {
    try {
        Field field = Unsafe.class.getDeclaredField("theUnsafe");
        // 将字段的通路權限設定為true
        field.setAccessible(true);
        // 因為theUnsafe字段在Unsafe類中是一個靜态字段,是以通過Field.get()擷取字段值時,可以傳null擷取
        Unsafe unsafe = (Unsafe) field.get(null);
        // 控制台能列印出對象哈希碼
        System.out.println(unsafe);
    } catch (Exception e) {
        e.printStackTrace();
    }
}
           

3. Unsafe功能介紹以及實際應用

  • 下面将Unsafe類的API分為8大類,針對每一類操作的API方法以及常見的應用場景作介紹。

3.1 CAS操作

  • 在Java的鎖中,經常會出現CAS操作,它們最終都調用了Unsafe類中的CAS操作方法,

    compareAndSwapInt()、compareAndSwapLong()、compareAndSwapObject()

    這三個CAS方法都是native方法,具體實作是在JVM中實作,它們的作用是比較并交換,這個操作是原子操作。關于CAS更詳細的講解可以參考這篇文章:初識CAS的實作原理。
  • Unsafe在隊列同步器AQS(AbstractQueuedSynchronizer)、原子類中都有應用,現在以隊列同步器AQS為例,看看AQS當中是如何使用Unsafe類的。
  • 在AQS中擷取同步狀态時,如果目前線程能擷取到鎖,那麼就會去嘗試修改同步狀态state的值,這個時候就用到了Unsafe類。compareAndSetState()是AQS類中的一個方法,它實際調用的是Unsafe類的compareAndSwapInt()方法。
protected final boolean compareAndSetState(int expect, int update) {
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
           

3.2 記憶體操作

  • Unsafe能直接操作記憶體,它能直接進行申請記憶體、釋放記憶體、記憶體拷貝等操作。值得注意的是Unsafe直接申請的記憶體是堆外記憶體。何謂堆外記憶體呢?堆外是相對于JVM的記憶體來說的,通常我們應用程式運作後,建立的對象均在JVM記憶體中的堆中,堆記憶體的管理是JVM來管理的,而堆外記憶體指的是計算機中的直接記憶體,不受JVM管理。是以使用Unsafe類來申請對外記憶體時,要特别注意,否則容易出現記憶體洩漏等問題。
  • Unsafe類對記憶體的操作在網絡通信架構中應用廣泛,如:Netty、MINA等通信架構。在java.nio包中的DirectByteBuffer中,記憶體的申請、釋放等邏輯都是調用Unsafe類中的對應方法來實作的。下面是DirectByteBuffer類的部分源碼。
DirectByteBuffer(int cap) {                   

    super(-1, 0, cap, cap);
    boolean pa = VM.isDirectMemoryPageAligned();
    int ps = Bits.pageSize();
    long size = Math.max(1L, (long)cap + (pa ? ps : 0));
    Bits.reserveMemory(size, cap);

    long base = 0;
    try {
        // 調用unsafe申請記憶體
        base = unsafe.allocateMemory(size);
    } catch (OutOfMemoryError x) {
        Bits.unreserveMemory(size, cap);
        throw x;
    }
    // 初始化記憶體
    unsafe.setMemory(base, size, (byte) 0);
    if (pa && (base % ps != 0)) {
        // Round up to page boundary
        address = base + ps - (base & (ps - 1));
    } else {
        address = base;
    }
    cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
    att = null;

}
           
  • Netty作為一個高性能架構,它有一個特點就是“零拷貝”,操作的是堆外記憶體。在操作堆外記憶體時,它最終使用的DirectByteBuffer來對堆外記憶體進行操作的。例如Netty架構中

    io.netty.buffer.UnpooledUnsafeDirectByteBuf

    類申請記憶體時的源碼如下:
public class UnpooledUnsafeDirectByteBuf extends AbstractReferenceCountedByteBuf {
    protected ByteBuffer allocateDirect(int initialCapacity) {
        // 調用ButeBuffer來申請堆外記憶體,ButeBuffer是java.nio包下的内
        return ByteBuffer.allocateDirect(initialCapacity);
    }
}
           
  • java.nio.ByteBuffer

    類是通過DirectByteBuffer類來操作記憶體,DirectByteBuffer又是通過Unsafe類來操作記憶體,是以最終實際上Netty對堆外的記憶體的操作是通過Unsafe類中的API來實作的。

3.3 線程排程相關

  • Unsafe中提供了兩個和線程排程相關的native方法,分别是park()和unPark(),它們的作用分别是阻塞線程、喚醒線程。在JUC包下實作的鎖中,通常會用到

    LockSupport.park()、LockSupport.unpark()

    方法來進行線程間的通信。LockSupport中的這些方法最終調用的是Unsafe類的park()和unPark()。下面是LockSupport類的部分源代碼。
public class LockSupport {
    
    // UNSAFE是Unsafe類的執行個體
    public static void park() {
    	// 阻塞線程
        UNSAFE.park(false, 0L);
    }

    public static void unpark(Thread thread) {
        if (thread != null)
        	// 喚醒線程
            UNSAFE.unpark(thread);
    }

}
           

3.4 數組相關

  • Unsafe類中和數組相關的方法有兩個:

    arrayBaseOffset()、arrayIndexScale()

// 傳回數組中第一個元素在記憶體中的偏移量
public native int arrayBaseOffset(Class<?> arrayClass);
// 傳回數組中每個元素占用的記憶體大小,機關是位元組
public native int arrayIndexScale(Class<?> arrayClass);
           
  • 根據這兩個方法能計算出數組中的每一個元素在記憶體中的偏移量。下面通過AtomicIntegerArray類的源碼來說明Unsafe類對數組的操作。AtomicIntegerArray類的部分源碼如下:
public class AtomicIntegerArray implements java.io.Serializable {
    private static final long serialVersionUID = 2862133569453604235L;

    private static final Unsafe unsafe = Unsafe.getUnsafe();
    // 擷取數組中第一進制素在記憶體中的偏移量
    private static final int base = unsafe.arrayBaseOffset(int[].class);
    private static final int shift;
    private final int[] array;

    static {
        // 擷取數組中每個元素占用的記憶體大小
        // 對于int類型的元素,占用的是4個位元組大小,是以此時傳回的是4
        int scale = unsafe.arrayIndexScale(int[].class);
        if ((scale & (scale - 1)) != 0)
            throw new Error("data type scale not a power of two");
        shift = 31 - Integer.numberOfLeadingZeros(scale);
    }

    private static long byteOffset(int i) {
        // 根據數組中第一個元素在記憶體中的偏移量和每個元素占用的大小,
        // 計算出數組中第i個元素在記憶體中的偏移量
        return ((long) i << shift) + base;
    }
}
           

3.5 對象相關操作

  • Unsafe類中提供了很多操作對象執行個體的方法。這些方法基本都是成對出現的,例如

    getObject()和putObject()

    ,一個是從記憶體中擷取給定對象的指定偏移量的Object類型對象,一個是向記憶體中寫。與此類似的還有getInt()、getLong()…等方法。還有一組加了volatile語義的方法,例如:

    getObjectValotile()、putObjectVolatile()

    ,它們的作用就是使用volatile語義擷取值和存儲值。什麼是volatile語義呢?就是讀資料時每次都從記憶體中取最新的值,而不是使用CPU緩存中的值;存資料時将值立馬重新整理到記憶體,而不是先寫到CPU緩存,等以後再重新整理回記憶體。部分方法注釋如下:
//從對象o的指定位址偏移量offset處擷取變量的引用,與此類似方法有:getInt,getLong等等
public native Object getObject(Object o, long offset);
//對對象o的指定位址偏移量offset處設值,與此類似方法有:putInt,putLong等等
public native void putObject(Object o, long offset, Object x);
//從對象o的指定位址偏移量offset處擷取變量的引用,使用volatile語義讀取,與此類似方法有:getIntVolatile,getLongVolatile等等
public native Object getObjectVolatile(Object o, long offset);
//對對象o的指定位址偏移量offset處設值,使用volatile語義存儲,與此類似方法有:putIntVolatile,putLongVolatile等等
public native void putObjectVolatile(Object o, long offset, Object x);
           
  • 和對象相關操作的方法還有一個十分常用的方法:

    objectFieldOffset()

    。它的作用是擷取對象的某個非靜态字段

    相對于該對象

    的偏移位址,它與

    staticFieldOffset()

    的作用類似,但是存在一點差別。staticFieldOffset()擷取的是靜态字段相對于類對象(即類所對應的Class對象)的偏移位址。靜态字段存在于方法區中,靜态字段每次擷取的偏移量的值都是相同的。
// 擷取對象的某個非靜态字段相對于該對象的偏移位址
public native long objectFieldOffset(Field f);
           
  • objectFieldOffset()

    的應用場景十分廣泛,因為在Unsafe類中,大部分API方法都需要傳入一個offset參數,這個參數表示的是偏移量,要想直接操作記憶體中某個位址的資料,就必須先找到這個資料在哪兒,而通過offset就能知道這個資料在哪兒。是以這個方法應用得十分廣泛,下面以AtomicInteger類為例:在靜态代碼塊中,通過

    objectFieldOffset()

    擷取了value屬性在記憶體中的偏移量,這樣後面将value寫入到記憶體時,就能根據offset來寫入了。
public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;

    // setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
        try {
            // 在static靜态塊中調用objectFieldOffset()方法,擷取value字段在記憶體中的偏移量
            // 因為後面AtomicInteger在進行原子操作時,需要調用Unsafe類的CAS方法,而這些方法均需要傳入offset這個參數
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    private volatile int value;
}
           

3.6 Class相關操作

  • Unsafe類中提供了一些和Class操作相關的方法,例如擷取靜态字段在記憶體中的偏移量的方法:

    staticFieldOffset()

    ,擷取靜态字段的對象指針:

    staticFieldBase()

// 擷取給定靜态字段的偏移量
public native long staticFieldOffset(Field f);

// 擷取給定靜态字段的對象指針
public native Object staticFieldBase(Field f);
           
  • 另外在JDK1.8開始,Java開始支援lambda表達式,而lambda表達式的實作是由位元組碼指令

    invokedynimic

    VM Anonymous Class

    模闆機制來實作的,

    VM Anonymous Class

    模闆機制最終會使用到Unsafe類的defineAnonymousClass()方法來建立匿名類。對這一塊感興趣的朋友可以去查閱一下相關的資料,歡迎分享。
// 定義一個匿名内部類
 public native Class<?> defineAnonymousClass(Class<?> hostClass, byte[] data, Object[] cpPatches);
           

3.7 記憶體屏障相關

  • Unsafe類從JDK1.8開始,提供了三個和記憶體屏障相關的API方法。分别是

    loadFence()、 storeFence() 、fullFence()

// 禁止load操作重排序
public native void loadFence();

// 禁止store操作重排序
public native void storeFence();

// 禁止load和store操作重排序
public native void fullFence();
           

3.8 系統相關

  • Unsafe類中提供了兩個和系統相關的API方法。
// 擷取指針的大小,機關是位元組。
// 對于64位系統,傳回8,表示指針大小是8位元組
// 對于32位系統,傳回4,表示指針大小是4位元組
public native int addressSize();

// 傳回記憶體頁的大小,機關是位元組。傳回值一定是2的多少次幂
public native int pageSize();
           
  • 例如如下示例,是在筆者電腦上運作的結果:
public static void main(String[] args) {
    Unsafe unsafe = null;
    try {
        Field field = Unsafe.class.getDeclaredField("theUnsafe");
        field.setAccessible(true);
        unsafe = (Unsafe) field.get(null);
    } catch (Exception e) {
        e.printStackTrace();
    }
    // 指針大小
    System.out.println(unsafe.addressSize());
    // 記憶體頁大小
    System.out.println(unsafe.pageSize());
}
           
  • 控制台列印結果如下。筆者電腦的指針大小為8位元組,記憶體頁大小為4096位元組,即4KB。
8
4096
           
  • 這兩個方法在

    java.nio.Bits

    類中有實際應用。Bits作為工具類,提供了計算所申請記憶體需要占用多少記憶體頁的方法,這個時候需要知道硬體的記憶體頁大小,才能計算出占用記憶體頁的數量。是以在這裡借助了Unsafe.pageSize()方法來實作。

    Bits

    類的部分源碼如下。
class Bits { 
    static int pageSize() {
        if (pageSize == -1)
        	// 擷取記憶體頁大小
            pageSize = unsafe().pageSize();
        return pageSize;
    }

    // 根據記憶體大小,計算需要的記憶體頁數量
    static int pageCount(long size) {
        return (int)(size + (long)pageSize() - 1L) / pageSize();
    }  
}
           

4. 總結

  • 本文詳細介紹了Unsafe類的使用,以及各類API方法的作用和應用場景。對于Java中并發程式設計,Java的源碼裡面存着這大量的Unsafe類的使用,主要使用的是和CAS操作相關的三個方法,是以搞清楚這三個方法,對看懂Java并發程式設計的源碼有很大幫助。
  • 另外Unsafe類中

    objectFieldOffset(Field f)

    這個方法很常用,它是擷取字段在記憶體中的偏移量,通常和Unsafe類中的其他方法結合使用。通過這個方法能知道要修改的資料在記憶體中的位置,然後再通過Unsafe類中其他方法來根據資料在記憶體中的位置進而來修改資料。
  • 看完本篇文章,相信你現在應該能看懂JUC包中的很多源碼了。
  • 關于CAS相關的介紹可以參考另一篇文章。初識CAS的實作原理
Unsafe類的源碼解讀以及使用場景