概述
在上篇博客中,我簡單介紹了無鎖同步 CAS 如何使用以及部分它的特性。本篇我打算整理一下 CAS 的實現原理即 Unsafe 類相關的知識。
Unsafe
本篇博客分以下幾個模塊展開:
- Unsafe 類簡單介紹
- CAS 更新基礎類型原理
- CAS 更新對象引用原理
- CAS 更新數組類型原理
- CAS 更新對象屬性原理
- Unsafe 類方法總結
- 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 的方式大同小異,都是通過對象和偏移量,確定要操作的內存地址,直接從內存層面比較期望值後判斷是否執行更新操作。
這裏我們主要分析一下如何獲取數組元素的內存地址:
- 首先通過 Unsafe 類的 arrayBaseOffset() 方法確定數組首個元素地址
- 其次通過 Unsafe 類的 arrayIndexScale() 方法確定數組中每個元素所佔的空間大小
- 判斷數組中元素對象所佔的大小是否 2 的冪次方(硬規定)
- 通過 31 - Integer.numberOfLeadingZeros(scale) 計算出每個元素轉二進制後,需要移動的零的數量
- 通過 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 修改對象屬性也是通過計算偏移地址的方式來實現,這裏我簡單敘述一下整個構造方法的大致流程:
- 獲取類對象所輸入的屬性值
- 獲取修飾符,判斷屬性訪問權限是否包含
- 判斷屬性類型是否 Integer
- 判斷屬性是否 volatile 修飾
- 計算該屬性在對象內存的偏移量
有了偏移量,後面的操作就很大衆了,這裏我不做贅述。
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 類確實可以通過反射的方式創建,並且直接操作內存修改屬性。