Unsafe類的介紹和使用

近期在看JDK8的ConcurrentHashMap源碼時,發現裏面大量用到了Unsafe類的API,這裏來深刻研究一下。java

0 簡介

Java是一個安全的面向對象的編程語言。這裏的安全指的是什麼呢?不妨從什麼是不安全的角度來看看。linux

啥是不安全呢?這裏以C語言爲例,在C語言中:c++

  • 當編寫了開闢內存的代碼以後,須要想着手動去回收這塊內存,不然會形成內存泄漏;
  • 指針的操做提供了不少的便利,但若是出現了例如計算指針指向地址錯誤的問題,就會產生不少意想不到的結果;

其餘的不安全的狀況這裏再也不一一列舉。在Java中,很好的解決了C語言中諸多的不安全問題。例如用GC解決了內存回收的問題,而Java中自己沒有指針的概念,只是提供了引用類型,而引用類型是沒法直接修改其引用的內存地址的,因此指針誤操做的問題也獲得了有效的解決。編程

那麼,在Java中有沒有突破這些限制的方法呢?答案是確定的,他就是今天要聊的sum.misc.Unsafe類。安全

使用Unsafe的API,你能夠:微信

  • 開闢內存:allocateMemory
  • 擴充內存:reallocateMemory
  • 釋放內存:freeMemory
  • 在指定的內存塊中設置值:setMemory
  • 未經安全檢查的加載Class:defineClass
  • 原子性的更新實例對象指定偏移內存地址的值:compareAndSwapObject
  • 獲取系統的負載狀況:getLoadAverage,等同於linux中的uptime
  • 不調用構造函數來建立一個類的實例:allocateInstance

本文會介紹幾個API的使用方式,但主要關注可用於處理多線程併發問題的幾個API:多線程

  • compareAndSwapInt
  • getAndAddInt
  • getIntVolatile

1 在OpenJDK中查看Unsafe源碼

想要了解Unsafe,最直接的一個方式就是看源碼啦。併發

可是從Oracle官方下載的JDK中,Unsafe類是沒有註釋的。而OpenJDK中是有的,咱們能夠從OpenJDK源碼入手。app

下面介紹一下如何經過OpenJDK查看Unsafe源碼(一樣適用與查看其它類的源碼以及查看native實現)。dom

1.1 下載OpenJDK8

從下面連接下載 OpenJDK8

點下面這裏下載便可

OpenJDK8下載

下載後是zip包,解壓到一個地方就好。

1.2 下載NetBeans

由於咱們接下來要同時看C++和Java的源碼,NetBeans是同時支持這兩種語言的,因此這裏經過NetBeans來看OpenJDK的源碼。

下載地址爲:

NetBeans下載

http://137.254.56.27/download/trunk/nightly/latest

注意要下載這個ALL版本的,只有這個才能同時支持Java和C++。

NetBeans下載

1.3 導入OpenJDK源碼

  • 文件 -> 新建項目
  • 按下面這樣選擇,而後下一步
    openjdk_import1
  • 選擇剛纔解壓出來的openjdk根目錄
    openjdk_import2
  • 點擊「完成」

Unsafe的源碼在jdk/src/share/classes/sun/misc/Unsafe.java

2 Unsafe的獲取

經過源碼發現,Unsafe在JVM中是一個單例對象,咱們不能直接去new它。

private Unsafe() {}

private static final Unsafe theUnsafe = new Unsafe();
複製代碼

繼續往下看,而後能夠發現有這樣一個方法

/** * Provides the caller with the capability of performing unsafe * operations. * * <p> The returned <code>Unsafe</code> object should be carefully guarded * by the caller, since it can be used to read and write data at arbitrary * memory addresses. It must never be passed to untrusted code. * * <p> Most methods in this class are very low-level, and correspond to a * small number of hardware instructions (on typical machines). Compilers * are encouraged to optimize these methods accordingly. * * <p> Here is a suggested idiom for using unsafe operations: * * <blockquote><pre> * class MyTrustedClass { * private static final Unsafe unsafe = Unsafe.getUnsafe(); * ... * private long myCountAddress = ...; * public int getCount() { return unsafe.getByte(myCountAddress); } * } * </pre></blockquote> * * (It may assist compilers to make the local variable be * <code>final</code>.) * * @exception SecurityException if a security manager exists and its * <code>checkPropertiesAccess</code> method doesn't allow * access to the system properties. */
@CallerSensitive
public static Unsafe getUnsafe() {
    Class<?> caller = Reflection.getCallerClass();
    if (!VM.isSystemDomainLoader(caller.getClassLoader()))
        throw new SecurityException("Unsafe");
    return theUnsafe;
}
複製代碼

看似咱們能夠經過Unsafe.getUnsafe()來獲取Unsafe的實例。

其實否則,這個方法在return以前作了一個校驗,他會經過VM.isSystemDomainLoader方法校驗調用者的ClassLoader,此方法的實現以下

/** * Returns true if the given class loader is in the system domain * in which all permissions are granted. */
public static boolean isSystemDomainLoader(ClassLoader loader) {
    return loader == null;
}
複製代碼

若是調用者的ClassLoader==null,在getUnsafe方法中才能夠成功返回實例,不然會拋出SecurityException("Unsafe")異常。

啥時候是null呢?能夠想到只有由啓動類加載器(BootstrapClassLoader)加載的class纔是null。

PS:關於類加載器能夠參考筆者的另外一篇文章: 深刻分析Java類加載器原理

