你一定不知道的Java Unsafe用法詳解
Unsafe是什麼
首先我們說Unsafe類位於rt.jar裡面sun.misc包下面,Unsafe翻譯過來是不安全的,這倒不是說這個類是不安全的,而是說開發人員使用Unsafe是不安全的,也就是不推薦開發人員直接使用Unsafe。而且Oracle JDK源碼包裡面是沒有Unsafe的源碼的。其實JUC包裡面的類大部分都用到瞭Unsafe,可以說Unasfe是java並發包的基石。
如何正確地獲取Unsafe對象
我們從源碼中看如何獲取Unsafe對象
private Unsafe() { }
首先構造方法私有化,這就說明我們不能通過new Unsafe的方式創建Unsafe對象。
@CallerSensitive public static Unsafe getUnsafe() { Class var0 = Reflection.getCallerClass(); if (!VM.isSystemDomainLoader(var0.getClassLoader())) { throw new SecurityException("Unsafe"); } else { return theUnsafe; } }
我又發現有一個靜態方法,方法名為getUnsafe,而且返回值為Unsafe,看來調用這個方法可以獲取Unsafe對象。
為此我又編寫瞭如下代碼測試這樣是否行得通
public static void main(String[] args) throws Exception{ Unsafe unsafe = Unsafe.getUnsafe(); System.out.println(unsafe); }
誰知道控制臺竟然報錯瞭
看錯誤提示信息是權限方面的錯誤,但是我看AtomicBoolean類獲取Unsafe的方式就是調用getUnsafe方法,可能是隻允許JDK內部的類可以通過這種方式訪問吧,這裡我們不深究,再想別的辦法獲取。
繼續看源碼找突破口
Unsafe類裡面第一個常量是 private static final Unsafe theUnsafe; 用static和final修飾而且沒有直接賦值,這就說明肯定有靜態代碼塊對theUnsafe賦值瞭,然後再類的底部發現瞭。
static { registerNatives(); Reflection.registerMethodsToFilter(Unsafe.class, new String[]{"getUnsafe"}); theUnsafe = new Unsafe(); ARRAY_BOOLEAN_BASE_OFFSET = theUnsafe.arrayBaseOffset(boolean[].class); ARRAY_BYTE_BASE_OFFSET = theUnsafe.arrayBaseOffset(byte[].class); ARRAY_SHORT_BASE_OFFSET = theUnsafe.arrayBaseOffset(short[].class); ARRAY_CHAR_BASE_OFFSET = theUnsafe.arrayBaseOffset(char[].class); ARRAY_INT_BASE_OFFSET = theUnsafe.arrayBaseOffset(int[].class); ARRAY_LONG_BASE_OFFSET = theUnsafe.arrayBaseOffset(long[].class); ARRAY_FLOAT_BASE_OFFSET = theUnsafe.arrayBaseOffset(float[].class); ARRAY_DOUBLE_BASE_OFFSET = theUnsafe.arrayBaseOffset(double[].class); ARRAY_OBJECT_BASE_OFFSET = theUnsafe.arrayBaseOffset(Object[].class); ARRAY_BOOLEAN_INDEX_SCALE = theUnsafe.arrayIndexScale(boolean[].class); ARRAY_BYTE_INDEX_SCALE = theUnsafe.arrayIndexScale(byte[].class); ARRAY_SHORT_INDEX_SCALE = theUnsafe.arrayIndexScale(short[].class); ARRAY_CHAR_INDEX_SCALE = theUnsafe.arrayIndexScale(char[].class); ARRAY_INT_INDEX_SCALE = theUnsafe.arrayIndexScale(int[].class); ARRAY_LONG_INDEX_SCALE = theUnsafe.arrayIndexScale(long[].class); ARRAY_FLOAT_INDEX_SCALE = theUnsafe.arrayIndexScale(float[].class); ARRAY_DOUBLE_INDEX_SCALE = theUnsafe.arrayIndexScale(double[].class); ARRAY_OBJECT_INDEX_SCALE = theUnsafe.arrayIndexScale(Object[].class); ADDRESS_SIZE = theUnsafe.addressSize(); }
第四行對theUnsafe進行瞭賦值。也就是說在類加載完成後Unsafe裡面的theUnsafe常量就已經賦值好瞭Unsafe對象,如果我們想獲取Unsafe對象隻要用反射拿到theUnsafe屬性就可以瞭。
/** * 獲得Unsafe * @throws NoSuchFieldException * @throws IllegalAccessException */ public static Unsafe getUnsafe() throws NoSuchFieldException, IllegalAccessException { Field field = Unsafe.class.getDeclaredField("theUnsafe"); //私有屬性可以訪問 field.setAccessible(true); Unsafe unsafe = (Unsafe) field.get(null); System.out.println(unsafe); return unsafe; }
Unsafe實現CAS鎖
CAS是compare and swap的縮寫,中文翻譯成比較並交換。 在juc包下Atomic開頭的類都是使用的CAS鎖實現的並發條件下對一個變量賦值不覆蓋的。我們也可以自己使用Unsafe實現CAS鎖。
interface Counter{ void increment(); long getCounter(); } /** * 自己用unsafe實現CAS鎖 */ static class CasCounter implements Counter{ private volatile long counter = 0; private Unsafe unsafe; private long offset; CasCounter() throws NoSuchFieldException, IllegalAccessException { unsafe = getUnsafe(); //獲得該類counter屬性內存偏移量起始位置 offset = unsafe.objectFieldOffset(CasCounter.class.getDeclaredField("counter")); } @Override public void increment() { long current = counter; //循環判斷是否賦值成功 第一個參數為調用本方法的對象,第二個參數為要更改屬性的內存偏移量,第三個參數是未修改之前的值,第四個參數是想要修改為那個值。 while (!unsafe.compareAndSwapLong(this,offset,current,current+1)){ current = counter; } } @Override public long getCounter() { return counter; } }
這裡我們主要看unsafe.objectFieldOffset(CasCounter.class.getDeclaredField(“counter”));這一行代碼,因為通過CAS對屬性值進行更改是直接在內存上進行更改的,所以我們需要拿到這個對象的counter屬性的內存偏移量。
再看increment方法,這裡我們在更改之前先拿到counter的值,unsafe.compareAndSwapLong方法就是根據內存偏移量進行更改值的,第一個參數確定那個對象,第二個參數確定那個屬性,第三個參數比對要更改屬性的原值,第四個參數要更改的值。如果更改成功則返回true,更改失敗返回false。這裡的邏輯是更改失敗就一直更改,知道更改成功才跳出循環。這樣就會有效的防止屬性值被覆蓋的問題。 我們寫的CasCounter類就實現瞭AtomicInteger類的部分功能。
使用Unsafe創建對象
我們都知道反射可以‘走後門’創建對象,其實Unsafe也是可以的
static class Simple{ static { System.out.println("類初始化"); } private long l = 0; public Simple(){ this.l = 1; System.out.println("對象初始化"); } public long get(){ return l; } } public static void main(String[] args) throws Exception { Unsafe unsafe = getUnsafe(); //相當於直接在內存中開辟一塊地址,不運行構造方法 Simple simple = (Simple) unsafe.allocateInstance(Simple.class); System.out.println("通過unsafe創建對象不會運行構造方法: " + simple.get()); System.out.println("但是可以通過對象獲得class對象" + simple.getClass()); System.out.println("也可以拿到類加載器 " + simple.getClass().getClassLoader()); }
控制臺輸出如下
這裡我們發現使用Unsafe創建對象並沒有運行構造方法,而隻是將對象創建出來瞭。而使用反射創建對象是會運行構造方法的和使用new的方式創建對象別無二致。所以不推薦使用Unsafe創建對象。
Unsafe加載類
既然Unsafe是直接操作的內存那應該也可以加載類,下面我們看看Unsafe是如何加載類的。
我們先自己編寫A類
public class A { private int i = 0; public A(){ this.i = 10; } public int get(){ return i; } }
然後運行javac A.java 生成A.class此時A.class的位置是F:\tmp
其次我們編寫Unsafe加載class的代碼
/** * 通過class文件獲得二進制 * @return * @throws IOException */ public static byte[] loadClassContent() throws IOException { File file = new File("F:\\tmp\\a.class"); FileInputStream fis = new FileInputStream(file); byte[] content = new byte[(int) file.length()]; fis.read(content); fis.close(); return content; } public static void main(String[] args) throws Exception { Unsafe unsafe = getUnsafe(); byte[] bytes = loadClassContent(); Class<?> aClass = unsafe.defineClass(null, bytes, 0, bytes.length,null,null); Method get = aClass.getMethod("get"); int i = (int) get.invoke(aClass.newInstance(), null); System.out.println(i); }
這裡unsafe.defineClass方法就是加載類的方法。
運行後輸出結果為10
這樣我們就實現瞭通過Unsafe加載類。
Unsafe更改私有屬性值
我們都知道反射可以更改對象私有屬性值,其實Unsafe也可以直接更改私有屬性值,代碼如下
static class Guard{ private int ACCESS_ALLOWED = 1; private boolean allow(){ return 42==ACCESS_ALLOWED; } public void work(){ if (allow()){ System.out.println("你進行瞭暗箱操作"); } } } public static void main(String[] args) throws Exception { Unsafe unsafe = getUnsafe(); Guard guard = new Guard(); Field access_allowed = guard.getClass().getDeclaredField("ACCESS_ALLOWED"); unsafe.putInt(guard,unsafe.objectFieldOffset(access_allowed),42); guard.work(); }
輸出結果為 你進行瞭暗箱操作 ,putInt方法第一個參數是要更改的屬性屬於哪個對象,第二個參數是要更改屬性的內存偏移量,第三個參數是要改成什麼值。其實就是直接更改指定內存地址中的int屬性的值。這樣我們就完成瞭使用Unsafe更改對象私有屬性值。
Unsafe類能直接操作內存的特性決定瞭它能走太多的後門瞭,而且大部分方法都是native修飾的,底層調用的C++。估計這也是Unsafe的不安全的原因。
總結
到此這篇關於Java Unsafe用法詳解的文章就介紹到這瞭,更多相關Java Unsafe用法內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- Java基礎之Unsafe內存操作不安全類詳解
- Java Unsafe學習筆記分享
- 淺析從同步原語看非阻塞同步以及Java中的應用
- Java 中的 Unsafe 魔法類的作用大全
- Java Unsafe 類的講解