一半是天使,一半是惡魔
Unsafe是在
sun.misc
包下的類,
不屬于Java标準
,提供了一些相對
底層方法
來操作
系統底層資源
。很多Java的基礎類庫,包括一些被廣泛使用的高性能開發庫都是基于Unsafe類開發的,比如
Netty、Cassandra、Hadoop、Kafka
等。Unsafe類在提升Java運作效率,增強Java語言底層操作能力方面起了很大的作用。
為什麼說它一半是天使一半是魔鬼呢?要回答這個問題,我們還是要從其特性來解釋。
Unsafe類使Java擁有了像C語言的指針一樣
操作記憶體空間的能力
,一旦能夠直接操作記憶體,這也就意味着
- 不受JVM管理,也就意味着無法被GC,需要我們
,稍有不慎就會出現手動GC
,嚴重時甚至可能引起JVM崩潰。記憶體洩漏
- Unsafe的不少方法中必須提供
,偏移量要自己計算,一旦出現問題就是原始位址(記憶體位址)和被替換對象的位址
,會導緻整個JVM執行個體崩潰。JVM崩潰級别的異常
- 直接操作記憶體,也意味着其速度更快,在高并發的條件之下能夠很好地提高效率。
-
由于它實作的功能過于底層,是以Java官方并不建議使用的,官方文檔也幾乎沒有。
一.擷取Unsafe執行個體
- Unsafe類是
的,不允許繼承。且構造函數是"final"
的,是以我們private
。無法在外部對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類使用了單例模式,需要通過一個 靜态方法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)
。在JVM的類加載機制中,自定義的類使用的類加載器是SecurityException異常
,是以這個時候校驗失敗,會抛出異常。應用程式類加載器(Application ClassLoader)
- 這是因為在Unsafe類的
- 那麼如何才能擷取到Unsafe類的執行個體呢?有
。2種方案
- 第1種方案:将我們自定義的類(如Demo類)所在的Jar包所在的路徑通過
參數添加到Java指令中,當程式啟動時,-Xbootclasspath
,這樣校驗就通過了。顯然這種方式比較麻煩,而且不太實用,因為在項目中,可能需要在Bootstrap ClassLoader會加載Demo類
,如果通過Java指令行這種方式去指定,就會很多地方都使用Unsafe類
很麻煩,而且容易出現纰漏。
#-Xbootclasspath/a 把調用Unsafe相關方法的類A所在jar包路徑追加到預設的bootstrap路徑中,使得A被引導類加載器加載,進而通過Unsafe.getUnsafe方法安全的擷取Unsafe執行個體。 java -Xbootclasspath/a: ${path} # 其中path為調用Unsafe相關方法的類所在jar包路徑
- 第2種方案:通過
擷取單例對象theUnsafe。反射
public Unsafe getUnsafe() throws IllegalAccessException { //Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");//也可以這樣,作用相同 Field unsafeField = Unsafe.class.getDeclaredFields()[0]; // 将字段的通路權限設定為true unsafeField.setAccessible(true); //因為theUnsafe字段在Unsafe類中是一個靜态字段,是以通過Field.get()擷取字段值時,可以傳null擷取 Unsafe unsafe = (Unsafe) unsafeField.get(null); return unsafe; }
- 第1種方案:将我們自定義的類(如Demo類)所在的Jar包所在的路徑通過
二.Unsafe功能介紹以及實際應用
Unsafe的功能如下圖:
0.什麼是native方法?
在java中,這類方法被稱為
本地方法
(Native Method),簡單的說就是
由Java調用非Java代碼的接口,被調用的方法是由非java 語言實作的
。
- native方法是通過
實作調用的,從JNI(Java Native Interface)規範
開始 JNI 标準就是java平台的一部分,它允許Java代碼和其他語言的代碼進行互動。Java1.1
- 它可以
,并編譯成由C或C++語言來實作
,然後直接供java進行調用。DLL(windows)、SO(linux)(動态連結庫)
- 具體了解JNI可以看我這篇文章【Java基礎】JNI機制開發指南—認識JNI原理及如何用 Java 調用 C 的動态連結庫
1.對象相關操作
- Unsafe類中提供了很多操作對象執行個體的方法。這些方法基本都是
的,例如成對出現
,一個是從記憶體中擷取給定對象的指定偏移量的Object類型對象,一個是向記憶體中寫。與此類似的還有getInt()、getLong()…等方法。getObject()和putObject()
- 還有一組加了
的方法,例如:volatile語義
,它們的作用就是getObjectValotile()、putObjectVolatile()
。使用volatile語義擷取值和存儲值
- 什麼是volatile語義呢? 就是讀資料時每次都從主記憶體中取最新的值,而不是使用線程工作記憶體中的值;存資料時将值立馬重新整理到主記憶體,而不是先寫到工作記憶體,等以後再重新整理回主記憶體。
1.1.普通讀寫
通過Unsafe可以讀寫一個類的屬性, 即使這個屬性是私有的,也可以對這個屬性進行讀寫
。
讀寫一個Object屬性的相關方法
public native Object getObject(Object o, long offset);
public native void putObject(Object o, long offset, Object x);
- getObject():從對象o的指定位址偏移量offset處擷取變量的引用,與此類似方法有:getInt,getLong等等
- putObject():對對象o的指定位址偏移量offset處設值,與此類似方法有:putInt,putLong等等
- 其他的
也有對應的方法。原始資料類型
Unsafe還可以直接在一個位址上讀寫
public native byte getByte(long var1);
public native void putByte(long var1, byte var3);
- getByte():用于從指定記憶體位址處開始讀取一個byte。
- putByte():用于從指定記憶體位址寫入一個byte。
- 其他的
也有對應的方法。原始資料類型
測試: 對一個對象的屬性進行讀寫:
@Test
public void fieldTest() throws NoSuchFieldException {
Unsafe unsafe = getUnsafe();
User user = new User();
//擷取對象中字段的age字段記憶體位址(這個位址不是記憶體中的絕對位址而是一個相對位址),通過這個偏移位址對Integer的屬性值進行了讀寫操作
long fieldOffset = unsafe.objectFieldOffset(User.class.getDeclaredField("age"));
System.out.println("offset:" + fieldOffset);
//設定改記憶體位址的值為20,Integer為int的包裝類型,是以需要使用putObject、getObject來寫入和讀取字段值
unsafe.putObject(user, fieldOffset, 20);
//擷取該記憶體位址的值
System.out.println("age:" + unsafe.getObject(user, fieldOffset));
System.out.println("age:" + user.getAge());
}
//操作對象
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private Integer age;
}
1.2.volatile讀寫
普通的讀寫無法保證可見性和有序性,而volatile讀寫就可以保證可見性和有序性。
public native Object getObjectVolatile(Object o, long offset);
public native void putObjectVolatile(Object o, long offset, Object x);
- getObjectVolatile():從對象o的指定位址偏移量offset處擷取變量的引用,使用volatile語義讀取,與此類似方法有:getIntVolatile,getLongVolatile等等
- putObjectVolatile():對對象o的指定位址偏移量offset處設值,使用volatile語義存儲,與此類似方法有:putIntVolatile,putLongVolatile等等
volatile讀寫相對普通讀寫是更加昂貴的,因為需要
保證可見性和有序性
,而與volatile寫入相比
putOrderedXX寫入
代價相對較低,
putOrderedXX寫入不保證可見性,但是保證有序性
,所謂有序性,就是保證指令不會重排序。
1.3.有序寫入
有序寫入隻保證寫入的有序性,不保證可見性,就是說一個線程的寫入不保證其他線程立馬可見。
public native void putOrderedObject(Object var1, long var2, Object var4);
public native void putOrderedInt(Object var1, long var2, int var4);
public native void putOrderedLong(Object var1, long var2, long var4);
2.偏移量相關(記憶體位址)
我們可以使用Unsafe擷取
對象的指針
,通過
對指針進行偏移
,我們不僅可以
直接修改指針指向的資料(即使它們是私有的),甚至可以找到JVM已經認定為垃圾、可以進行回收的對象。
- 如:arrayBaseOffset(擷取數組第一個元素的記憶體位址)、arrayIndexScale(擷取數組中元素的增量位址) 配合起來使用,就可以
。定位數組中每個元素在記憶體中的位置
- 由于Java的數組最大值為
,使用Unsafe類的記憶體配置設定方法可以實作Integer.MAX_VALUE
。實際上這樣的資料就可以認為是C數組,是以需要注意超大數組
。在合适的時間釋放記憶體
- 由于Java的數組最大值為
public native long staticFieldOffset(Field var1);
public native long objectFieldOffset(Field var1);//(常用)
public native Object staticFieldBase(Field var1);
public native int arrayBaseOffset(Class<?> var1);
public native int arrayIndexScale(Class<?> var1);
- staticFieldOffset():用于擷取靜态屬性Field在對象中的偏移量,讀寫靜态屬性時必須擷取其偏移量。這個值對于給定的字段是
唯一且固定不變的
- objectFieldOffset():用于擷取非靜态屬性Field在對象執行個體中的偏移量,讀寫對象的非靜态屬性時會用到這個偏移量。
- staticFieldBase():擷取一個靜态類中給定字段的對象指針
- arrayBaseOffset():用于傳回數組中第一個元素實際位址相對整個數組對象的位址的偏移量。
- arrayIndexScale():傳回數組中每個元素占用的記憶體大小,機關是位元組
測試: 靜态屬性讀取
@Test
public void staticTest() throws Exception {
Unsafe unsafe = getUnsafe();
//首先建立一個People 對象,這是因為如果一個類沒有被執行個體化,那麼它的靜态屬性也不會被初始化,最後擷取的字段屬性将是null。
//是以在擷取靜态屬性前,需要調用shouldBeInitialized方法,判斷在擷取前是否需要初始化這個類。如果删除建立Unsafe 對象的語句,運作結果會變為:getObject會傳回null
People people=new People();
//是否需要執行個體化一個類,通常在擷取一個靜态屬性的時候使用
System.out.println("shouldBeInitialized >> " +unsafe.shouldBeInitialized(User.class));
Field sexField = People.class.getDeclaredField("name");
long fieldOffset = unsafe.staticFieldOffset(sexField);
System.out.println("staticFieldOffset >> " +fieldOffset);
Object fieldBase = unsafe.staticFieldBase(sexField);
System.out.println("staticFieldBase >> " +fieldBase);
Object object = unsafe.getObject(fieldBase, fieldOffset);
System.out.println("getObject >> " +object);
}
@Data
public class People {
public static String name = "Hydra";
private int age;
}
測試結果
3.直接記憶體操作
我們都知道
Java不可以直接對記憶體進行操作
,對象記憶體的配置設定和回收都是由
JVM
幫助我們實作的。但是
Unsafe為我們在Java中提供了直接操作記憶體的能力。
- Unsafe能直接進行申請記憶體、釋放記憶體、記憶體拷貝等操作。
的是Unsafe直接申請的記憶體是需要注意
。堆外記憶體
- 什麼是堆外記憶體呢? 堆外是
,通常我們應用程式運作後,建立的對象均在JVM記憶體中的堆中,堆記憶體是由JVM來管理的,相對于JVM的記憶體來說的
是以使用Unsafe類來申請對外記憶體時,要特别注意,否則容易出現記憶體洩漏等問題。而堆外記憶體指的是計算機中的直接記憶體,不受JVM管理。
- 什麼是堆外記憶體呢? 堆外是
// 配置設定記憶體
public native long allocateMemory(long var1);
// 重新配置設定記憶體
public native long reallocateMemory(long var1, long var3);
// 記憶體初始化
public native void setMemory(long var1, long var3, byte var5);
// 記憶體複制
public native void copyMemory(Object var1, long var2, Object var4, long var5, long var7);
// 清除記憶體
public native void freeMemory(long var1);
//擷取記憶體位址
getAddress(long var1)
測試: 記憶體操作
@Test
public void memoryTest() {
Unsafe unsafe = getUnsafe();
int size = 4;
//(配置設定記憶體)使用allocateMemory方法申請4位元組長度的記憶體空間,傳回記憶體位址
long allocateAddr = unsafe.allocateMemory(size);
//(重新配置設定記憶體)使用reallocateMemory方法重新配置設定了一塊8位元組長度的記憶體空間,傳回記憶體位址
long reallocateAddr = unsafe.reallocateMemory(allocateAddr, size * 2);
System.out.println("allocateAddr: " + allocateAddr);
System.out.println("reallocateAddr: " + reallocateAddr);
try {
//(記憶體初始化)調用setMemory方法向每個位元組寫入内容為byte類型的1
unsafe.setMemory(null, allocateAddr, size, (byte) 1);
for (int i = 0; i < 2; i++) {
//(記憶體複制)調用copyMemory方法進行了兩次記憶體的拷貝,每次拷貝記憶體位址allocateAddr開始的4個位元組,分别拷貝到以reallocateAddr 和reallocateAddr+4開始的記憶體空間上:
unsafe.copyMemory(null, allocateAddr, null, reallocateAddr + size * i, 4);
}
//當使用Unsafe調用getInt方法時,因為一個int型變量占4個位元組,會一次性讀取4個位元組,組成一個int的值,對應的十進制結果為16843009,可以通過圖示了解這個過程:
System.out.println("allocateAddr: " + unsafe.getInt(allocateAddr));
//拷貝完成後,使用getLong方法一次性讀取8個位元組,得到long類型的值為72340172838076673。
System.out.println("reallocateAddr: " + unsafe.getLong(reallocateAddr));
} finally {
//(記憶體釋放)需要注意,通過這種方式配置設定的記憶體屬于堆外記憶體,是無法進行垃圾回收的,需要我們把這些記憶體當做一種資源去手動調用freeMemory方法進行釋放,否則會産生記憶體洩漏。
// 通用的操作記憶體方式是在try中執行對記憶體的操作,最終在finally塊中進行記憶體的釋放。
unsafe.freeMemory(allocateAddr);
unsafe.freeMemory(reallocateAddr);
}
}
執行結果
Unsafe類對記憶體的操作在網絡通信架構中應用廣泛,如:。在
Netty、MINA等通信架構
中的
java.nio包
中,記憶體的申請、釋放等邏輯都是調用Unsafe類中的對應方法來實作的。
DirectByteBuffer
- 利用copyMemory(),我們可以實作一個通用的對象拷貝方法,無需再對每一個對象都實作clone方法,當然這通用的方法隻能做到
對象淺拷貝。
4.CAS相關
JUC中大量運用了CAS操作,可以說
CAS操作是JUC的基礎
,Unsafe中提供了
int,long和Object
的CAS操作:
CAS(Compare And Swap,比較并交換) 算法是一種無鎖算法,屬于樂觀鎖,是Java提供的
非阻塞原子性操作
。在不使用鎖的情況下實作多線程下的同步。在并發包中(
java.util.concurrent.atomic
)原子類型都是使用CAS來實作的。
- 樂觀鎖的核心思路就是每次不加鎖而是假設修改資料之前其他線程一定不會修改,如果因為修改過産生沖突就失敗就重試,直到成功為止。
- CAS算法是
的,多個線程競争鎖隻會有一個勝出,其餘線程并不會阻塞,而是繼續嘗試擷取更新,當然也可以主動放棄。是以不可能出現死鎖的情況。也就是說非阻塞
。無鎖操作天生免疫死鎖
- CAS通過硬體保證了比較更新的原子性,在JDK中Unsafe類提供了一系列的
方法,compareAndSwap*
- JVM中的CAS操作就是利用了CPU提供的cmpxchgl指令實作的原子性的。
- CAS算法是
/**
* @param o 包含要修改field的對象
* @param offset 對象中某field的偏移量
* @param expected 期望值
* @param update 更新值
* 如果o+offset對應的的值與expected相同,則将字段的值設為update這個新值,并且此更新是不可被中斷的,也就是一個原子操作。
* @return true | false
*/
public final native boolean compareAndSwapObject(Object o, long offset,int expected,int update);
public final native boolean compareAndSwapInt(Object o, long offset, int expected, int update);
public final native boolean compareAndSwapLong(Object o, long offset, long expected, long update);
-
,在Java的鎖中,經常會出現CAS操作,它們最終都調用了CAS一般用于樂觀鎖
,這3個CAS方法都是Unsafe類中的CAS操作方法,compareAndSwapInt()、compareAndSwapLong()、compareAndSwapObject()
,具體實作是在native方法
,它們的作用是比較并交換,這個操作是原子操作。 關于CAS更詳細的講解可以參考這篇文章:初識CAS的實作原理JVM中實作
Unsafe在
隊列同步器AQS(AbstractQueuedSynchronizer)
、
原子類
中都有應用,現在以隊列同步器AQS為例,看看AQS當中是如何使用Unsafe類的。
- 在AQS中擷取
時,如果目前線程能擷取到鎖,那麼就會去嘗試修改同步狀态state的值,這個時候就用到了Unsafe類。同步狀态
是AQS類中的一個方法,它實際調用的是compareAndSetState()
。Unsafe類的compareAndSwapInt()方法
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
測試
public class CasTest {
private volatile int num;
private Unsafe unsafe = UnsafeTest.getUnsafe();
public static void main(String[] args) {
//使用兩個線程去修改int型屬性num的值,并且隻有在num的值等于傳入的參數x減一時,才會将num的值變為x,也就是實作對num的加一的操作
CasTest cas = new CasTest();
new Thread(() -> {
for (int i = 1; i < 50; i++) {
cas.increment(i);
System.out.print(Thread.currentThread().getName() + ":" + cas.num + " ");
}
},"a").start();
new Thread(() -> {
for (int i = 50; i < 100; i++) {
cas.increment(i);
System.out.print(Thread.currentThread().getName() + ":" + cas.num + " ");
}
},"b").start();
new Thread(() -> {
for (int i = 100; i < 150; i++) {
cas.increment(i);
System.out.print(Thread.currentThread().getName() + ":" + cas.num + " ");
}
},"c").start();
new Thread(() -> {
for (int i = 150; i < 200; i++) {
cas.increment(i);
System.out.print(Thread.currentThread().getName() + ":" + cas.num + " ");
}
},"d").start();
}
/**
* 自增
*
* @param x
*/
private void increment(int x) {
// 需要注意的是,在調用compareAndSwapInt方法後,會直接傳回true或false的修改結果,是以需要我們在代碼中手動添加自旋的邏輯。
// 在AtomicInteger類的設計中,也是采用了将compareAndSwapInt的結果作為循環條件,直至修改成功才退出死循環的方式來實作的原子性的自增操作。
while (true) {
try {
long fieldOffset = UnsafeTest.getUnsafe().objectFieldOffset(CasTest.class.getDeclaredField("num"));
if (unsafe.compareAndSwapInt(this, fieldOffset, x - 1, x)) {
break;
}
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
}
}
}
執行結果
5.線程排程
//取消阻塞線程
public native void unpark(Object thread);
//阻塞線程
public native void park(boolean isAbsolute, long time);
//獲得對象鎖(可重入鎖)
@Deprecated
public native void monitorEnter(Object o);
//釋放對象鎖,,如果對一個沒有被monitorEnter加鎖的對象執行此方法,會抛出IllegalMonitorStateException異常。
@Deprecated
public native void monitorExit(Object o);
//嘗試擷取對象鎖
@Deprecated
public native boolean tryMonitorEnter(Object o);
- park()和unpark() 相信看過
的都不會陌生,這兩個方法主要用來LockSupport類
。 将一個線程進行挂起是通過挂起和喚醒線程
實作的,調用 park()後,線程将一直阻塞直到逾時或者中斷等條件出現。park()
可以終止一個挂起的線程,使其恢複正常。unpark()
-
方法用于獲得對象鎖,monitorEnter
用于釋放對象鎖,如果對一個沒有被monitorExit
的對象執行此方法,會抛出monitorEnter加鎖
。IllegalMonitorStateException異常
嘗試擷取對象鎖,如果成功則傳回true,反之傳回falsetryMonitorEnter方法
- Java中的
就是通過這2個指令來實作的。 ,synchronized鎖
已經被标記為Deprecated,不建議被使用monitor相關的3個方法
- Java中的
應用場景: Java鎖和同步器架構的核心類
AbstractQueuedSynchronizer
,就是通過調
用LockSupport.park()和LockSupport.unpark()
實作線程的
阻塞和喚醒
的,而LockSupport的park、unpark方法實際是調用Unsafe的park、unpark方式來實作。
//-------------------LockSupport的部分源碼---------------------
// 挂起線程
public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker); // 通過Unsafe的putObject方法設定阻塞阻塞目前線程的blocker
UNSAFE.park(false, 0L); // 通過Unsafe的park方法來阻塞目前線程,注意此方法将目前線程阻塞後,目前線程就不會繼續往下走了,直到其他線程unpark此線程
setBlocker(t, null); // 清除blocker
}
// 喚醒線程
public static void unpark(Thread thread) {
if (thread != null)
UNSAFE.unpark(thread);
}
測試:使用Unsafe的park()和unpark()
@Test
public void testParkAndUnpark() {
Unsafe unsafe = getUnsafe();
Thread mainThread = Thread.currentThread();
new Thread(() -> {
try {
//休眠5s
TimeUnit.SECONDS.sleep(5);
System.out.println("子線程嘗試喚醒主線程");
unsafe.unpark(mainThread);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
System.out.println("阻塞主線程");
unsafe.park(false, 0L);
System.out.println("喚醒主線程成功");
}
執行結果:線程開始運作後先進行睡眠,確定主線程能夠調用
park方法
阻塞自己,子線程在睡眠5秒後,調用
unpark方法
喚醒主線程,使主線程能繼續向下執行。如下圖所示:
6.類加載相關
public native Class<?> defineClass(String name, byte[] b, int off, int len,ClassLoader loader,ProtectionDomain protectionDomain);
public native Class<?> defineAnonymousClass(Class<?> hostClass, byte[] data, Object[] cpPatches);
public native Object allocateInstance(Class<?> c); throws InstantiationException;
public native boolean shouldBeInitialized(Class<?> c);
public native void ensureClassInitialized(Class<?> c);
- defineClass():定義一個類,用于在運作時動态地建立類(此方法會跳過JVM的所有安全檢查)。
- defineAnonymousClass():用于在運作時動态地建立匿名内部類。
在Lambda表達式中就是使用ASM動态生成位元組碼,然後利用該方法定義實作相應的函數式接口的匿名類。
- allocateInstance():用于建立一個類的執行個體,但是不會調用這個執行個體的構造方法,如果這個類還未被初始化,則初始化這個類。
這個對象在對象反序列化的時候會很有用,能夠重建和設定final字段,而不需要調用構造方法。
- shouldBeInitialized():
,通常在擷取一個類的靜态屬性的時候使用(因為一個類如果沒初始化,它的靜态屬性也不會初始化)。 當且僅當ensureClassInitialized方法不生效時傳回false。判斷是否需要初始化一個類
- ensureClassInitialized():
。通常在擷取一個類的靜态屬性的時候使用(因為一個類如果沒初始化,它的靜态屬性也不會初始化)。檢測給定的類是否已經初始化
典型應用
- 正常對象執行個體化方式:通常我們使用new機制來的建立對象。當類隻有有參構造函數且沒有無參構造函數時,則必須使用有參構造函數進行對象執行個體化,且使用有參構造函數時,必須傳遞相應個數的參數才能完成對象執行個體化。
- 非正常的執行個體化方式:而Unsafe中提供
,僅通過Class對象就可以建立此類的執行個體對象,而且不需要調用其構造函數、初始化代碼、JVM安全檢查等。它抑制修飾符檢測,也就是即使allocateInstance方法
,隻需提供構造器是private修飾的也能通過此方法執行個體化
即可建立相應的對象。由于這種特性,allocateInstance在java.lang.invoke、Objenesis(提供繞過類構造器的對象生成方式)、Gson(反序列化時用到)中都有相應的應用。class對象
測試1: 使用allocateInstance
@Data
public class A {
private int b;
public A(){
this.b =1;
}
}
@Test
public void objTest() throws Exception{
A a1=new A();
System.out.println(a1.getB());
A a2 = A.class.newInstance();
System.out.println(a2.getB());
A a3= (A) unsafe.allocateInstance(A.class);
System.out.println(a3.getB());
}
執行結果: 列印結果分别為1、1、0,說明通過allocateInstance方法建立對象過程中,不會調用類的構造方法。使用這種方式建立對象時,隻用到了Class對象,是以說如果想要跳過對象的初始化階段或者跳過構造器的安全檢查,就可以使用這種方法。在上面的例子中,如果将A類的構造函數改為private類型,将無法通過構造函數和反射建立對象,但allocateInstance方法仍然有效。
測試2: 使用defineClass在運作時動态地建立一個類
- 在實際使用過程中,可以隻傳入
、位元組數組
以及起始位元組的下标
度,預設情況下,讀取的位元組長
來源于調用此方法的執行個體。下面的例子中實作了反編譯生成後的class檔案的功能:類加載器(ClassLoader)和保護域(ProtectionDomain)
@Test
public void defineTest() {
Unsafe unsafe = getUnsafe();
String fileName = "E:\\04_resource_study\\java_base_demo\\target\\classes\\com\\demo\\concurrent\\unsafe\\User.class";
File file = new File(fileName);
try (FileInputStream fis = new FileInputStream(file)) {
//緩存大小為檔案大小
byte[] content = new byte[(int) file.length()];
//讀取流
fis.read(content);
//動态建立class
Class clazz = unsafe.defineClass(null, content, 0, content.length, null, null);
//建立一個對象
Object o = clazz.newInstance();
Object age = clazz.getMethod("getAge").invoke(o, null);
System.out.println(age);
} catch (Exception e) {
e.printStackTrace();
}
}
@Data
public class User {
private Integer age = 18;
}
執行結果
7.記憶體屏障
- Unsafe類從JDK1.8開始,提供了三個和記憶體屏障相關的API方法。分别是loadFence()、 storeFence() 、fullFence(),用于定義記憶體屏障,
。避免代碼重排序
硬體層的記憶體屏障分為2種:Load Barrier 和 Store Barrier即讀屏障和寫屏障。
.
記憶體屏障有2個作用:
- 阻止屏障兩側的指令重排序
- 強制把寫緩沖區/高速緩存中的髒資料等寫回主記憶體,讓緩存中相應的資料失效
- loadFence() :表示該方法之前的
在記憶體屏障之前完成。所有load操作
- storeFence():表示該方法之前的
在記憶體屏障之前完成。所有store操作
- fullFence():表示該方法之前的所有
在記憶體屏障之前完成。load、store操作
// 禁止load操作重排序
public native void loadFence();
// 禁止store操作重排序
public native void storeFence();
// 禁止load和store操作重排序
public native void fullFence();
測試:
@Test
public void memoryBarrierTest() {
Unsafe unsafe = getUnsafe();
ChangeThread changeThread = new ChangeThread();
new Thread(changeThread).start();
while (true) {
boolean flag = changeThread.isFlag();
//加入讀記憶體屏障
unsafe.loadFence();
if (flag) {
System.out.println("detected flag changed");
break;
}
}
System.out.println("main thread end");
}
@Getter
ChangeThread implements Runnable {
/**
* volatile
**/
boolean flag = false;
@Override
public void run() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("subThread change flag to:" + flag);
flag = true;
}
}
執行結果
而如果删掉上面代碼中的
loadFence方法
,那麼主線程将無法感覺到
flag
發生的變化,會一直在
while中循環
。用圖來表示上面的過程:
- 了解
的小夥伴們應該清楚:Java記憶體模型(JMM)
。上面的圖中的流程就是子線程借助于主記憶體,将修改後的結果同步給了主線程,進而修改主線程中的工作空間,跳出循環。運作中的線程不能直接操作主記憶體中的變量的,隻能操作自己工作記憶體中的變量,然後同步到主記憶體中,并且線程的工作記憶體是不能共享的
典型應用
- 在Java 8中引入了一種鎖的新機制——
,它可以看成是讀寫鎖的一個改進版本。StampedLock提供了一種StampedLock
的實作,這種樂觀讀鎖類似于無鎖的操作,樂觀讀鎖
,進而緩解完全不會阻塞寫線程擷取寫鎖
。讀多寫少時寫線程“饑餓”現象
- 由于StampedLock提供的樂觀讀鎖不阻塞寫線程擷取讀鎖,當線程共享變量從主記憶體load到線程工作記憶體時,會存在資料不一緻問題,是以當使用StampedLock的樂觀讀鎖時,需要遵從如下圖用例中使用的模式來確定資料的一緻性。
8.系統相關
- Unsafe中提供的
和addressSize
方法用于擷取系統資訊pageSize
- 調用addressSize方法會
,如果在64位系統下預設會傳回傳回系統指針的大小
,而32位系統則會傳回8
。4
- 調用pageSize方法會傳回
,值為2的整數幂。記憶體頁的大小
- 調用addressSize方法會
// 擷取指針的大小,機關是位元組。
// 對于64位系統,傳回8,表示指針大小是8位元組
// 對于32位系統,傳回4,表示指針大小是4位元組
public native int addressSize();
// 傳回記憶體頁的大小,機關是位元組。傳回值一定是2的多少次幂
public native int pageSize();
測試
@Test
public void systemTest() {
Unsafe unsafe = getUnsafe();
System.out.println(unsafe.addressSize());
System.out.println(unsafe.pageSize());
}
執行結果:本人電腦的指針大小為8位元組,記憶體頁大小為4096位元組,即4KB。
8
4096
這兩個方法的應用場景比較少,在中,在使用pageCount計算所需的記憶體頁的數量時,調用了
java.nio.Bits類
方法擷取記憶體頁的大小。另外,在使用
pageSize
方法拷貝記憶體時,調用了
copySwapMemory
方法,檢測
addressSize
的情況。
32位系統
9.Unsafe具體實作的相關原子方法
//原子方式将目前值與輸入值相加并傳回結果
//擷取記憶體位址為obj+offset的變量值, 并将該變量值加上delta
//obj: 目前要操作的對象(其實就是 AtomicInteger 執行個體)
//offset: 目前要操作的變量偏移量(可以了解為 CAS 中的記憶體目前值)
//delta: 期望記憶體中的值
//v: 要修改的新值
public final int getAndAddInt(Object obj, long offset, int delta) {
int v;
do {
//通過對象和偏移量擷取變量的值
//由于volatile的修飾, 所有線程看到的v都是一樣的
v= this.getIntVolatile(obj, offset);
/*
while中的compareAndSwapInt()方法嘗試修改v的值,具體通過objoffset擷取變量的值
如果這個值和v不一樣, 說明其他線程修改了obj+offset位址處的值, 此時compareAndSwapInt()傳回false, 繼續循環
如果這個值和v一樣, 說明沒有其他線程修改obj+offset位址處的值, 此時可以将obj+offset位址處的值改為v+delta, compareAndSwapInt()傳回true, 退出循環
Unsafe類中的compareAndSwapInt()方法是原子操作, 是以compareAndSwapInt()修改obj+offset位址處的值的時候不會被其他線程中斷
*/
//比較一下 offset 和記憶體目前值 v 是否相等,如果相等那我就将記憶體值 v 修改為 v + delta(delta 就是 1,也可以是其他數)。
} while(!this.compareAndSwapInt(obj, offset, v, v + delta));
return v;
}
//同上
public final long getAndAddLong(Object var1, long var2, long var4) {
long var6;
do {
var6 = this.getLongVolatile(var1, var2);
} while(!this.compareAndSwapLong(var1, var2, var6, var6 + var4));
return var6;
}
//以原子方式設定為給定值并傳回舊值
public final int getAndSetInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var4));
return var5;
}
//同上
public final long getAndSetLong(Object var1, long var2, long var4) {
long var6;
do {
var6 = this.getLongVolatile(var1, var2);
} while(!this.compareAndSwapLong(var1, var2, var6, var4));
return var6;
}
//同上
public final Object getAndSetObject(Object var1, long var2, Object var4) {
Object var5;
do {
var5 = this.getObjectVolatile(var1, var2);
} while(!this.compareAndSwapObject(var1, var2, var5, var4));
return var5;
}
三.總結
- Java并發程式設計中,存着這大量的Unsafe類的使用,主要使用的是和CAS操作相關的三個方法,是以搞清楚這三個方法,對看懂Java并發程式設計的源碼有很大幫助。
- 另外Unsafe類中
,它是objectFieldOffset(Field f)這個方法很常用
,通常和Unsafe類中的其他方法結合使用。通過這個方法能知道要修改的資料在記憶體中的位置,然後再通過Unsafe類中其他方法來根據資料在記憶體中的位置進而來修改資料。擷取字段在記憶體中的偏移量
大家肯定想一探究竟和
compareAndSwapXXX
這2個方法中做了什麼事情,很遺憾,個人水準有限,目前還沒有能力去探究,隻知道這種寫法是
objectFieldOffset
,會調用到
JNI
,最終會把對應的指令發送給CPU,這是可以保證原子性的。
C或者C++的系統調用
Unsafe确實存在着一些安全隐患,在我看來,一項技術具有不安全因素并不可怕,可怕的是它在使用過程中被濫用。盡管之前有傳言說會在Java9中移除Unsafe類,不過它還是照樣已經存活到了Java16,按照存在即合理的邏輯,隻要使用得當,它還是能給我們帶來不少的幫助,是以最後還是建議大家,在使用Unsafe的過程中一定要做到使用
謹慎使用、避免濫用
。
參考如下文章
- Java魔法類:Unsafe應用解析
- Java雙刃劍之Unsafe類詳解