Java Unsafe學習筆記分享
sun.misc.Unsafe
作用:可以用來在任意內存地址位置處讀寫數據,支持一些CAS原子操作
Java最初被設計為一種安全的受控環境。盡管如此,HotSpot還是包含瞭一個後門sun.misc.Unsafe,提供瞭一些可以直接操控內存和線程的底層操作。Unsafe被JDK廣泛應用於java.nio和並發包等實現中,這個不安全的類提供瞭一個觀察HotSpot JVM內部結構並且可以對其進行修改,但是不建議在生產環境中使用
獲取Unsafe實例
Unsafe對象不能直接通過new Unsafe()或調用Unsafe.getUnsafe()獲取,原因如下:
- 不能直接new Unsafe(),原因是Unsafe被設計成單例模式,構造方法是私有的;
- 不能通過調用Unsafe.getUnsafe()獲取,因為getUnsafe被設計成隻能從引導類加載器(bootstrap class loader)加載
@CallerSensitive public static Unsafe getUnsafe() { Class var0 = Reflection.getCallerClass(); if (!VM.isSystemDomainLoader(var0.getClassLoader())) { throw new SecurityException("Unsafe"); } else { return theUnsafe; } }
獲取實例
//方法一:我們可以令我們的代碼“受信任”。運行程序時,使用bootclasspath選項,指定系統類路徑加上你使用的一個Unsafe路徑 java -Xbootclasspath:/usr/jdk1.7.0/jre/lib/rt.jar:. com.mishadoff.magic.UnsafeClient // 方法二 static { try { Field field = Unsafe.class.getDeclaredField("theUnsafe"); field.setAccessible(true); UNSAFE = (Unsafe) field.get(null); } catch (Exception e) { } }
註意:忽略你的IDE。比如:eclipse顯示”Access restriction…”錯誤,但如果你運行代碼,它將正常運行。如果這個錯誤提示令人煩惱,可以通過以下設置來避免:
Preferences -> Java -> Compiler -> Errors/Warnings -> Deprecated and restricted API -> Forbidden reference -> Warning
重點API
- allocateInstance(Class<?> var1)不調用構造方法生成對象
User instance = (User) UNSAFE.allocateInstance(User.class);
- objectFieldOffset(Field var1)返回成員屬性在內存中的地址相對於對象內存地址的偏移量
- putLong,putInt,putDouble,putChar,putObject等方法,直接修改內存數據(可以越過訪問權限)
package com.quancheng; import sun.misc.Unsafe; import java.lang.reflect.Field; public class CollectionApp { private static sun.misc.Unsafe UNSAFE; public static void main(String[] args) { try { User instance = (User) UNSAFE.allocateInstance(User.class); instance.setName("luoyoub"); System.err.println("instance:" + instance); instance.test(); Field name = instance.getClass().getDeclaredField("name"); UNSAFE.putObject(instance, UNSAFE.objectFieldOffset(name), "huanghui"); instance.test(); } catch (Exception e) { e.printStackTrace(); } } static { try { Field field = Unsafe.class.getDeclaredField("theUnsafe"); field.setAccessible(true); UNSAFE = (Unsafe) field.get(null); } catch (Exception e) { } } } class User { private String name; public void setName(String name) { this.name = name; } public void test() { System.err.println("hello,world" + name); } }
copyMemory
:內存數據拷貝freeMemory
:用於釋放allocateMemory和reallocateMemory申請的內存compareAndSwapInt
/compareAndSwapLongCAS
操作getLongVolatile
/putLongVolatile
使用場景
避免初始化
當你想要跳過對象初始化階段,或繞過構造器的安全檢查,或實例化一個沒有任何公共構造器的類,allocateInstance方法是非常有用的,使用構造器、反射和unsafe初始化它,將得到不同的結果
public class CollectionApp { private static sun.misc.Unsafe UNSAFE; public static void main(String[] args) throws IllegalAccessException, InstantiationException { A a = new A(); a.test(); // output ==> 1 A a1 = A.class.newInstance(); a1.test(); // output ==> 1 A instance = (A) UNSAFE.allocateInstance(A.class); instance.test(); // output ==> 0 } static { try { Field field = Unsafe.class.getDeclaredField("theUnsafe"); field.setAccessible(true); UNSAFE = (Unsafe) field.get(null); } catch (Exception e) { } } } class A{ private long a; public A(){ a = 1; } public void test(){ System.err.println("a==>" + a); } }
內存崩潰(Memory corruption)
Unsafe可用於繞過安全的常用技術,直接修改內存變量;實際上,反射可以實現相同的功能。但值得關註的是,我們可以修改任何對象,甚至沒有這些對象的引用
Guard guard = new Guard(); guard.giveAccess(); // false, no access // bypass Unsafe unsafe = getUnsafe(); Field f = guard.getClass().getDeclaredField("ACCESS_ALLOWED"); unsafe.putInt(guard, unsafe.objectFieldOffset(f), 42); // memory corruption guard.giveAccess(); // true, access granted
註意:我們不必持有這個對象的引用
- 淺拷貝(Shallow copy)
- 動態類(Dynamic classes)
我們可以在運行時創建一個類,比如從已編譯的.class文件中。將類內容讀取為字節數組,並正確地傳遞給defineClass方法;當你必須動態創建類,而現有代碼中有一些代理, 這是很有用的
private static byte[] getClassContent() throws Exception { File f = new File("/home/mishadoff/tmp/A.class"); FileInputStream input = new FileInputStream(f); byte[] content = new byte[(int)f.length()]; input.read(content); input.close(); return content; } byte[] classContents = getClassContent(); Class c = getUnsafe().defineClass( null, classContents, 0, classContents.length); c.getMethod("a").invoke(c.newInstance(), null); // 1
拋出異常(Throw an Exception)
該方法拋出受檢異常,但你的代碼不必捕捉或重新拋出它,正如運行時異常一樣
getUnsafe().throwException(new IOException());
大數組(Big Arrays)
正如你所知,Java數組大小的最大值為Integer.MAX_VALUE。使用直接內存分配,我們創建的數組大小受限於堆大小;實際上,這是堆外內存(off-heap memory)技術,在java.nio包中部分可用;
這種方式的內存分配不在堆上,且不受GC管理,所以必須小心Unsafe.freeMemory()的使用。它也不執行任何邊界檢查,所以任何非法訪問可能會導致JVM崩潰
class SuperArray { private final static int BYTE = 1; private long size; private long address; public SuperArray(long size) { this.size = size; address = getUnsafe().allocateMemory(size * BYTE); } public void set(long i, byte value) { getUnsafe().putByte(address + i * BYTE, value); } public int get(long idx) { return getUnsafe().getByte(address + idx * BYTE); } public long size() { return size; } } long SUPER_SIZE = (long)Integer.MAX_VALUE * 2; SuperArray array = new SuperArray(SUPER_SIZE); System.out.println("Array size:" + array.size()); // 4294967294 for (int i = 0; i < 100; i++) { array.set((long)Integer.MAX_VALUE + i, (byte)3); sum += array.get((long)Integer.MAX_VALUE + i); } System.out.println("Sum of 100 elements:" + sum); // 300
並發(Concurrency)
幾句關於Unsafe的並發性。compareAndSwap方法是原子的,並且可用來實現高性能的、無鎖的數據結構
掛起與恢復
定義:
public native void unpark(Thread jthread); public native void park(boolean isAbsolute, long time); // isAbsolute參數是指明時間是絕對的,還是相對的
將一個線程進行掛起是通過park方法實現的,調用park後,線程將一直阻塞直到超時或者中斷等條件出現。unpark可以終止一個掛起的線程,使其恢復正常。整個並發框架中對線程的掛起操作被封裝在 LockSupport類中,LockSupport類中有各種版本pack方法,但最終都調用瞭Unsafe.park()方法;
unpark函數為線程提供“許可(permit)”,線程調用park函數則等待“許可”。這個有點像信號量,但是這個“許可”是不能疊加的,“許可”是一次性的;比如線程B連續調用瞭三次unpark函數,當線程A調用park函數就使用掉這個“許可”,如果線程A再次調用park,則進入等待狀態,見下例Example1
Example1: // 針對當前線程已經調用過unpark(多次調用unpark的效果和調用一次unpark的效果一樣) public static void main(String[] args) throws InterruptedException { Thread currThread = Thread.currentThread(); UNSAFE.unpark(currThread); UNSAFE.unpark(currThread); UNSAFE.unpark(currThread); UNSAFE.park(false, 0); UNSAFE.park(false, 0); System.out.println("SUCCESS!!!"); } // 恢復線程interrupt() && UNSAFE.unpark()運行結果一樣 public static void main(String[] args) throws InterruptedException { Thread currThread = Thread.currentThread(); new Thread(()->{ try { Thread.sleep(3000); System.err.println("sub thread end"); // currThread.interrupt(); UNSAFE.unpark(currThread); } catch (Exception e) { e.printStackTrace(); } }).start(); UNSAFE.park(false, 0); System.out.println("SUCCESS!!!"); } // 如果是相對時間也就是isAbsolute為false(註意這裡後面的單位納秒)到期的時候,與Thread.sleep效果相同,具體有什麼區別有待深入研究 //相對時間後面的參數單位是納秒 UNSAFE.park(false, 3000000000l); System.out.println("SUCCESS!!!"); long time = System.currentTimeMillis()+3000; //絕對時間後面的參數單位是毫秒 UNSAFE.park(true, time); System.out.println("SUCCESS!!!");
註意,unpark函數可以先於park調用。比如線程B調用unpark函數,給線程A發瞭一個“許可”,那麼當線程A調用park時,它發現已經有“許可”瞭,那麼它會馬上再繼續運行。實際上,park函數即使沒有“許可”,有時也會無理由地返回,實際上在SUN Jdk中,object.wait()也有可能被假喚醒;
註意:unpark方法最好不要在調用park前對當前線程調用unpark
Unsafe API
sun.misc.Unsafe類包含105個方法。實際上,對各種實體操作有幾組重要方法,其中的一些如下: Info.僅返回一些低級的內存信息 addressSize pageSize Objects.提供用於操作對象及其字段的方法 allocateInstance ##直接獲取對象實例 objectFieldOffset Classes.提供用於操作類及其靜態字段的方法 staticFieldOffset defineClass defineAnonymousClass ensureClassInitialized Arrays.操作數組 arrayBaseOffset arrayIndexScale Synchronization.低級的同步原語 monitorEnter tryMonitorEnter monitorExit compareAndSwapInt putOrderedInt Memory.直接內存訪問方法 allocateMemory copyMemory freeMemory getAddress getInt putInt
知識點
Unsafe.park()當遇到線程終止時,會直接返回(不同於Thread.sleep,Thread.sleep遇到thread.interrupt()會拋異常)
// Thread.sleep會拋異常 public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(()->{ try { System.err.println("sub thread start"); Thread.sleep(10000); System.err.println("sub thread end"); } catch (InterruptedException e) { e.printStackTrace(); } }); thread.start(); TimeUnit.SECONDS.sleep(3); thread.interrupt(); System.out.println("SUCCESS!!!"); } output==> sub thread start SUCCESS!!! java.lang.InterruptedException: sleep interrupted at java.lang.Thread.sleep(Native Method) at com.quancheng.ConcurrentTest.lambda$main$0(ConcurrentTest.java:13) at java.lang.Thread.run(Thread.java:745) Process finished with exit code 0 public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(()->{ System.err.println("sub thread start"); UNSAFE.park(false,0); System.err.println("sub thread end"); }); thread.start(); TimeUnit.SECONDS.sleep(3); UNSAFE.unpark(thread); System.out.println("SUCCESS!!!"); } output==> sub thread start sub thread end SUCCESS!!! Process finished with exit code 0
unpark無法恢復處於sleep中的線程,隻能與park配對使用,因為unpark發放的許可隻有park能監聽到
public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(() -> { try { System.err.println("sub thread start"); TimeUnit.SECONDS.sleep(10); System.err.println("sub thread end"); } catch (Exception e) { e.printStackTrace(); } }); thread.start(); TimeUnit.SECONDS.sleep(3); UNSAFE.unpark(thread); System.out.println("SUCCESS!!!"); }
park和unpark的靈活之處
上面已經提到,unpark函數可以先於park調用,這個正是它們的靈活之處。
一個線程它有可能在別的線程unPark之前,或者之後,或者同時調用瞭park,那麼因為park的特性,它可以不用擔心自己的park的時序問題,否則,如果park必須要在unpark之前,那麼給編程帶來很大的麻煩!!
”考慮一下,兩個線程同步,要如何處理?
在Java5裡是用wait/notify/notifyAll來同步的。wait/notify機制有個很蛋疼的地方是,比如線程B要用notify通知線程A,那麼線程B要確保線程A已經在wait調用上等待瞭,否則線程A可能永遠都在等待。編程的時候就會很蛋疼。
另外,是調用notify,還是notifyAll?
notify隻會喚醒一個線程,如果錯誤地有兩個線程在同一個對象上wait等待,那麼又悲劇瞭。為瞭安全起見,貌似隻能調用notifyAll瞭“
park/unpark模型真正解耦瞭線程之間的同步,線程之間不再需要一個Object或者其它變量來存儲狀態,不再需要關心對方的狀態
以上為個人經驗,希望能給大傢一個參考,也希望大傢多多支持WalkonNet。
推薦閱讀:
- Java基礎之Unsafe內存操作不安全類詳解
- Java並發編程系列之LockSupport的用法
- Java並發編程之LockSupport類詳解
- Java多線程之Park和Unpark原理
- 你一定不知道的Java Unsafe用法詳解