Java無鎖系列——2、CAS 實現原理 Unsafe 類簡單介紹

概述

在上篇博客中,我簡單介紹了無鎖同步 CAS 如何使用以及部分它的特性。本篇我打算整理一下 CAS 的實現原理即 Unsafe 類相關的知識。


Unsafe

本篇博客分以下幾個模塊展開:

  1. Unsafe 類簡單介紹
  2. CAS 更新基礎類型原理
  3. CAS 更新對象引用原理
  4. CAS 更新數組類型原理
  5. CAS 更新對象屬性原理
  6. Unsafe 類方法總結
  7. Unsafe 類示例

1、Unsafe 類簡單介紹

Unsafe 類處於包 sun.misc 下,該包由 sun 公司內部實現,不屬於 J2EE 開發規範。

java 代碼中任何 CAS 操作最終都是通過調用 Unsafe 類中的 native 方式實現,也就是說:Unsafe 通過調用操作系統底層資源實現任務。

從名稱就可以看出,Unsafe 類是不安全的。它可以像C語言指針那樣直接操作內存。一般我們不建議直接使用 Unsafe 類處理任務。


2、CAS 更新基礎類型原理

上篇博客我們提到 CAS 可以更新基礎類型數據,這裏我們就拿 AtomicInteger 類的源碼進行分析:

private static final Unsafe unsafe = Unsafe.getUnsafe();

private static final long valueOffset;

static {
    try {
        valueOffset = unsafe.objectFieldOffset
            (AtomicInteger.class.getDeclaredField("value"));
    } catch (Exception ex) { throw new Error(ex); }
}

private volatile int value;

public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

通過上面的源碼,我們可以看出:AtomicInteger 類的 CAS 方法最終還是調用了 Unsafe 類的方法。Unsafe 類方法的源碼如下所示:

public native long objectFieldOffset(Field var1);

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

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

首先通過 Reflection.getCallerClass() 可以表明,調用此方法必須包含以下權限:

  • bootstrap class loader 加載的類可以調用
  • extension class loader 加載的類可以調用

而我們用戶編寫的類都是通過 application class loader 加載的,也就是說用戶編寫的代碼不能通過這個靜態方法直接獲取對象實例,並且 Unsafe 類本身也沒有提供公開的構造方法。

這樣做的原因也非常明顯:防止用戶直接使用 Unsafe 類。然而事實上,只要願意的話,同樣可以通過反射創建該類對象實例,相應出現的風險也需要程序員自己承擔。

下面我們再看 AtomicInteger 對象調用 Unsafe 類方法時所傳遞的參數:

  • this:對象本身
  • valueOffset:AtomicInteger 對象 value 屬性偏移地址
  • expect:期望值
  • update:新值

也就是說,AtomicInteger 首先根據 objectFieldOffset() 方法確定 value 屬性在對象上的偏移值。然後將對象本身,偏移值,期望值,新值作爲參數傳遞過去。在 compareAndSwapInt() 方法中:首先根據對象確定內存,然後根據偏移值獲取到要操作的內存地址,直接拿期望值和內存中的值做比較,如果相等的話就將新值寫入內存。

從這裏也就可以看出 Unsafe 類直接通過操作內存完成 CAS 操作,調用的方法本身又是 native 方法,因此操作本身就是原子的,也就是說不會出現線程安全問題。

有了上面的鋪墊,我們再來看另一個 AtomicInteger 常用的方法源碼:

AtomicInteger 源碼:

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

Unsafe 源碼:

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

   return var5;
}

public native int getIntVolatile(Object var1, long var2);

通過源碼我們可以很清晰的看出,AtomicInteger 類的自增操作實際上也是通過 while 循環配合 unsafe 類方法實現的,最終執行成功後將計算的結果從內存中直接讀出並返回。


3、CAS 更新對象引用原理

CAS 更新對象引用的原理實際上和更新基礎類型相似,下面我們直接看源碼:

AtomicReference 源碼:

private static final long valueOffset;

static {
    try {
        valueOffset = unsafe.objectFieldOffset
            (AtomicReference.class.getDeclaredField("value"));
    } catch (Exception ex) { throw new Error(ex); }
}

private volatile V value;

public final boolean compareAndSet(V expect, V update) {
    return unsafe.compareAndSwapObject(this, valueOffset, expect, update);
}

Unsafe 源碼:

public native long objectFieldOffset(Field var1);

public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);

從源碼來看,CAS 更新基礎數據和更新對象引用的原理基本是相同的,這裏我不做過多贅述。


4、CAS 更新數組類型原理

上篇博客中我們提到 CAS 更新數組類型有三種,這裏我主要拿 AtomicIntegerArray 的源碼來做說明:

AtomicIntegerArray 源碼:

