Unsafe-java的魔法類-AtomicInteger的原子操作

Unsafe-java的魔法類

Unsafe的介紹

Unsafe的源代碼:http://www.docjar.com/html/api/sun/misc/Unsafe.java.html

Unsafe類全限定名爲sun.misc.Unsafe,顧名思義不是安全的。

一般而言,編寫底層代碼或者影響JVM是很難實現的,當然你可以使用JNI來達到目的,JNI需要和C打交道。

在java平臺通過sun.misc.Unsafe的API,也可以進行底層編碼,比如操作目標對象的地址,直接修改屬性字段所在的地址的值…當然使用這個類比較危險,所以慎用。

Unsafe在原子類中大量使用到了,可以通過compareAndSwapXX方法調用底層操作糸統的原子操作指令,來進行原子操作。

Unsafe對象的獲取

考慮創建Unsafe對象之前,先來看下這個類的源代碼:

public final class Unsafe {
    private static final Unsafe theUnsafe;
    private Unsafe() {
    }
    @CallerSensitive
    public static Unsafe getUnsafe() {
        Class var0 = Reflection.getCallerClass();
        if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
            throw new SecurityException("Unsafe");
        } else {
            return theUnsafe;
        }
    }
    static {
        theUnsafe = new Unsafe();
    }

Unsafe 類有這麼一些特點:

  1. final修飾的
  2. 有一個靜態變量也是Unsafe類型的實例theUnsafe
  3. 構造器私有
  4. 靜態方法getUnsafe()可以獲得一個Unsafe實例對象theUnsafe,但是檢查了類加載器(只有JVM的引導加載器才允許,否則拋出SecurityException異常)
  5. 靜態代碼塊實例化了theUnsafe變量

那麼我們要創建Unsafe 的實例對象,怎麼做呢?

針對上面的特點針對性的解決。以下是兩種方案。

追加類到引導類加載器BoostrapClassloader

我們可以利用Unsafe類的靜態方法getUnsafe(),但是這個方法會檢查類加載器是否爲BoostrapClassloader。

所以我們可以將當前類所在路徑追加到BoostrapClassloader的掃描路徑下去。

這裏擴展一點: 得到BoostrapClassloader加載的路徑:

String property = System.getProperty("sun.boot.class.path");
for (String s : property.split(";")) {
    System.out.println(s);
}

輸出:

C:\Program Files\Java\jdk1.8.0_211\jre\lib\resources.jar
C:\Program Files\Java\jdk1.8.0_211\jre\lib\rt.jar
C:\Program Files\Java\jdk1.8.0_211\jre\lib\sunrsasign.jar
C:\Program Files\Java\jdk1.8.0_211\jre\lib\jsse.jar
C:\Program Files\Java\jdk1.8.0_211\jre\lib\jce.jar
C:\Program Files\Java\jdk1.8.0_211\jre\lib\charsets.jar
C:\Program Files\Java\jdk1.8.0_211\jre\lib\jfr.jar
C:\Program Files\Java\jdk1.8.0_211\jre\classes

擴展:得到Extend類加載器加載的路徑:

String property2 = System.getProperty("java.ext.dirs");
for (String s : property2.split(";")) {
    System.out.println(s);
}

輸出:

C:\Program Files\Java\jdk1.8.0_211\jre\lib\ext
C:\Windows\Sun\Java\lib\ext

Xbootclasspath擴展

Java 命令行提供瞭如何擴展BoostrapClassloader的簡單方法.

  • -Xbootclasspath: 新的jar 完全替換jdk的的Java class 搜索路徑,不建議;
  • -Xbootclasspath/a:追加的jar 追加在jdk的java class搜索路徑後面,很實用(多個jar在unix冒號分隔,windows分號分隔);
  • -Xbootclasspath/p:放在前面的jar 放jdk的java class搜索路徑前面,不建議;以免引起不必要的衝突。

所以我們可以通過在IDEA中設置jvm運行時參數(添加VM options):

