死磕 java魔法類之Unsafe解析

彤哥讀原始碼發表於2019-05-05

問題

(1)Unsafe是什麼?

(2)Unsafe只有CAS的功能嗎?

(3)Unsafe為什麼是不安全的?

(4)怎麼使用Unsafe?

簡介

本章是java併發包專題的第一章,但是第一篇寫的卻不是java併發包中類,而是java中的魔法類sun.misc.Unsafe。

Unsafe為我們提供了訪問底層的機制,這種機制僅供java核心類庫使用,而不應該被普通使用者使用。

但是,為了更好地瞭解java的生態體系,我們應該去學習它,去了解它,不求深入到底層的C/C++程式碼,但求能瞭解它的基本功能。

獲取Unsafe的例項

檢視Unsafe的原始碼我們會發現它提供了一個getUnsafe()的靜態方法。

@CallerSensitive
public static Unsafe getUnsafe() {
    Class var0 = Reflection.getCallerClass();
    if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
        throw new SecurityException("Unsafe");
    } else {
        return theUnsafe;
    }
}
複製程式碼

但是,如果直接呼叫這個方法會丟擲一個SecurityException異常,這是因為Unsafe僅供java內部類使用,外部類不應該使用它。

那麼,我們就沒有方法了嗎?

當然不是,我們有反射啊!檢視原始碼,我們發現它有一個屬性叫theUnsafe,我們直接通過反射拿到它即可。

public class UnsafeTest {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        Field f = Unsafe.class.getDeclaredField("theUnsafe");
        f.setAccessible(true);
        Unsafe unsafe = (Unsafe) f.get(null);
    }
}
複製程式碼

使用Unsafe例項化一個類

假如我們有一個簡單的類如下:

class User {
    int age;

    public User() {
        this.age = 10;
    }
}
複製程式碼

如果我們通過構造方法例項化這個類,age屬性將會返回10。

User user1 = new User();
// 列印10
System.out.println(user1.age);
複製程式碼

如果我們呼叫Unsafe來例項化呢?

User user2 = (User) unsafe.allocateInstance(User.class);
// 列印0
System.out.println(user2.age);
複製程式碼

age將返回0,因為Unsafe.allocateInstance()只會給物件分配記憶體,並不會呼叫構造方法,所以這裡只會返回int型別的預設值0。

修改私有欄位的值

使用Unsafe的putXXX()方法,我們可以修改任意私有欄位的值。

public class UnsafeTest {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, InstantiationException {
        Field f = Unsafe.class.getDeclaredField("theUnsafe");
        f.setAccessible(true);
        Unsafe unsafe = (Unsafe) f.get(null);

        User user = new User();
        Field age = user.getClass().getDeclaredField("age");
        unsafe.putInt(user, unsafe.objectFieldOffset(age), 20);

        // 列印20
        System.out.println(user.getAge());
    }
}

class User {
    private int age;

    public User() {
        this.age = 10;
    }

    public int getAge() {
        return age;
    }
}
複製程式碼

一旦我們通過反射呼叫得到欄位age,我們就可以使用Unsafe將其值更改為任何其他int值。(當然,這裡也可以通過反射直接修改)

丟擲checked異常

我們知道如果程式碼丟擲了checked異常,要不就使用try...catch捕獲它,要不就在方法簽名上定義這個異常,但是,通過Unsafe我們可以丟擲一個checked異常,同時卻不用捕獲或在方法簽名上定義它。

// 使用正常方式丟擲IOException需要定義在方法簽名上往外拋
public static void readFile() throws IOException {
    throw new IOException();
}
// 使用Unsafe丟擲異常不需要定義在方法簽名上往外拋
public static void readFileUnsafe() {
    unsafe.throwException(new IOException());
}
複製程式碼

使用堆外記憶體

如果程式在執行過程中JVM上的記憶體不足了,會導致頻繁的進行GC。理想情況下,我們可以考慮使用堆外記憶體,這是一塊不受JVM管理的記憶體。

使用Unsafe的allocateMemory()我們可以直接在堆外分配記憶體,這可能非常有用,但我們要記住,這個記憶體不受JVM管理,因此我們要呼叫freeMemory()方法手動釋放它。

假設我們要在堆外建立一個巨大的int陣列,我們可以使用allocateMemory()方法來實現:

class OffHeapArray {
    // 一個int等於4個位元組
    private static final int INT = 4;
    private long size;
    private long address;

