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的不安全的原因。
如果喜歡本篇文章不妨關注點贊收藏,有什麼困惑歡迎評論。
歡迎關注接地氣程式設計師公眾號。