跟我一塊兒剖析 Java 併發源碼之 Unsafe

  本篇文章是系列文章《跟我一塊兒剖析Java併發源碼》的第一篇文章。之後會每週保持定時更新。這些系列文章算是對《java併發編程系統與模型》的一些補充吧,真心但願你們能支持這本書,您的支持就是我最大的動力!
  在java併發相關的源代碼學習中,有一個類常常出現,這個類就是位於sun.misc包中的Unsafe類。好比,屬於Java併發包中最重要的類之一的AbstractQueuedSynchronizer中就常常調用這個類的方法。
  今天就來簡單剖析一下這個類。
  Unsafe類是一個很低級別的類,執行低級別的不安全的操做。因此使用的時候要當心,只有那些得到信任的代碼才能調用。爲何說它是比較低級的呢?由於它能直接操做任意的內存。那爲何它是危險的呢?由於它能直接操做任意的內存。javascript

  Unsafe類方法衆多,一一講述沒太必要,聰明的大家看完這篇文章再看看源碼理解其餘方法絕對沒什麼問題。先來看看有compareAndSwap開頭的一系列方法,從名字就能夠看出這確定是使用的CAS算法。CAS算法對這裏再也不詳細說明了。在《java併發編程系統與模型》這本書有詳細敘述。java

  隨便拿一個compareAndSwapInt舉例:算法

public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);複製代碼

  這個方法能夠在在對象o的內存偏移offset後與指望值比較,若是等於指望值,就更新爲x。因爲是直接操做內存的。好比要這樣更新一個對象的某個屬性,就要獲得這個屬性在內存中的偏移量。unsafe提供了objectFieldOffset方法來獲得某個屬性在對象中的偏移量:編程

public native long objectFieldOffset(Field f);複製代碼

  好比有這個一個對象:bootstrap

class User{

   private  String   name

      }複製代碼

  要獲得屬性name 偏移量, 就可使用安全

nameOffset = unsafe.objectFieldOffset(User.class.getDeclaredField("name"));複製代碼

  還有一些put方法,好比併發

public native void putOrderedInt(Object obj, long offset, int value);複製代碼

就是直接把這個對象內存偏移offset而後直接賦int值。性能

Unsafe類是一個受保護的類,是不能直接在程序中使用的。直接的使用會拋出SecurityException異常,下面來測驗一下(import sun.misc.Unsafe 須要手工添加,Eclispe或者其餘IDE並不會直接提示):學習

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import sun.misc.Unsafe;

public class UnSafeTest {
    public static void main(String[] args) {
        try {
            Unsafe unsafe = Unsafe.getUnsafe();
            User user = new User();
            long ageOffset = unsafe.objectFieldOffset(filed);
            unsafe.putInt(user, ageOffset, 10);
            System.out.println(user.getAge());
            System.out.println(unsafe);
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}複製代碼

查看獲取實例的方法中有一個VM.isSystemDomainLoader檢測,若是不是的話,會拋出SecurityException:ui

@CallerSensitive
    public static Unsafe getUnsafe() {
        Class<?> caller = Reflection.getCallerClass();
        if (!VM.isSystemDomainLoader(caller.getClassLoader()))
            throw new SecurityException("Unsafe");
        return theUnsafe;
    }複製代碼

  來看看M.isSystemDomainLoader內部的方法:

public static boolean isSystemDomainLoader(ClassLoader loader) {
        return loader == null;
    }複製代碼

  在Java中,若是一個對象的classLoader等於null,這就說明這個對象的類加載器是boostrap classloader,那麼若是類是由bootstrap classloader加載的話,那麼它就是受信任的代碼。

  能夠直接打印String的ClassLoader來檢測一下結論是否正確:

System.out.println(String.class.getClassLoader());

  理論上有兩種方法能夠打破這種限制。一種就是將User類變成SystemDomainLoader,JAVA自己的類加載機制致使了改變SystemDomainLoader方法暫時較難作到,通用的都是經過反射的方法。一種是經過反射其實體變量theUnsafe:

public class UnSafeTest {
    public static void main(String[] args) {
        try {                
            User user = new User();
            Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafe.setAccessible(true);
            Unsafe UNSAFE = (Unsafe) theUnsafe.get(null);
            System.out.println(UNSAFE);
            Field filed = user.getClass().getDeclaredField("age");
            long ageOffset = UNSAFE.objectFieldOffset(filed);
            UNSAFE.putInt(user, ageOffset, 10);
            System.out.println(user.getAge());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}複製代碼

  打印user對象的age屬性,結果輸出了10。

  另一種就是經過其構造器反射,從新獲得一個實例:

public class UnSafeTest {
    public static void main(String[] args) {
        try {

            Constructor<Unsafe> con = Unsafe.class.getDeclaredConstructor();
            // 用該私有構造方法建立對象
            // IllegalAccessException:非法的訪問異常。
            // 暴力訪問
            con.setAccessible(true);// 值爲true則指示反射的對象在使用時應該取消Java語言訪問檢查。

            User user = new User();

            System.out.println(UNSAFE);
// Unsafe unsafe =(Unsafe) clazz.newInstance();
            Field filed = user.getClass().getDeclaredField("age");
            long ageOffset = UNSAFE.objectFieldOffset(filed);
            UNSAFE.putInt(user, ageOffset, 10);
            System.out.println(user.getAge());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}複製代碼

其中

Constructor<Unsafe> con = Unsafe.class.getDeclaredConstructor();複製代碼

也能夠替換成class.forname的形式:

Constructor<Unsafe> con = (Constructor<Unsafe>) Class.forName("sun.misc.Unsafe").getDeclaredConstructor();複製代碼

爲何要直接操做內存呢?

由於快啊!

若是說被修改的屬性是一個基本類型,那麼直接操做內存的優點並不大。可是若是被修改的屬性是一個對象,差異就比較大了。不信來作一個很是簡單的比較:

public class User {
    private Integer age;

    public int getAge() {
        return age;
    }

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



}複製代碼
public class UnSafeTest {
    public static void main(String[] args) {
        try {
            Constructor<Unsafe> con = (Constructor<Unsafe>) Class.forName("sun.misc.Unsafe").getDeclaredConstructor();
            con.setAccessible(true);
            User user = new User();
            Unsafe UNSAFE = con.newInstance(null);
            Field filed = user.getClass().getDeclaredField("age");
            long s1=System.currentTimeMillis();
            for(int i=0;i<1000000;i++){
                user.setAge(i);
            }
            System.out.println(System.currentTimeMillis()-s1);
            long ageOffset = UNSAFE.objectFieldOffset(filed);
            long s2=System.currentTimeMillis();
            for(int i=0;i<1000000;i++){
                UNSAFE.putInt(user, ageOffset, i);
            }
            System.out.println(System.currentTimeMillis()-s2);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}複製代碼

  set方法和putInt各執行一百萬次,性能差了好幾倍。若是被修改的屬性是一個很是複雜的對象的話,性能差距會更大。由於每次set值的時候,JVM內部依舊會每次去找這個對象屬性的內存偏移量。如今我直接將偏移量拿出來了,不用每次找偏移量了,速度加快那是必然滴,固然被修改的對象確定是一個對象。

  在下一週分析AbstractQueuedSynchronizer類的時候,還會結合AbstractQueuedSynchronizer類實現中如何具體的使用Unsafe類進行說明,這裏就暫時告一段落了。

小參考:

bandrzejczak.com/blog/2015/0…
stackoverflow.com/questions/1…
stackoverflow.com/questions/2…

相關文章
相關標籤/搜索