CAS基本知识
CAS全称叫Compare-And-Swap,翻译过来是比较并替换,用来将某个内存地址X中的值替换为新的数据,基本思想是,判断地址中的值是否和预期值一样,如果一样,则进行替换,否则什么也不做,CAS技术在并发程序中经常用到,Java并发包中的很多类都是基于CAS实现的,比如原子类AtomicInteger、AtomicLong等还有AbstractQueuedSynchronizer,CAS具体实现是在Unsafe类中,Unsafe类中的方法都是native方法,由JVM本地实现,但是,我们却不能直接使用Unsafe,原因一是Unsafe构造方法是private的,只能通过静态方法getUnsafe获得,但是在getUnsafe进行了判断,如果当前类的类加载器不是Boot ClassLoader,则会报错,那就没办法使用了吗?并不是,可以通过万能的反射。
另外通过类名Unsafe就能看出来,是不安全的意思,因此Java官方并不建议使用。
private Unsafe() {
}
@CallerSensitive
public static Unsafe getUnsafe() {
Class var0 = Reflection.getCallerClass();
if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
throw new SecurityException("Unsafe");
} else {
return theUnsafe;
}
}
反射获取Unsafe。
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe)field.get(null);
System.out.println(unsafe);
} catch (Exception e) {
e.printStackTrace();
}
汇编实现
基本用法就不介绍了,这里主要介绍他底层靠什么指令实现的,拿这个方法来说,compareAndSwapInt会读取传入对象o在内存中偏移量为offset位置的值与期望值expected作比较,相等就把x值赋值给offset位置的值,然后方法返回true,如果不相等,什么也不做,返回false。
public final native boolean compareAndSwapInt(Object o, long offset,
int expected,
int x);
由于这是个native方法,需要查看对应c++实现,经过查阅资料,在/jdk9u/hotspot/src/share/vm/unsafe.cpp
中有以下代码:
static JNINativeMethod jdk_internal_misc_Unsafe_methods[] = {
......
{CC "compareAndSetObject",CC "(" OBJ "J" OBJ "" OBJ ")Z", FN_PTR(Unsafe_CompareAndSetObject)},
{CC "compareAndSetInt", CC "(" OBJ "J""I""I"")Z", FN_PTR(Unsafe_CompareAndSetInt)},
{CC "compareAndSetLong", CC "(" OBJ "J""J""J"")Z", FN_PTR(Unsafe_CompareAndSetLong)},
{CC "compareAndExchangeObject", CC "(" OBJ "J" OBJ "" OBJ ")" OBJ, FN_PTR(Unsafe_CompareAndExchangeObject)},
{CC "compareAndExchangeInt", CC "(" OBJ "J""I""I"")I", FN_PTR(Unsafe_CompareAndExchangeInt)},
{CC "compareAndExchangeLong", CC "(" OBJ "J""J""J"")J", FN_PTR(Unsafe_CompareAndExchangeLong)},
......
};
这个是保存方法对应关系的静态数组,由此得知,对应的是compareAndSetInt是Unsafe_CompareAndSetInt,所以直接在unsafe中搜索Unsafe_CompareAndExchangeInt即可。
UNSAFE_ENTRY(jint, Unsafe_CompareAndExchangeInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x)) {
oop p = JNIHandles::resolve(obj);
jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
return (jint)(Atomic::cmpxchg(x, addr, e));
} UNSAFE_END
这个方法大概流程如下:
- 通过 JNIHandles::resolve() 获取obj在内存中OOP实例p;
- 根据value的内存偏移值offset去内存中取指针addr;
- 获得更新值x、指针addr、期待值e参数后调用Atomic::cmpxchg(x, addr, e);
Atomic::cmpxchg
最终实现在hotspot/src/os_cpu/linux_x86/vm/atomic_linux_x86
的atomic_linux_x86.inline.hpp中,针对不同的操作系统,JVM 对于 Atomic::cmpxchg有不同的实现,在这里查看linux_x86的实现。
inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
int mp = os::is_MP();
__asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
: "=a" (exchange_value)
: "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
: "cc", "memory");
return exchange_value;
}
下面是这段代码解释:
os::is_MP
判断是否为多处理器,如果是多处理器则返回 true。
__asm__
内嵌汇编代码。
volatile
告诉编译器对访问该变量的代码就不再进行优化,和Java中的volatile不同。
LOCK_IF_MP
#define LOCK_IF_MP(mp) "cmp $0, " #mp "; je 1f; lock; 1: "
根据当前系统是否为多核处理器决定是否为cmpxchgl指令添加lock前缀。
cmpxchgl
汇编指令,用于实现交换,单核处理器使用 cmpxchgl 命令实现 CAS 操作,多核处理器使用带 lock 前缀的cmpxchgl 命令实现 CAS 操作。
所以这里还需要重点了解下lock前缀,intel的手册对lock前缀的说明如下:
- 确保对内存的读-改-写操作原子执行。在Pentium及Pentium之前的处理器中,带有lock前缀的指令在执行期间会锁住总线,使得其他处理器暂时无法通过总线访问内存。很显然,这会带来昂贵的开销。从Pentium 4,Intel Xeon及P6处理器开始,intel在原有总线锁的基础上做了一个很有意义的优化:如果要访问的内存区域(area of memory)在lock前缀指令执行期间已经在处理器内部的缓存中被锁定(即包含该内存区域的缓存行当前处于独占或以修改状态),并且该内存区域被完全包含在单个缓存行(cache line)中,那么处理器将直接执行该指令。由于在指令执行期间该缓存行会一直被锁定,其它处理器无法读/写该指令要访问的内存区域,因此能保证指令执行的原子性。这个操作过程叫做缓存锁定(cache locking),缓存锁定将大大降低lock前缀指令的执行开销,但是当多处理器之间的竞争程度很高或者指令访问的内存地址未对齐时,仍然会锁住总线。
- 禁止该指令与之前和之后的读和写指令重排序。
- 把写缓冲区中的所有数据刷新到内存中