因此在咱們本身的代碼中是不能直接經過這個方法獲取Unsafe實例的。

還有啥別的辦法麼?有的!反射大法好!

在源碼中能夠發現,它是用theUnsafe字段來引用unsafe實例的,那咱們能夠嘗試經過反射獲取theUnsafe字段,進而獲取Unsafe實例。代碼以下:

import sun.misc.Unsafe;

import java.lang.reflect.Field;

public class UnsafeTest1 {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        Class klass = Unsafe.class;
        Field field = klass.getDeclaredField("theUnsafe");
        field.setAccessible(true);
        Unsafe unsafe = (Unsafe) field.get(null);
        System.out.println(unsafe.toString());
    }
}
複製代碼

運行此代碼,沒有報錯,大功告成。

3 Unsafe幾個API的使用

3.1 經過內存偏移量原子性的更新成員變量的值

import sun.misc.Unsafe;

import java.lang.reflect.Field;

/** * Description: * * @author zhiminxu * @package com.lordx.sprintbootdemo * @create_time 2019-03-22 */
public class UnsafeTest {

    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, InterruptedException {
        Test test = new Test();
        test.test();
    }
}

class Test {

    private int count = 0;

    public void test() throws NoSuchFieldException, IllegalAccessException {
        // 獲取unsafe實例
        Class klass = Unsafe.class;
        Field field = klass.getDeclaredField("theUnsafe");
        field.setAccessible(true);
        Unsafe unsafe = (Unsafe) field.get(null);

        // 獲取count域的Field
        Class unsafeTestClass = Test.class;
        Field fieldCount = unsafeTestClass.getDeclaredField("count");
        fieldCount.setAccessible(true);

        // 計算count的內存偏移量
        long countOffset = (int) unsafe.objectFieldOffset(fieldCount);
        System.out.println(countOffset);

        // 原子性的更新指定偏移量的值(將count的值修改成3)
        unsafe.compareAndSwapInt(this, countOffset, count, 3);

        // 獲取指定偏移量的int值
        System.out.println(unsafe.getInt(this, countOffset));
    }
}
複製代碼

3.2 用Unsafe模擬synchronized

用到了Unsafe中的monitorEnter和monitorExit方法,但monitorEnter後必定要記着monitorExit。

import sun.misc.Unsafe;

import java.lang.reflect.Field;

/** * Description: * * @author zhiminxu * @package com.lordx.sprintbootdemo * @create_time 2019-03-22 */
public class UnsafeTest {

    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, InterruptedException {
        final Test test = new Test();
        // 模擬兩個線程併發給Test.count遞增的場景
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 1000000; i++) {
                    test.addCount();
                }
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 1000000; i++) {
                    test.addCount();
                }
            }
        }).start();
        Thread.sleep(5000);
        System.out.println(test.getCount());
    }
}

class Test {

    private int count = 0;

    public int getCount() {
        return this.count;
    }

    private Unsafe unsafe;
    public Test() {
        try {
            Class klass = Unsafe.class;
            Field field = klass.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            unsafe = (Unsafe) field.get(null);
        }catch (Exception e) {
            e.printStackTrace();
        }
    }

    private Object lock = new Object();

    public void addCount() {
        // 給lock對象設置鎖
        unsafe.monitorEnter(lock);
        count++;
        // 給lock對象解鎖
        unsafe.monitorExit(lock);
    }


}
複製代碼

4 Unsafe中幾個線程安全API的實現原理

4.1 compareAndSwapInt

此方法在Unsafe中的源碼爲

/** * Atomically update Java variable to <tt>x</tt> if it is currently * holding <tt>expected</tt>. * 若是對象o指定offset所持有的值是expected,那麼將它原子性的改成值x。 * @return <tt>true</tt> if successful */
public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);
複製代碼

在OpenJDK中能夠看到這個方法的native實現,在unsafe.cpp中。

UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
  UnsafeWrapper("Unsafe_CompareAndSwapInt");
  // #1
  oop p = JNIHandles::resolve(obj);
  // #2
  jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
  // #3
  return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END
複製代碼

代碼#1將目標對象轉換爲oop,oop是本地實現中oopDesc類的實現,其定義在oop.hpp中。oopDesc是全部class的頂層baseClass,它描述了Java object的格式,使Java object中的field能夠被C++訪問。

代碼#2負責獲取oop中指定offset的內存地址,指針變量addr記錄的就是這個地址中存儲的int值。

代碼#3調用Atomic::cmpxchg來原子性的完成值得替換。

4.2 getAndAddInt

此方法的源碼以下

/** * Atomically adds the given value to the current value of a field * or array element within the given object <code>o</code> * at the given <code>offset</code>. * * @param o object/array to update the field/element in * @param offset field/element offset * @param delta the value to add * @return the previous value * @since 1.8 */
public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    do {
        v = getIntVolatile(o, offset);
    } while (!compareAndSwapInt(o, offset, v, v + delta));
    return v;
}
複製代碼

用於原子性的將值delta加到對象o的offset上。

getIntVolatile方法用於獲取對象o指定偏移量的int值,此操做具備volatile內存語義,也就是說,即便對象o指定offset的變量不是volatile的,次操做也會使用volatile語義,會強制從主存獲取值。

而後經過compareAndSwapInt來替換值,直到替換成功後,退出循環。


歡迎關注個人微信公衆號

公衆號
相關文章
相關標籤/搜索