private static final int base = unsafe.arrayBaseOffset(int[].class);

private static final int shift;

static {
    int scale = unsafe.arrayIndexScale(int[].class);
    if ((scale & (scale - 1)) != 0)
        throw new Error("data type scale not a power of two");
    shift = 31 - Integer.numberOfLeadingZeros(scale);
}

public final boolean compareAndSet(int i, int expect, int update) {
    return compareAndSetRaw(checkedByteOffset(i), expect, update);
}

private boolean compareAndSetRaw(long offset, int expect, int update) {
    return unsafe.compareAndSwapInt(array, offset, expect, update);
}

private long checkedByteOffset(int i) {
    if (i < 0 || i >= array.length)
        throw new IndexOutOfBoundsException("index " + i);

    return byteOffset(i);
}

private static long byteOffset(int i) {
    return ((long) i << shift) + base;
}

Unsafe 源碼:

public native int arrayIndexScale(Class<?> var1);

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

下面我直接給出 compareAndSwapInt() 方法中這四個參數分別代表的意義:

  • array:數組本身
  • offset:對應要操作的下標元素地址
  • expect:期望值
  • update:更新值

事實上 Unsafe 實現 CAS 的方式大同小異,都是通過對象和偏移量,確定要操作的內存地址,直接從內存層面比較期望值後判斷是否執行更新操作。

這裏我們主要分析一下如何獲取數組元素的內存地址:

  1. 首先通過 Unsafe 類的 arrayBaseOffset() 方法確定數組首個元素地址
  2. 其次通過 Unsafe 類的 arrayIndexScale() 方法確定數組中每個元素所佔的空間大小
  3. 判斷數組中元素對象所佔的大小是否 2 的冪次方(硬規定)
  4. 通過 31 - Integer.numberOfLeadingZeros(scale) 計算出每個元素轉二進制後,需要移動的零的數量
  5. 通過 byteOffset() 方法計算出參數下標所對應的實際地址
每個數組的元素地址 = 數組首元素地址 + 下標 * 每個數組元素的內存大小

numberOfLeadingZeros() 方法返回前綴 0 的數量:
32 位操作系統下,4轉二進制爲 00000000 00000000 00000000 00000100,此時前綴0的個數爲29

我們對上述等式進行變形:

下標 * 每個數組元素的內存大小 變形爲二進制形式:

假設數組中每個元素大小爲4位,轉換爲2進制後表示爲 100,此時 numberOfLeadingZeros() 方法計算前綴0有29個,31 - 29 後計算出後面有2個零。

address  = base + index * size
下標0:address = base + 0 * 4    等同於  base + 0 << 2
下標1:address = base + 1 * 4    等同於  base + 1 << 2
下標2:address = base + 2 * 4    等同於  base + 2 << 2
...

有了偏移量,後面的操作就沒有什麼好說的了,比較交換。


5、CAS 更新對象屬性原理

最後我們再來看看,CAS 更新對象屬性的原理。我們直接看代碼:

AtomicIntegerFieldUpdater 源碼:

@CallerSensitive
public static <U> AtomicIntegerFieldUpdater<U> newUpdater(Class<U> tclass,
                                                          String fieldName) {
    return new AtomicIntegerFieldUpdaterImpl<U>
        (tclass, fieldName, Reflection.getCallerClass());
}

AtomicIntegerFieldUpdaterImpl(final Class<T> tclass,
                              final String fieldName,
                              final Class<?> caller) {
    final Field field;
    final int modifiers;
    try {
        field = AccessController.doPrivileged(
            new PrivilegedExceptionAction<Field>() {
                public Field run() throws NoSuchFieldException {
                    return tclass.getDeclaredField(fieldName);
                }
            });
        modifiers = field.getModifiers();
        sun.reflect.misc.ReflectUtil.ensureMemberAccess(
            caller, tclass, null, modifiers);
        ClassLoader cl = tclass.getClassLoader();
        ClassLoader ccl = caller.getClassLoader();
        if ((ccl != null) && (ccl != cl) &&
            ((cl == null) || !isAncestor(cl, ccl))) {
            sun.reflect.misc.ReflectUtil.checkPackageAccess(tclass);
        }
    } catch (PrivilegedActionException pae) {
        throw new RuntimeException(pae.getException());
    } catch (Exception ex) {
        throw new RuntimeException(ex);
    }

    if (field.getType() != int.class)
        throw new IllegalArgumentException("Must be integer type");

    if (!Modifier.isVolatile(modifiers))
        throw new IllegalArgumentException("Must be volatile type");
        
    this.cclass = (Modifier.isProtected(modifiers) &&
                   tclass.isAssignableFrom(caller) &&
                   !isSamePackage(tclass, caller))
                  ? caller : tclass;
    this.tclass = tclass;
    this.offset = U.objectFieldOffset(field);
}

