1. 程式人生 > >sun.misc.Unsafe操作手冊

sun.misc.Unsafe操作手冊

Java是一個安全的開發工具,它阻止開發人員犯很多低階的錯誤,而大部份的錯誤都是基於記憶體管理方面的。如果你想搞破壞,可以使用Unsafe這個類。這個類是屬於sun.* API中的類,並且它不是J2SE中真正的一部份,因此你可能找不到任何的官方文件,更可悲的是,它也沒有比較好的程式碼文件。

例項化sun.misc.Unsafe

如果你嘗試建立Unsafe類的例項,基於以下兩種原因是不被允許的:

  1. Unsafe類的建構函式是私有的;
  2. 雖然它有靜態的getUnsafe()方法,但是如果你嘗試呼叫Unsafe.getUnsafe(),會得到一個SecutiryException。這個類只有被JDK信任的類例項化。
    但是這總會是有變通的解決辦法的,一個簡單的方式就是使用反射進行例項化:
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);

使用sun.misc.Unsafe

1. 建立例項

通過allocateInstance()方法,你可以建立一個類的例項,但是卻不需要呼叫它的建構函式、初始化程式碼、各種JVM安全檢查以及其它的一些底層的東西。即使建構函式是私有,我們也可以通過這個方法建立它的例項。

public class UnsafePlayer {
    public
static void main(String[] args) throws Exception { //通過反射例項化Unsafe Field f = Unsafe.class.getDeclaredField("theUnsafe"); f.setAccessible(true); Unsafe unsafe = (Unsafe) f.get(null); //例項化私有的建構函式 Player player = (Player) unsafe.allocateInstance(Player.class
); player.setName("jack"); System.out.println(player.getName()); } public static class Player{ @Getter @Setter private String name; private Player(){} } }

2. 可以分配記憶體和釋放記憶體

類中提供的3個本地方法allocateMemory、reallocateMemory、freeMemory分別用於分配記憶體,擴充記憶體和釋放記憶體,與C語言中的3個方法對應。

3. 通過記憶體偏移地址修改變數值

public native long objectFieldOffset(Field field);
返回指定靜態field的記憶體地址偏移量,在這個類的其他方法中這個值只是被用作一個訪問特定field的一個方式。這個值對於給定的field是唯一的,並且後續對該方法的呼叫都應該返回相同的值。

public native int arrayBaseOffset(Class arrayClass);
獲取給定陣列中第一個元素的偏移地址。為了存取陣列中的元素,這個偏移地址與arrayIndexScale方法的非0返回值一起被使用。
public native int arrayIndexScale(Class arrayClass)
獲取使用者給定陣列定址的換算因子。如果不能返回一個合適的換算因子的時候就會返回0。這個返回值能夠與arrayBaseOffset一起使用去存取這個陣列class中的元素

public native boolean compareAndSwapInt(Object obj, long offset,int expect, int update);
在obj的offset位置比較integer field和期望的值,如果相同則更新。這個方法的操作應該是原子的,因此提供了一種不可中斷的方式更新integer field。當然還有與Object、Long對應的compareAndSwapObject和compareAndSwapLong方法。

public native void putOrderedInt(Object obj, long offset, int value);
設定obj物件中offset偏移地址對應的整型field的值為指定值。這是一個有序或者有延遲的putIntVolatile方法,並且不保證值的改變被其他執行緒立即看到。只有在field被volatile修飾並且期望被意外修改的時候使用才有用。當然還有與Object、Long對應的putOrderedObject和putOrderedLong方法。

public native void putObjectVolatile(Object obj, long offset, Object value);
設定obj物件中offset偏移地址對應的object型field的值為指定值。支援volatile store語義。
與這個方法對應的get方法為:
public native Object getObjectVolatile(Object obj, long offset);
獲取obj物件中offset偏移地址對應的object型field的值,支援volatile load語義
這兩個方法還有與Int、Boolean、Byte、Short、Char、Long、Float、Double等型別對應的相關方法.

public native void putObject(Object obj, long offset, Object value);
設定obj物件中offset偏移地址對應的object型field的值為指定值。與putObject方法對應的是getObject方法。Int、Boolean、Byte、Short、Char、Long、Float、Double等型別都有getXXX和putXXX形式的方法。

下面通過一個組合示例來了解一下如何使用它們,詳細如下:

public class UnsafePlayerCAS {
    public static void main(String[] args) throws Exception{
        //通過反射例項化Unsafe
        Field f = Unsafe.class.getDeclaredField("theUnsafe");
        f.setAccessible(true);
        Unsafe unsafe = (Unsafe) f.get(null);

        //例項化私有的建構函式
        Player player = (Player) unsafe.allocateInstance(Player.class);
        String name = "jack";
        int age = 19;
        player.setName(name);
        player.setAge(age);
        for (Field field : Player.class.getDeclaredFields()) {
            System.out.println(field.getName() + ":對應的記憶體偏移地址:"
                    + unsafe.objectFieldOffset(field));
        }
        //上面的輸出為 name:對應的記憶體偏移地址:16
        //age:對應的記憶體偏移地址:12
        //修改記憶體偏移地址為12的值(age),返回true,說明通過記憶體偏移地址修改age的值成功
        System.out.println(unsafe.compareAndSwapInt(player, 12, age, age + 1));
        System.out.println("age修改後的值:" + player.getAge());

        //修改記憶體偏移地址為12的值,但是修改後不保證立馬能被其他的執行緒看到。
        unsafe.putOrderedInt(player, 12, age + 2);
        System.out.println("age修改後的值:" + player.getAge());

        //修改記憶體偏移地址為16的值,volatile修飾,修改能立馬對其他執行緒可見
        unsafe.putObjectVolatile(player, 16, "tom");
        System.out.println("name:" + player.getName());
        System.out.println(unsafe.getObjectVolatile(player,16));
    }

