神奇的Unsafe,你get了嗎

本文原創地址,個人博客jsbintask的博客(食用效果最佳),轉載請註明出處!java

簡介

Unsafe是jdk提供的一個直接訪問操做系統資源的工具類(底層c++實現),它能夠直接分配內存,內存複製,copy,提供cpu級別的CAS樂觀鎖等操做。它的目的是爲了加強java語言直接操做底層資源的能力,無疑帶來不少方便。可是,使用的同時就得額外當心!它的整體做用以下(圖片來源網絡): c++

Unsafe

Unsafe位於sun.misc包下,jdk中的併發編程包juc(java.util.concurrent)基本所有靠Unsafe實現,因而可知其重要性。算法

基本使用

Unsafe被設計爲單例,而且只容許被引導類加載器(BootstrapClassLoader)加載的類使用: 編程

Unsafe
因此咱們本身寫的類是沒法直接經過 Unsafe.getUnsafe()獲取的。固然,既然是java代碼,咱們就可使用一點 歪道,好比經過反射直接new一個或者將其內部靜態成員變量 theUnsafe獲取出來:

public static void main(String[] args) throws Exception{
    // method 1
    Class<Unsafe> unsafeClass = Unsafe.class;
    Constructor<Unsafe> constructor = unsafeClass.getDeclaredConstructor();
    constructor.setAccessible(true);
    Unsafe unsafe1 = constructor.newInstance();
    System.out.println(unsafe1);

    // method2
    Field theUnsafe = unsafeClass.getDeclaredField("theUnsafe");
    theUnsafe.setAccessible(true);
    Unsafe unsafe2 = (Unsafe) theUnsafe.get(null);
    System.out.println(unsafe2);
}
複製代碼

Unsafe
如今咱們可以在本身代碼裏面使用Unsafe了,接下來看下它的使用以及jdk使用操做的。

CAS

CAS譯爲Compare And Swap,它是樂觀鎖的一種實現。假設內存值爲v,預期值爲e,想要更新成得值爲u,當且僅當內存值v等於預期值e時,纔將v更新爲u。 這樣能夠有效避免多線程環境下的同步問題。數組

在unsafe中,實現CAS算法經過cpu的原子指令cmpxchg實現,它對應的方法以下: 網絡

Unsafe
簡單介紹下它使用的參數, var1爲內存中要操做的對象, var2爲要操做的值的內存地址偏移量, var4爲預期值, var5爲想要更新成的值。

爲了方便理解,舉個栗子。類User有一個成員變量name。咱們new了一個對象User後,就知道了它在內存中的起始值,而成員變量name在對象中的位置偏移是固定的。這樣經過這個起始值和這個偏移量就可以定位到name在內存中的具體位置。多線程

因此咱們如今的問題就是如何得出name在對象User中的偏移量,Unsafe天然也提供了相應的方法: 併發

Unsafe
他們分別爲獲取靜態成員變量,成員變量的方法,因此咱們可使用unsafe直接更新內存中的值:

public class UnsafeTest {
    public static void main(String[] args) throws Exception {
        Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
        theUnsafe.setAccessible(true);
        Unsafe unsafe = (Unsafe) theUnsafe.get(null);

        User user = new User("jsbintask");
        long nameOffset = unsafe.objectFieldOffset(User.class.getDeclaredField("name"));
        unsafe.compareAndSwapObject(user, nameOffset, "jsbintask1", "jsbintask2");
        System.out.println("第一次更新後的值:" + user.getName());
        unsafe.compareAndSwapObject(user, nameOffset, "jsbintask", "jsbintask2");
        System.out.println("第二次更新後的值:" + user.getName());
    }
}

class User {
    private String name;

