跟我一起剖析 Java 併發原始碼之 Unsafe

徐家三少發表於2019-03-03

  本篇文章是系列文章《跟我一起剖析Java併發原始碼》的第一篇文章。以後會每週保持定時更新。這些系列文章算是對《java併發程式設計系統與模型》的一些補充吧,真心希望大家能支援這本書,您的支援就是我最大的動力!
  在java併發相關的原始碼學習中,有一個類經常出現,這個類就是位於sun.misc包中的Unsafe類。比如,屬於Java併發包中最重要的類之一的AbstractQueuedSynchronizer中就經常呼叫這個類的方法。
  今天就來簡單剖析一下這個類。
  Unsafe類是一個很低階別的類,執行低階別的不安全的操作。所以使用的時候要小心,只有那些獲得信任的程式碼才能呼叫。為什麼說它是比較低階的呢?因為它能直接操作任意的記憶體。那為什麼它是危險的呢?因為它能直接操作任意的記憶體。

  Unsafe類方法眾多,一一講述沒太必要,聰明的你們看完這篇文章再看看原始碼理解其他方法絕對沒什麼問題。先來看看有compareAndSwap開頭的一系列方法,從名字就可以看出這肯定是使用的CAS演算法。CAS演算法對這裡不再詳細說明了。在《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);複製程式碼

  比如有這個一個物件:


    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:

  @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…

相關文章