-Xbootclasspath/a:D:\framework\concurrent\target\concurrent-1.0-SNAPSHOT.jar 

再run即可。注意這裏要打包成jar,才能追加

/*
運行時添加 VM options
-Xbootclasspath/a:D:\framework\concurrent\target\concurrent-1.0-SNAPSHOT.jar
*/
Unsafe unsafe0 = Unsafe.getUnsafe();
System.out.println("-Xbootclasspath/a:添加jar包:" + unsafe0);  // 可以獲得Unsafe實例對象

在這裏插入圖片描述

這種方式太難了…所以我們使用下面的第二種方式。

反射Field獲取Unsafe實例對象【推薦用法】

從Unsafe的特點來看,我們得知,其內部存在一個theUnsafe變量就是Unsafe的一個實例:

private static final Unsafe theUnsafe;

既然構造器不能用,我們就使用反射直接獲取該字段的值:

// 所以,選擇反射獲取
// 因爲內部存在一個單例實例:Unsafe theUnsafe;
Class clazz = Unsafe.class;
Field field = clazz.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe)field.get(null);   // 得到其內部的字段theUnsafe

Unsafe的API

Unsafe提供了大量的API(100多個方法),來操作底層。

主要分爲這麼幾大類:

Info獲得信息

返回一些底層的內存信息。

  • addressSize:本地指針大小,值一般爲4或者8;存儲在本地塊的原始類型由他們的內容信息決定。
  • pageSize:返回內存頁大小,字節爲單位,值爲2的n次方
// info
System.out.println(unsafe.pageSize());  // 4096
System.out.println(unsafe.addressSize());  // 8

Objects操作對象

提供操作對象以及它的字段的方法。

  • allocateInstance:分配一個實例對象,但是不會調用任何的構造器(變量值沒有初始化)

    • public native Object allocateInstance(Class cls) 
      

示例,很有意思:

@Data
public class UnsafeDemo {
    private String name = "zs";
    private int age = 18;
}

// 運行以下代碼的結果:
UnsafeDemo unsafeDemo3 = (UnsafeDemo)unsafe.allocateInstance(UnsafeDemo.class);
System.out.println(unsafeDemo3);  // UnsafeDemo(name=null, age=0)
UnsafeDemo unsafeDemo1 = new UnsafeDemo();
System.out.println(unsafeDemo1);   // UnsafeDemo(name=zs, age=18)
  • objectFieldOffset: 獲得某個類的某個字段在內存中的偏移量

    • public native long objectFieldOffset(Field f);
      

示例:獲得UnsafeDemo類的age字段在內存中的偏移量

long ageOffset =
                unsafe.objectFieldOffset(UnsafeDemo.class.getDeclaredField("age"));
System.out.println(ageOffset); // 12

Classes操作類對象

提供操作類對象、靜態字段的方法。

  • staticFieldOffset:獲得靜態字段的偏移量

    • public native long staticFieldOffset(Field f);
      

示例:

@Data
public class UnsafeDemo {
    private String name = "zs";
    private int age = 18;
    private static int status = 1;
}

// staticFieldOffset:獲得靜態字段的偏移量
Field status = UnsafeDemo.class.getDeclaredField("status");
long statusOffset = unsafe.staticFieldOffset(status);
System.out.println(statusOffset);  // 104
  • defineClass : 告知JVM定義一個類,但是不要做安全檢查;默認情況下,類加載器和保護域來自調用者的Class。

    • public native Class defineClass(String name, byte[] b, int off, int len,
      									ClassLoader loader,
      									ProtectionDomain protectionDomain);
      
  • defineAnonymousClass:定義一個不被類加載器、系統感知的類。

    • /**
      * @params hostClass 鏈接的上下文,訪問控制,類加載器
      * @params data      字節碼文件的字節數組形式
      * @params cpPatches 如果存在非空數據,則替換data中的
        829        */
      */
      public native Class defineAnonymousClass(Class hostClass, byte[] data, Object[] cpPatches);
      
  • ensureClassInitialized: 確保給定的類已經初始化,這通常需要結合獲取類的靜態字段庫。

    • public native void ensureClassInitialized(Class c);
      