    public static class Player{
        @Getter @Setter private String name;
        @Getter @Setter private int age;
        private Player(){}
    }
}

4. 掛起與恢復

將一個執行緒進行掛起是通過park方法實現的,呼叫 park後,執行緒將一直阻塞直到超時或者中斷等條件出現。unpark可以終止一個掛起的執行緒,使其恢復正常。整個併發框架中對執行緒的掛起操作被封裝在 LockSupport類中,LockSupport類中有各種版本pack方法,但最終都呼叫了Unsafe.park()方法。

public native void park(boolean isAbsolute, long timeout);
阻塞一個執行緒直到unpark出現、執行緒被中斷或者timeout時間到期。如果一個unpark呼叫已經出現了,這裡只計數。timeout為0表示永不過期。當isAbsolute為true時,timeout是相對於新紀元之後的毫秒。否則這個值就是超時前的納秒數。這個方法執行時也可能不合理地返回。

public native void unpark(Thread thread);
釋放被park建立的在一個執行緒上的阻塞.這個方法也可以被使用來終止一個先前呼叫park導致的阻塞這個操作操作時不安全的,因此執行緒必須保證是活的.這是java程式碼不是native程式碼。引數thread指要解除阻塞的執行緒。

下面來看一下LockSupport類中關於Unsafe.park和Unsafe.unpark的使用:

private static void setBlocker(Thread t, Object arg) {
    // Even though volatile, hotspot doesn't need a write barrier here.
    UNSAFE.putObject(t, parkBlockerOffset, arg);
}
//恢復阻塞執行緒
public static void unpark(Thread thread) {
    if (thread != null)
        UNSAFE.unpark(thread);
}
//一直阻塞當前執行緒
public static void park(Object blocker) {
    Thread t = Thread.currentThread();
    setBlocker(t, blocker);
    UNSAFE.park(false, 0L);
    setBlocker(t, null);
}
//阻塞當前執行緒nanos納秒
public static void parkNanos(Object blocker, long nanos) {
    if (nanos > 0) {
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);
        UNSAFE.park(false, nanos);
        setBlocker(t, null);
    }
}

public static void parkUntil(Object blocker, long deadline) {
    Thread t = Thread.currentThread();
    setBlocker(t, blocker);
    UNSAFE.park(true, deadline);
    setBlocker(t, null);
}
//一直阻塞當前執行緒
public static void park() {
    UNSAFE.park(false, 0L);
}
//阻塞當前執行緒nanos納秒
public static void parkNanos(long nanos) {
    if (nanos > 0)
        UNSAFE.park(false, nanos);
}

public static void parkUntil(long deadline) {
    UNSAFE.park(true, deadline);
}

下面是使用LockSupport的示例:

public class LockDemo {
    public static void main(String[] args) throws InterruptedException {
        ThreadPark threadPark = new ThreadPark();
        threadPark.start();
        ThreadUnpark threadUnPark = new ThreadUnpark(threadPark);
        threadUnPark.start();
        //等待threadUnPark執行成功
        threadUnPark.join();
        System.out.println("執行成功....");
    }

    static class ThreadPark extends Thread{
        @Override public void run(){
            System.out.println(Thread.currentThread() + "我將被阻塞在這了60s....");
            LockSupport.parkNanos(1000000000L * 60);
            System.out.println(Thread.currentThread() +"我被恢復正常了....");
        }
    }

    static class ThreadUnpark extends Thread{
        public Thread thread = null;

        public ThreadUnpark(Thread thread) {
            this.thread = thread;
        }

        public void run(){
            System.out.println("提前恢復阻塞執行緒ThreadPark");
            //恢復阻塞執行緒
            LockSupport.unpark(thread);
        }
    }
}

程式輸出:

Thread[Thread-0,5,main]我將被阻塞在這了60s....
提前恢復阻塞執行緒ThreadPark
Thread[Thread-0,5,main]我被恢復正常了....
執行成功....

當然sun.misc.Unsafe中還有一些其它的功能,讀者可以繼續深挖。sun.misc.Unsafe提供了可以隨意檢視及修改JVM中執行時的資料結構,儘管這些功能在JAVA開發本身是不適用的,Unsafe是一個用於研究學習HotSpot虛擬機器非常棒的工具,因為它不需要呼叫C++程式碼,或者需要建立即時分析的工具。

參考資料

  1. https://blog.csdn.net/fenglibing/article/details/17138079
  2. https://blog.csdn.net/dfdsggdgg/article/details/51538601
  3. https://blog.csdn.net/aesop_wubo/article/details/7537278
  4. https://blog.csdn.net/dfdsggdgg/article/details/51543545

歡迎支援《RabbitMQ實戰指南》以及關注微信公眾號:朱小廝的部落格。