    public User(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}
複製代碼

Unsafe
由於內存中name的值爲"jsbintask",而第一次使用 compareAndSwapObject方法預期值爲"jsbintask1",這顯然是不相等的,因此第一次更新失敗,第二次咱們傳入了正確的預期值,更新成功!

若是咱們分析juc包下的Atomic開頭的原子類就會發現,它內部的原子操做所有來源於unsafe的CAS方法,好比AtomicInteger的getAndIncrement方法,內部直接調用unsafe的getAndAddInt方法,它的實現原理爲:cas失敗,就循環,直到成功爲止,這就是咱們所說的自旋鎖app

Unsafe

內存分配

Unsafe還給咱們提供了直接分配內存,釋放內存,拷貝內存,內存設置等方法,值得注意的是,這裏的內存指的是堆外內存!它是不受jvm內存模型掌控的,因此使用須要及其當心:jvm

//分配內存, 至關於C++的malloc函數
public native long allocateMemory(long bytes);
//釋放內存
public native void freeMemory(long address);
//在給定的內存塊中設置值
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);
//爲給定地址設置值,忽略修飾限定符的訪問限制,與此相似操做還有: putInt,putDouble,putLong,putChar等
public native void putObject(Object o, long offset, Object x);
複製代碼

咱們能夠寫一段代碼驗證一下:

public static void main(String[] args) throws Exception {
    Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
    theUnsafe.setAccessible(true);
    Unsafe unsafe = (Unsafe) theUnsafe.get(null);

    // 分配 10M的堆外內存
    long _10M_Address = unsafe.allocateMemory(1 * 1024 * 1024 * 10);
    // 將10M內存的 前面1M內存值設置爲10
    unsafe.setMemory(_10M_Address, 1 * 1024 * 1024 * 1, (byte) 10);
    // 獲取第1M內存的值: 10
    System.out.println(unsafe.getByte(_10M_Address + 1000));
    // 獲取第1M內存後的值: 0(沒有設置)
    System.out.println(unsafe.getByte(_10M_Address + 1 * 1024 * 1024 * 5));
}
複製代碼

Unsafe
咱們分配了10M內存,而且將前1M內存的值設置爲了10,取出了內存中的值進行比較,驗證了unsafe的方法。

堆外內存不受jvm內存模型掌控,在nio(netty,mina)中大量使用對外內存進行管道傳輸,copy等,使用它們的好處以下:

  • 對垃圾回收停頓的改善。因爲堆外內存是直接受操做系統管理而不是JVM,因此當咱們使用堆外內存時,便可保持較小的堆內內存規模。從而在GC時減小回收停頓對於應用的影響。
  • 提高程序I/O操做的性能。一般在I/O通訊過程當中,會存在堆內內存到堆外內存的數據拷貝操做,對於須要頻繁進行內存間數據拷貝且生命週期較短的暫存數據,都建議存儲到堆外內存。 而在jdk中,堆外內存對應的類爲DirectByteBuffer,它內部也是經過unsafe分配的內存:
    Unsafe
    這裏值得注意的是,對外內存的回收藉助了Cleaner這個類。

線程調度

經過Unsafe還能夠直接將某個線程掛起,這和調用Object.wait()方法做用是同樣的,可是效率確更高!

Unsafe
咱們熟知的AQS( AbstractQueuedSynchronizer)內部掛起線程使用了 LockSupport方法,而LockSupport內部依舊使用的是Unsafe:
Unsafe
咱們一樣能夠寫一段代碼驗證:

public static void main(String[] args) throws Exception {
    Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
    theUnsafe.setAccessible(true);
    Unsafe unsafe = (Unsafe) theUnsafe.get(null);

    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 10; i++) {
            if (i == 5) {
                // i == 5時,將當前線程掛起
                unsafe.park(false, 0L);
            }
            System.out.println(Thread.currentThread().getName() + " printing i : " + i);
        }
    }, " Thread__Unsafe__1");

    t1.start();

    // 主線程休息三秒
    Thread.sleep(3000L);
    for (int i = 0; i < 10; i++) {
        System.out.println(Thread.currentThread().getName() + " printing i : " + i);
        if (i == 9) {
            // 將線程 t1 喚醒
            unsafe.unpark(t1);
        }
    }

    System.in.read();
}
複製代碼

