深讀源碼-java魔法類之Unsafe解析

問題

(1)Unsafe是什麼?

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

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

(4)怎麼使用Unsafe?

簡介

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

很多低級語言中可用的技巧在Java中都是不被允許的。Java是一個安全的開發工具,它阻止開發人員犯很多低級的錯誤,而大部

份的錯誤都是基於內存管理方面的。我們知道JAVA作爲高級語言的重要創新一點就是在於JVM的內存管理功能,這完全區別於C

語言開發過程中需要對變量的內存分配小心控制,JVM很大程度解放了碼農對於內存的調整。

一直以來,JAVA在大多數人心目中沒有辦法對內存進行操作的,其實不然,Unsafe類就是一把操作JAVA內存的鑰匙。如果你想

搞破壞,可以使用Unsafe這個類。這個類是屬於sun.* API中的類。

Unsafe做操作的是直接內存區,所以該類沒有辦法通過HotSpot的GC進行回收,需要進行手動回收,因此在使用此類時需要注意

內存泄漏(Memory Leak)和內存溢出(Out Of Memory)。

因爲這是一個平臺相關的類,因此在實際開發中,建議不要使用。但是,爲了更好地瞭解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內存操作案例

設置基本數據類型變量的值:

public class UnsafeDemo {
    private int i = 0;

    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        //獲取Unsafe實例
        Field f = Unsafe.class.getDeclaredField("theUnsafe"); // Internal reference
        f.setAccessible(true);//設置爲true,通過反射獲取私有變量的時候,會忽略訪問修飾符的檢查  
        Unsafe unsafe = (Unsafe) f.get(null);

        //獲取字段i在內存中偏移量
        long offset = unsafe.objectFieldOffset(UnsafeDemo.class.getDeclaredField("i"));

        //創建對象實例,設置字段的值
        UnsafeDemo unsafeDemo = new UnsafeDemo();
        unsafe.putInt(unsafeDemo, offset, 100);

        //打印結果
        System.out.println(unsafeDemo.i);
    }
}

上述代碼輸出:

100

注意在這個案例中,我們不是直接給int型變量i賦值,而是通過調用以下方法進行賦值:

unsafe.putInt(unsafeDemo, offset, 100);

其中offset是表示的是i在內存中的偏移量。何謂偏移量?

 JVM的實現可以自由選擇如何實現Java對象的“佈局”,也就是在內存裏Java對象的各個部分放在哪裏,包括對象的實例字段和一些元數據之類。sun.misc.Unsafe裏關於對象字段訪問的方法把對象佈局抽象出來,它提供了objectFieldOffset()方法用於獲取某個字段相對Java對象的“起始地址”的偏移量,也提供了getInt、getLong、getObject之類的方法可以使用前面獲取的偏移量來訪問某個Java對象的某個字段。

在上例中,我們通過putInt方法給一個int變量i賦值,類似的,Unsafe也提供了putLong、putFloat、putDouble、putChar、

putByte、putShort、putBoolean、以及putObject等方法給對應類型的變量賦值。並提供了相應的get方法。

突破限制創建實例

通過allocateInstance()方法,你可以創建一個類的實例,但是卻不需要調用它的構造函數、初使化代碼、各種JVM安全檢查以及

其它的一些底層的東西。即使構造函數是私有,我們也可以通過這個方法創建它的實例。

(這個對單例模式情有獨鍾的程序員來說將會是一個噩夢,它們沒有辦法阻止這種方式調用)

看下面一個實例(注:爲了配合這個主題,譯者將原實例中的public構造函數修改爲了私有的):

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

        // 這將在不進行任何初始化的情況下創建player類的實例
        Player player = (Player) unsafe.allocateInstance(Player.class);
        //打印年齡
        System.out.println(player.getAge());

        //給Player未實例化對象 設置年齡
        player.setAge(45);
        //打印年齡
        System.out.println(player.getAge());
    }
}

class Player {
    private int age;

    private Player() {
        this.age = 50;
    }

    public int getAge() {
        return this.age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

拋出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管理的。堆外內存介紹傳送門:《JAVA堆外內存的簡介和使用》

使用Unsafe的allocateMemory()我們可以直接在堆外分配內存,這可能非常有用,但我們要記住,這個內存不受JVM管理,因此

我們要調用freeMemory()方法手動釋放它。

假設我們要在堆外創建一個巨大的int數組,我們可以使用allocateMemory()方法來實現,在構造方法中調用allocateMemory()分配

內存,在使用完成後調用freeMemory()釋放內存,代碼如下:

import sun.misc.Unsafe;

import java.lang.reflect.Field;

/**
 * @author suidd
 * @name OffHeapArray
 * @description 堆外創建一個巨大的int數組,進行數據存儲、讀取demo
 * @date 2020/5/20 17:34
 * Version 1.0
 **/
public 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);
    }

    public static void main(String[] args) {
        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()方法構建線程安全的計數器。

我們定義了一個volatile的字段count,以便對它的修改所有線程都可見,並在類加載的時候獲取count在類中的偏移地址。

在increment()方法中,我們通過調用Unsafe的compareAndSwapInt()方法來嘗試更新之前獲取到的count的值,如果它沒有被其

它線程更新過,則更新成功,否則不斷重試直到成功爲止。

我們可以通過使用多個線程來測試我們的代碼:

import sun.misc.Unsafe;

import java.lang.reflect.Field;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.IntStream;

/**
 * @author suidd
 * @name UnsafeCounter
 * @description 基於Unsafe的compareAndSwapInt()方法構建線程安全的計數器Demo
 * @date 2020/5/20 17:42
 * Version 1.0
 **/
public class UnsafeCounter {
    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(UnsafeCounter.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;
    }

    public static void main(String[] args) throws InterruptedException {
        UnsafeCounter counter = new UnsafeCounter();
        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)阻塞/喚醒線程;

(7)sun.misc.Unsafe提供了可以隨意查看及修改JVM中運行時的數據結構,儘管這些功能在JAVA開發本身是不適用的,Unsafe

是一個用於研究學習HotSpot虛擬機非常棒的工具,因爲它不需要調用C++代碼,或者需要創建即時分析的工具。

 

彩蛋

論實例化一個類的方式?

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

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

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

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

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

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

import sun.misc.Unsafe;

import java.io.*;
import java.lang.reflect.Field;

/**
 * @author suidd
 * @name InstantialTest
 * @description 實例化一個類的方式
 * @date 2020/5/20 17:47
 * Version 1.0
 **/
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();
        }
    }
}

參考鏈接:https://www.cnblogs.com/tong-yuan/p/Unsafe.html

參考鏈接:http://www.tianshouzhi.com/api/tutorials/mutithread/304

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章