    private static Unsafe unsafe;
    static {
        try {
            Field f = Unsafe.class.getDeclaredField("theUnsafe");
            f.setAccessible(true);
            unsafe = (Unsafe) f.get(null);
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    // 構造方法,分配記憶體
    public OffHeapArray(long size) {
        this.size = size;
        // 引數位元組數
        address = unsafe.allocateMemory(size * INT);
    }
    
    // 獲取指定索引處的元素
    public int get(long i) {
        return unsafe.getInt(address + i * INT);
    }
    // 設定指定索引處的元素
    public void set(long i, int value) {
        unsafe.putInt(address + i * INT, value);
    }
    // 元素個數
    public long size() {
        return size;
    }
    // 釋放堆外記憶體
    public void freeMemory() {
        unsafe.freeMemory(address);
    }
}
複製程式碼

在構造方法中呼叫allocateMemory()分配記憶體,在使用完成後呼叫freeMemory()釋放記憶體。

使用方式如下:

OffHeapArray offHeapArray = new OffHeapArray(4);
offHeapArray.set(0, 1);
offHeapArray.set(1, 2);
offHeapArray.set(2, 3);
offHeapArray.set(3, 4);
offHeapArray.set(2, 5); // 在索引2的位置重複放入元素

int sum = 0;
for (int i = 0; i < offHeapArray.size(); i++) {
    sum += offHeapArray.get(i);
}
// 列印12
System.out.println(sum);

offHeapArray.freeMemory();
複製程式碼

最後,一定要記得呼叫freeMemory()將記憶體釋放回作業系統。

CompareAndSwap操作

JUC下面大量使用了CAS操作,它們的底層是呼叫的Unsafe的CompareAndSwapXXX()方法。這種方式廣泛運用於無鎖演算法,與java中標準的悲觀鎖機制相比,它可以利用CAS處理器指令提供極大的加速。

比如,我們可以基於Unsafe的compareAndSwapInt()方法構建執行緒安全的計數器。

class Counter {
    private volatile int count = 0;

    private static long offset;
    private static Unsafe unsafe;
    static {
        try {
            Field f = Unsafe.class.getDeclaredField("theUnsafe");
            f.setAccessible(true);
            unsafe = (Unsafe) f.get(null);
            offset = unsafe.objectFieldOffset(Counter.class.getDeclaredField("count"));
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    public void increment() {
        int before = count;
        // 失敗了就重試直到成功為止
        while (!unsafe.compareAndSwapInt(this, offset, before, before + 1)) {
            before = count;
        }
    }

    public int getCount() {
        return count;
    }
}
複製程式碼

我們定義了一個volatile的欄位count,以便對它的修改所有執行緒都可見,並在類載入的時候獲取count在類中的偏移地址。

在increment()方法中,我們通過呼叫Unsafe的compareAndSwapInt()方法來嘗試更新之前獲取到的count的值,如果它沒有被其它執行緒更新過,則更新成功,否則不斷重試直到成功為止。

我們可以通過使用多個執行緒來測試我們的程式碼:

Counter counter = new Counter();
ExecutorService threadPool = Executors.newFixedThreadPool(100);

// 起100個執行緒,每個執行緒自增10000次
IntStream.range(0, 100)
    .forEach(i->threadPool.submit(()->IntStream.range(0, 10000)
        .forEach(j->counter.increment())));

threadPool.shutdown();

Thread.sleep(2000);

// 列印1000000
System.out.println(counter.getCount());
複製程式碼

park/unpark

JVM在上下文切換的時候使用了Unsafe中的兩個非常牛逼的方法park()和unpark()。

當一個執行緒正在等待某個操作時,JVM呼叫Unsafe的park()方法來阻塞此執行緒。

當阻塞中的執行緒需要再次執行時,JVM呼叫Unsafe的unpark()方法來喚醒此執行緒。

我們之前在分析java中的集合時看到了大量的LockSupport.park()/unpark(),它們底層都是呼叫的Unsafe的這兩個方法。

總結

使用Unsafe幾乎可以操作一切:

(1)例項化一個類;

(2)修改私有欄位的值;

(3)丟擲checked異常;

(4)使用堆外記憶體;

(5)CAS操作;

(6)阻塞/喚醒執行緒;

彩蛋

論例項化一個類的方式?

(1)通過構造方法例項化一個類;

(2)通過Class例項化一個類;

(3)通過反射例項化一個類;

(4)通過克隆例項化一個類;

(5)通過反序列化例項化一個類;

(6)通過Unsafe例項化一個類;

public class InstantialTest {

    private static Unsafe unsafe;
    static {
        try {
            Field f = Unsafe.class.getDeclaredField("theUnsafe");
            f.setAccessible(true);
            unsafe = (Unsafe) f.get(null);
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
    
    public static void main(String[] args) throws Exception {
        // 1. 構造方法
        User user1 = new User();
        // 2. Class,裡面實際也是反射
        User user2 = User.class.newInstance();
        // 3. 反射
        User user3 = User.class.getConstructor().newInstance();
        // 4. 克隆
        User user4 = (User) user1.clone();
        // 5. 反序列化
        User user5 = unserialize(user1);
        // 6. Unsafe
        User user6 = (User) unsafe.allocateInstance(User.class);

        System.out.println(user1.age);
        System.out.println(user2.age);
        System.out.println(user3.age);
        System.out.println(user4.age);
        System.out.println(user5.age);
        System.out.println(user6.age);
    }

    private static User unserialize(User user1) throws Exception {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D://object.txt"));
        oos.writeObject(user1);
        oos.close();

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D://object.txt"));
        // 反序列化
        User user5 = (User) ois.readObject();
        ois.close();
        return user5;
    }

    static class User implements Cloneable, Serializable {
        private int age;

        public User() {
            this.age = 10;
        }

        @Override
        protected Object clone() throws CloneNotSupportedException {
            return super.clone();
        }
    }
}
複製程式碼

歡迎關注我的公眾號“彤哥讀原始碼”,檢視更多原始碼系列文章, 與彤哥一起暢遊原始碼的海洋。

qrcode

相關文章