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

掃描下方二維碼或者微信搜索公衆號菜鳥飛呀飛,便可關注微信公衆號,閱讀更多Spring源碼分析Java併發編程文章。java

微信公衆號

  在上一篇文章《初始CAS的實現原理》中,提到了Unsafe類相關方法,今天這篇文章將詳細介紹Unsafe類的源碼。   爲何要單獨用一篇文章介紹Unsafe類呢?這是由於在看源碼過程當中,常常會碰到它,例如JUC包下的原子類、AQS、Netty等源碼中,最終都會看見Unsafe類的使用。搞清楚Unsafe類的使用,對之後看源碼會有很大的幫助。程序員

1. Unsafe類簡介

  • Unsafe類是rt.jarsun.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表達式的實現是由字節碼指令invokedynimicVM 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類API
相關文章
相關標籤/搜索