概述
在上篇博客中,我简单介绍了无锁同步 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 类确实可以通过反射的方式创建,并且直接操作内存修改属性。