public final boolean compareAndSet(T obj, int expect, int update) {
    accessCheck(obj);
    return U.compareAndSwapInt(obj, offset, expect, update);
}
        
private final void accessCheck(T obj) {
    if (!cclass.isInstance(obj))
        throwAccessCheckException(obj);
}  

由此可見,CAS 修改對象屬性也是通過計算偏移地址的方式來實現,這裏我簡單敘述一下整個構造方法的大致流程:

  1. 獲取類對象所輸入的屬性值
  2. 獲取修飾符,判斷屬性訪問權限是否包含
  3. 判斷屬性類型是否 Integer
  4. 判斷屬性是否 volatile 修飾
  5. 計算該屬性在對象內存的偏移量

有了偏移量,後面的操作就很大衆了,這裏我不做贅述。


6、Unsafe 類方法總結

看到這裏,我想大家關於 CAS 實現的原理已經有了大概的認識。除了在 CAS 中使用,Unsafe 類能做的還有很多。這裏我列舉出以下常用的 Unsafe 方法:

// get put 屬性類(其他基礎類型省略):

// 根據對象和偏移量,從內存直接讀數據
public native int getInt(Object var1, long var2);
// 根據對象和偏移量,將新數據寫入內存
public native void putInt(Object var1, long var2, int var4);
// 根據對象和偏移量,從內存讀取對象
public native Object getObject(Object var1, long var2);
// 根據對象和偏移量,從新對象寫入內存
public native void putObject(Object var1, long var2, Object var4);
// 根據偏移量直接獲取值
public native int getInt(long var1);
// 根據偏移量直接寫值
public native void putInt(long var1, int var3);
...

// 內存操作類

// 獲取指定地址內存值
public native long getAddress(long address);
// 設置給定地址的內存值
public native void putAddress(long address, long x);
// 分配指定大小的內存
public native long allocateMemory(long bytes);
// 指定地址分配內存
public native long reallocateMemory(long address, long bytes);
// 釋放參數地址申請的內存
public native void freeMemory(long address);
// 將指定對象的給定offset偏移量內存塊中的所有字節設置爲固定值
public native void setMemory(Object o, long offset, long bytes, byte value);

// 內存地址類

// 獲取字段在對象上的偏移量
public native long objectFieldOffset(Field f);
// 獲取靜態字段在對象上的偏移量
public native long staticFieldOffset(Field f);
// 獲取數組首個元素地址
public native int arrayBaseOffset(Class arrayClass);
// 獲取數組每個元素的大小
public native int arrayIndexScale(Class arrayClass);

// CAS 相關

// 通過 CAS 設置對象
public final native boolean compareAndSwapObject(Object o, long offset,Object expected, Object x);                                                                                                  
// 通過 CAS 設置Integer
public final native boolean compareAndSwapInt(Object o, long offset,int expected,int x);
// 通過 CAS 設置 Long
public final native boolean compareAndSwapLong(Object o, long offset,long expected,long x);

這裏我暫時先列舉出這麼多,以後有用到其他方法時再加。


7、Unsafe 類示例

前文我們提到,可以通過 反射 的手段創建 Unsafe 對象,關於反射的知識我們後面再做整理。最後我們看一組具體示例:

public class UnsafeDemo {

    private int id;
    private String name;

    @Override
    public String toString() {
        return "UnsafeDemo{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }

    @Test
    public void test() throws Exception {
        Field field = Unsafe.class.getDeclaredField("theUnsafe");
        field.setAccessible(true);
        Unsafe unsafe = (Unsafe) field.get(null);
        System.out.println("通過反射創建的 Unsafe 類:" + unsafe);

        UnsafeDemo unsafeDemo = (UnsafeDemo) unsafe.allocateInstance(UnsafeDemo.class);
        Class unsafeDemoClass = unsafeDemo.getClass();

        Field id = unsafeDemoClass.getDeclaredField("id");
        Field name = unsafeDemoClass.getDeclaredField("name");

        unsafe.putInt(unsafeDemo, unsafe.objectFieldOffset(id), 1);
        unsafe.putObject(unsafeDemo, unsafe.objectFieldOffset(name), "李明");
        
        System.out.println(unsafeDemo.toString());
    }
}

執行結果

通過反射創建的 Unsafe 類:sun.misc.Unsafe@64a294a6
UnsafeDemo{id=1, name='李明'}

從輸出結果可以看出,unsafe 類確實可以通過反射的方式創建,並且直接操作內存修改屬性。


參考
https://blog.csdn.net/aguda_king/article/details/72355807
https://blog.csdn.net/javazejian/article/details/72772470
https://www.jianshu.com/p/2c1be41f6e59
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章