Unsafe
當線程t1運行到i=5時,被掛起,主線程執行,而主線程運行到i=9時,將t1喚醒,t1繼續打印! 在park出debug能夠觀察t1線程的狀態:
Unsafe
Unsafe

數組操做

對於數組,Unsafe提供了特別的方法返回不一樣類型數組在內存中的偏移量:

Unsafe
arrayBaseOffset方法返回數組在內存中的偏移量,這個值是固定的。 arrayIndexScale返回數組中的每個元素的內存地址換算因子。舉個栗子,double數組(注意不是包裝類型)每一個元素佔用8個字節,因此換算因子爲8,int類型則爲4,經過這兩個方法咱們就能定位數組中每一個元素的內存地址,從而賦值,下面代碼演示:

public static void main(String[] args) throws Exception{
    Class<Unsafe> unsafeClass = Unsafe.class;
    Constructor<Unsafe> constructor = unsafeClass.getDeclaredConstructor();
    constructor.setAccessible(true);
    Unsafe unsafe = constructor.newInstance();

    Integer[] integers = new Integer[10];
    // 打印數組的原始值
    System.out.println(Arrays.toString(integers));
    // 獲取Integer數組在內存中的固定的偏移量
    long arrayBaseOffset = unsafe.arrayBaseOffset(Integer[].class);
    System.out.println(unsafe.arrayIndexScale(Integer[].class));
    System.out.println(unsafe.arrayIndexScale(double[].class));
    // 將數組中第一個元素的更新爲100
    unsafe.putObject(integers, arrayBaseOffset, 100);
    // 將數組中第五個元素更新爲50 注意 引用類型佔用4個字節,因此內存地址 須要 4 * 4 = 16
    unsafe.putObject(integers, arrayBaseOffset + 16, 50);
    // 打印更新後的值
    System.out.println(Arrays.toString(integers));
}
複製代碼

Unsafe
咱們經過獲取Integer數組的內存偏移量,結合換算因子將第一個元素,第五個元素分別替換爲了100,50。驗證了咱們的說法。

數組的原子操做,juc包也已經提供了相應的工具類,好比AtomicIntegerArray內部就是同過Unsafe的上述方法實現了數組的原子操做。

Unsafe

其它操做

Unsafe還提供了操做系統級別的方法如獲取內存頁的大小public native int pageSize();,獲取系統指針大小public native int addressSize(); jdk8還加入了新的方法,內存屏障,它的目的是爲了防止指令重排序(編譯器爲了優化速度,會在保證單線程不出錯的狀況下將某些代碼的順序調換,好比先分配內存,或者先返回引用等,這樣在多線程環境下就會出錯):

//內存屏障,禁止load操做重排序。屏障前的load操做不能被重排序到屏障後,屏障後的load操做不能被重排序到屏障前
public native void loadFence();
//內存屏障,禁止store操做重排序。屏障前的store操做不能被重排序到屏障後,屏障後的store操做不能被重排序到屏障前
public native void storeFence();
//內存屏障,禁止load、store操做重排序
public native void fullFence();
複製代碼

jdk1.8引入的StampedLock就是基於此實現的樂觀讀寫鎖. 另外,jdk1.8引入了lambda表達式,它其實會幫咱們調用Unsafe的public native Class<?> defineAnonymousClass(Class<?> var1, byte[] var2, Object[] var3);方法生成匿名內部類,以下面的代碼:

public class UnsafeTest2 {
    public static void main(String[] args) {
        Function<String, Integer> function = Integer::parseInt;
        System.out.println(function.apply("100"));
    }
}
複製代碼

查看字節碼:

Unsafe
發現它調用了 LambdaMetafactory.metafactory方法,最終調用了 InnerClassLambdaMetafactory的spinInnerClass方法:
Unsafe

總結

經過反射能夠獲取Unsafe類的實例,他能夠幫助咱們進行堆外內存操做,內存copy,內存複製,線程掛起,提供了cpu級別的cas原子操做。另外還有lambda的匿名內部類的生成,數組內存操做等。juc包基本所有基於此類實現!

關注我,這裏只有乾貨!

相關文章
相關標籤/搜索