摘要:它的目的是為了增強語言直接操作底層資源的能力,無疑帶來很多方便。這樣可以有效避免多線程環境下的同步問題。另外還有的匿名內部類的生成,數組內存操作等。
簡介本文原創地址,我的博客:jsbintask的博客(食用效果最佳),轉載請注明出處!
Unsafe是jdk提供的一個直接訪問操作系統資源的工具類(底層c++實現),它可以直接分配內存,內存復制,copy,提供cpu級別的CAS樂觀鎖等操作。它的目的是為了增強java語言直接操作底層資源的能力,無疑帶來很多方便。但是,使用的同時就得額外小心!它的總體作用如下(圖片來源網絡):
Unsafe位于sun.misc包下,jdk中的并發編程包juc(java.util.concurrent)基本全部靠Unsafe實現,由此可見其重要性。
基本使用Unsafe被設計為單例,并且只允許被引導類加載器(BootstrapClassLoader)加載的類使用:
所以我們自己寫的類是無法直接通過Unsafe.getUnsafe()獲取的。當然,既然是java代碼,我們就可以使用一點歪道,比如通過反射直接new一個或者將其內部靜態成員變量theUnsafe獲取出來:
public static void main(String[] args) throws Exception{
// method 1
Class unsafeClass = Unsafe.class;
Constructor 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了,接下來看下它的使用以及jdk使用操作的。
CAS
CAS譯為Compare And Swap,它是樂觀鎖的一種實現。假設內存值為v,預期值為e,想要更新成得值為u,當且僅當內存值v等于預期值e時,才將v更新為u。 這樣可以有效避免多線程環境下的同步問題。
在unsafe中,實現CAS算法通過cpu的原子指令cmpxchg實現,它對應的方法如下:
簡單介紹下它使用的參數,var1為內存中要操作的對象,var2為要操作的值的內存地址偏移量,var4為預期值,var5為想要更新成的值。
為了方便理解,舉個栗子。類User有一個成員變量name。我們new了一個對象User后,就知道了它在內存中的起始值,而成員變量name在對象中的位置偏移是固定的。這樣通過這個起始值和這個偏移量就能夠定位到name在內存中的具體位置。
所以我們現在的問題就是如何得出name在對象User中的偏移量,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;
}
}
因為內存中name的值為"jsbintask",而第一次使用compareAndSwapObject方法預期值為"jsbintask1",這顯然是不相等的,所以第一次更新失敗,第二次我們傳入了正確的預期值,更新成功!
如果我們分析juc包下的Atomic開頭的原子類就會發現,它內部的原子操作全部來源于unsafe的CAS方法,比如AtomicInteger的getAndIncrement方法,內部直接調用unsafe的getAndAddInt方法,它的實現原理為:cas失敗,就循環,直到成功為止,這就是我們所說的自旋鎖!
內存分配
Unsafe還給我們提供了直接分配內存,釋放內存,拷貝內存,內存設置等方法,值得注意的是,這里的內存指的是堆外內存!它是不受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));
}
我們分配了10M內存,并且將前1M內存的值設置為了10,取出了內存中的值進行比較,驗證了unsafe的方法。
堆外內存不受jvm內存模型掌控,在nio(netty,mina)中大量使用對外內存進行管道傳輸,copy等,使用它們的好處如下:
對垃圾回收停頓的改善。由于堆外內存是直接受操作系統管理而不是JVM,所以當我們使用堆外內存時,即可保持較小的堆內內存規模。從而在GC時減少回收停頓對于應用的影響。
提升程序I/O操作的性能。通常在I/O通信過程中,會存在堆內內存到堆外內存的數據拷貝操作,對于需要頻繁進行內存間數據拷貝且生命周期較短的暫存數據,都建議存儲到堆外內存。 而在jdk中,堆外內存對應的類為DirectByteBuffer,它內部也是通過unsafe分配的內存: 這里值得注意的是,對外內存的回收借助了Cleaner這個類。
線程調度通過Unsafe還可以直接將某個線程掛起,這和調用Object.wait()方法作用是一樣的,但是效率確更高!
我們熟知的AQS(AbstractQueuedSynchronizer)內部掛起線程使用了LockSupport方法,而LockSupport內部依舊使用的是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();
}
當線程t1運行到i=5時,被掛起,主線程執行,而主線程運行到i=9時,將t1喚醒,t1繼續打印! 在park出debug可以觀察t1線程的狀態:
數組操作
對于數組,Unsafe提供了特別的方法返回不同類型數組在內存中的偏移量:
arrayBaseOffset方法返回數組在內存中的偏移量,這個值是固定的。arrayIndexScale返回數組中的每一個元素的內存地址換算因子。舉個栗子,double數組(注意不是包裝類型)每個元素占用8個字節,所以換算因子為8,int類型則為4,通過這兩個方法我們就能定位數組中每個元素的內存地址,從而賦值,下面代碼演示:
public static void main(String[] args) throws Exception{
Class unsafeClass = Unsafe.class;
Constructor 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));
}
我們通過獲取Integer數組的內存偏移量,結合換算因子將第一個元素,第五個元素分別替換為了100,50。驗證了我們的說法。
數組的原子操作,juc包也已經提供了相應的工具類,比如AtomicIntegerArray內部就是同過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<");方法生成匿名內部類,如下面的代碼:
public class UnsafeTest2 {
public static void main(String[] args) {
Function function = Integer::parseInt;
System.out.println(function.apply("100"));
}
}
查看字節碼:
發現它調用了LambdaMetafactory.metafactory方法,最終調用了InnerClassLambdaMetafactory的spinInnerClass方法:總結
通過反射可以獲取Unsafe類的實例,他可以幫助我們進行堆外內存操作,內存copy,內存復制,線程掛起,提供了cpu級別的cas原子操作。另外還有lambda的匿名內部類的生成,數組內存操作等。juc包基本全部基于此類實現!
關注我,這里只有干貨!
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/7198.html
摘要:表示一個異步任務的結果,就是向線程池提交一個任務后,它會返回對應的對象。它們分別提供兩個重要的功能阻塞當前線程等待一段時間直到完成或者異常終止取消任務。此時,線程從中返回,然后檢查當前的狀態已經被改變,隨后退出循環。 0 引言 前段時間需要把一個C++的項目port到Java中,因此時隔三年后重新熟悉了下Java。由于需要一個通用的線程池,自然而然就想到了Executors。 用了...
摘要:恢復流程如下判斷當前中的是否已標注為處理從鏈表中刪除已標注中止的事件,也就是刪除已經被恢復的事件將相關需要恢復的棧幀信息傳遞給方法的參數每個棧幀對應著一個未運行完的函數。 作為一個 gophper,我相信你對于 panic 和 recover 肯定不陌生,但是你有沒有想過。當我們執行了這兩條語句之后。底層到底發生了什么事呢?前幾天和同事剛好聊到相關的話題,發現其實大家對這塊理解還是比較...
摘要:因為它是不安全的,但是在特殊的場景下,使用了它。可以打破的類型和內存安全機制,讓你獲得眼前一亮的驚喜效果 在上一篇文章 《深入理解 Go Slice》 中,大家會發現其底層數據結構使用了 unsafe.Pointer。因此想著再介紹一下其關聯知識 原文地址:有點不安全卻又一亮的 Go unsafe.Pointer 前言 在大家學習 Go 的時候,肯定都學過 Go 的指針是不支持指針運算...
閱讀 2404·2021-11-23 09:51
閱讀 1217·2021-11-22 13:54
閱讀 3427·2021-09-24 10:31
閱讀 1092·2021-08-16 10:46
閱讀 3627·2019-08-30 15:54
閱讀 707·2019-08-30 15:54
閱讀 2889·2019-08-29 17:17
閱讀 3162·2019-08-29 15:08