一、概念
CAS(compare and swap/set)称为比较并交换或者自旋锁,是一种基于冲突检测的乐观锁,也称之非阻塞同步。换句话说就是不管三七二十一我先尝试操作,要是没有其他线程和我冲突那我就操作成功了,否则我就进行不断地重试直到没有冲突产生,期间不会去阻塞其他线程。
这样会产生一个问题那就是怎么保证我检测冲突和操作能够具有连贯的原子性?总不能我冲突检测完成了,但是在我操作的时候其他线程已经把这个值改了。但如果我加锁的话,那不还是阻塞同步(悲观锁)了吗?
因此这个CAS操作只能够通过硬件指令集来完成,在硬件的层面保障冲突检测和操作的原子性。在x86指令集中有一个cmpxchg来实现这个功能。正由于天然的硬件支持,CAS的效率在冲突不是非常剧烈的情况下会比悲观锁高很多,因为少了很多用户态内核态的切换以及线程挂起唤醒的开销。
二、原子类
JDK1.5之后,sun.misc.Unsafe类的出现使得Java类库中才开始使用cas操作(非安全类,应用层代码无法使用)。cas操作一般要三个参数:变量内存地址,期待值,新值。即在进行cas的时候cpu需要比较该内存地址变量的值是否为期待值,此阶段为冲突检测;如果一致那就说明没有冲突,然后将新值赋给该变量,此为交换阶段。
JDK中最具代表性的CAS我觉得就是一系列原子类了。来看下AtomicInteger是如何实现自增操作的:
// jdk:1.8, OS:MacOS
// java.util.concurrent.atomic.AtomicInteger
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
// sun.misc.Unsafe
public final int getAndAddInt(Object o, long valueOffset, int delta) {
int expect;
do {
expect = this.getIntVolatile(o, valueOffset);
} while(!this.compareAndSwapInt(o, valueOffset, expect, expect + delta));
return expect;
}
可以看到是以CAS(atomicInteger的地址,当前值,当前值+1)实现的,若有冲突则不停重读当前值然后加一,直到成功为止。上述Unsafe.compareAndSwapInt方法是一个本地方法,在Hotspot源码中如下
// hotspot/src/share/vm/prims/unsafe.cpp
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
UnsafeWrapper("Unsafe_CompareAndSwapInt");
oop p = JNIHandles::resolve(obj);
jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END
// hotspot/src/os_cpu/linux_x86/vm/atomic_linux_x86.inline.hpp
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;
}
可以看出,在汇编码层面最终是以lock cmpxchgl实现的。
三、ABA问题
CAS操作存在一个逻辑漏洞,那就是读取的时候变量值为A,再进行CAS(V,A,N)的时候这个A值是有可能被修改过的,例如上面AtomicInteger中的自增expect = this.getIntVolatile(o, valueOffset);
假设expect为0,当进行this.compareAndSwapInt(o, valueOffset, expect, expect + delta)
的时候这个expect值有可能已经被+1然后又-1过了,因为期间可能发生了线程切换。ABA->010
针对这个问题JDK提供AtomicStampedReference类来解决,它是通过控制版本形式来保障的。这个类在实例化的时候会要求存入一个引用和版本,然后CAS的时候会要求传入期望版本值和新版本值。不过根据《深入理解java虚拟机》的说法:这个类非常鸡肋,大部分情况下ABA问题不会影响程序并发的正确性,如果要解决ABA问题,使用传统的互斥同步会更高效。
// java.util.concurrent.atomic.AtomicStampedReference
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair<V> current = pair;
return
// 期望引用值需要等于当前引用值
expectedReference == current.reference &&
// 期望版本值需要等于当前版本值
expectedStamp == current.stamp &&
// 设置的是否还是原来的值或者新值是否设置成功
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}
四、参考
深入理解java虚拟机