Arrays操作數組

對數組的封裝。

  • arrayBaseOffset:數組對象的首元素的偏移量

    • public native int arrayBaseOffset(Class arrayClass)
      

示例:

int arrayBaseOffset =
                unsafe.arrayBaseOffset(new byte[]{1, 2, 3}.getClass());
System.out.println(arrayBaseOffset); // 16
  • arrayIndexScale:尋址因子

    • public native int arrayIndexScale(Class arrayClass);
      

Synchronization操作同步字

操作同步的一些底層封裝。

  • monitorEnter:鎖定對象, 必須通過monitorExit釋放鎖。

    • public native void monitorEnter(Object o);
      
  • tryMonitorEnter : 嘗試獲得鎖

    • public native boolean tryMonitorEnter(Object o);
      
  • monitorExit:釋放鎖

    • public native void monitorExit(Object o);
      
  • compareAndSwapIntCAS也是很實用的一個方法,AtomicInteger、AtomicBoolean等原子類都是通過Unsafe的CAP方法實現原子操作的。

    • /**
      原子操作:修改java變量的值爲x;
      如果對象o的偏移量offset(其實就是該對象的某個字段)表示的變量的值,目前是期望值expected,則將其修改爲x,返回true;
      如果目前是期望值不是expected,則不操作,返回false。
      */
      public final native boolean compareAndSwapInt(Object o, long offset,
                                                  int expected,
                                                  int x);
      

示例AtomicInteger#incrementAndGet方法的實現邏輯:

java.util.concurrent.atomic.AtomicInteger#incrementAndGet

public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

sun.misc.Unsafe#getAndAddInt

public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
        // 自旋+CAS
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}
  • putIntVolatile: putInt的Volatile版本

    • public native void    putIntVolatile(Object o, long offset, int x);
      
  • putOrderedInt: putIntVolatile的有序\惰性版本

    • public native void    putOrderedInt(Object o, long offset, int x);
      

Memory操作內存

直接訪問內存的方法。

  • allocateMemory:分配內存空間,並返回偏移量

    • public native long allocateMemory(long bytes);
      
  • copyMemory:複製內存空間

    • // @since 1.7
      public native void copyMemory(Object srcBase, long srcOffset,
          Object destBase, long destOffset,
          long bytes);
      
      public void copyMemory(long srcAddress, long destAddress, long bytes) {
      	copyMemory(null, srcAddress, null, destAddress, bytes);
      }
      
  • freeMemory:釋放內存空間

    • public native void freeMemory(long address);
      
  • getAddress: 獲得內存地址,無符號整數long

    • public native long getAddress(long address);
      
  • getInt:獲得指定偏移量的變量值

    • public native int getInt(Object o, long offset);
      
  • putInt:可以修改變量的值

    • /**
      參數一:對象
      參數二:字段偏移量
      參數三:要設置的值
      */
      public native void putInt(Object o, long offset, int x);
      

示例:

//通過魔法類修改對象實例的屬性值
UnsafeDemo unsafeDemo = new UnsafeDemo();
long ageOffset =
                unsafe.objectFieldOffset(UnsafeDemo.class.getDeclaredField("age"));
unsafe.putInt(unsafeDemo, ageOffset, 30);  // 將字段age的值改爲30

Unsafe實現CAS

案例:AtomicInteger的原子操作

示例AtomicInteger#incrementAndGet方法的實現邏輯:

java.util.concurrent.atomic.AtomicInteger#incrementAndGet

public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

sun.misc.Unsafe#getAndAddInt

public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
        // 自旋+CAS
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}

這裏分享的是常見的一些API,Unsafe還有很多其他類似的方法,不再一一列舉。

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