CAS(Compare and Swap),即比较并替换,实现并发算法时经常使用到的一种技术,Doug lea大神在java同步器中大量使用了CAS技术,鬼斧神工的实现了多线程执行的安全性。php
CAS的思想很简单:三个参数,一个当前内存值V、旧的预期值A、即将更新的值B,当且仅当预期值A和内存值V相同时,将内存值修改成B并返回true,不然什么都不作,并返回false。java
一个n++
的问题。算法
public class Case { public volatile int n; public void add() { n++; } }
经过javap -verbose Case
看看add方法的字节码指令缓存
public void add(); flags: ACC_PUBLIC Code: stack=3, locals=1, args_size=1 0: aload_0 1: dup 2: getfield #2 // Field n:I 5: iconst_1 6: iadd 7: putfield #2 // Field n:I 10: return
n++
被拆分红了几个指令:安全
getfield
拿到原始n;iadd
进行加1操做;putfield
写把累加后的值写回n;经过volatile修饰的变量能够保证线程之间的可见性,但并不能保证这3个指令的原子执行,在多线程并发执行下,没法作到线程安全,获得正确的结果,那么应该如何解决呢?多线程
在add
方法加上synchronized修饰解决。并发
public class Case { public volatile int n; public synchronized void add() { n++; } }
这个方案固然可行,可是性能上差了点,还有其它方案么?app
再来看一段代码函数
public int a = 1; public boolean compareAndSwapInt(int b) { if (a == 1) { a = b; return true; } return false; }
若是这段代码在并发下执行,会发生什么?oop
假设线程1和线程2都过了a==1
的检测,都准备执行对a进行赋值,结果就是两个线程同时修改了变量a,显然这种结果是没法符合预期的,没法肯定a的最终值。
解决方法也一样暴力,在compareAndSwapInt方法加锁同步,变成一个原子操做,同一时刻只有一个线程才能修改变量a。
除了低性能的加锁方案,咱们还可使用JDK自带的CAS方案,在CAS中,比较和替换是一组原子操做,不会被外部打断,且在性能上更占有优点。
下面以AtomicInteger
的实现为例,分析一下CAS是如何实现的。
public class AtomicInteger extends Number implements java.io.Serializable { // setup to use Unsafe.compareAndSwapInt for updates 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 int get() {return value;} }
看看AtomicInteger
如何实现并发下的累加操做:
public final int getAndAdd(int delta) { return unsafe.getAndAddInt(this, valueOffset, delta); } //unsafe.getAndAddInt 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; }
假设线程A和线程B同时执行getAndAdd操做(分别跑在不一样CPU上):
getIntVolatile(var1, var2)
拿到value值3,这时线程A被挂起。getIntVolatile(var1, var2)
方法获取到value值3,运气好,线程B没有被挂起,并执行compareAndSwapInt
方法比较内存值也为3,成功修改内存值为2。compareAndSwapInt
方法比较,发现本身手里的值(3)和内存的值(2)不一致,说明该值已经被其它线程提早修改过了,那只能从新来一遍了。compareAndSwapInt
进行比较替换,直到成功。整个过程当中,利用CAS保证了对于value的修改的并发安全,继续深刻看看Unsafe类中的compareAndSwapInt方法实现。
public final native boolean compareAndSwapInt(Object paramObject, long paramLong, int paramInt1, int paramInt2);
Unsafe类中的compareAndSwapInt,是一个本地方法,该方法的实现位于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
Atomic::cmpxchg
实现比较替换,其中参数x是即将更新的值,参数e是原内存的值。若是是Linux的x86,Atomic::cmpxchg
方法的实现以下:
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; }
看到这汇编,心里崩溃 😖
__asm__
表示汇编的开始
volatile
表示禁止编译器优化
LOCK_IF_MP
是个内联函数
#define LOCK_IF_MP(mp) "cmp $0, " #mp "; je 1f; lock; 1: "
Window的x86实现以下:
inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) { int mp = os::isMP(); //判断是不是多处理器 _asm { mov edx, dest mov ecx, exchange_value mov eax, compare_value LOCK_IF_MP(mp) cmpxchg dword ptr [edx], ecx } } // Adding a lock prefix to an instruction on MP machine // VC++ doesn't like the lock prefix to be on a single line // so we can't insert a label after the lock prefix. // By emitting a lock prefix, we can define a label after it. #define LOCK_IF_MP(mp) __asm cmp mp, 0 \ __asm je L0 \ __asm _emit 0xF0 \ __asm L0:
LOCK_IF_MP
根据当前系统是否为多核处理器决定是否为cmpxchg指令添加lock前缀。
intel手册对lock前缀的说明以下:
上面的第2点和第3点所具备的内存屏障效果,保证了CAS同时具备volatile读和volatile写的内存语义。
CAS存在一个很明显的问题,即ABA问题。
问题:若是变量V初次读取的时候是A,而且在准备赋值的时候检查到它仍然是A,那能说明它的值没有被其余线程修改过了吗?
若是在这段期间曾经被改为B,而后又改回A,那CAS操做就会误认为它历来没有被修改过。针对这种状况,java并发包中提供了一个带有标记的原子引用类AtomicStampedReference
,它能够经过控制变量值的版原本保证CAS的正确性。
做者:占小狼连接:https://www.jianshu.com/p/fb6e91b013cc來源:简书著做权归做者全部。商业转载请联系做者得到受权,非商业转载请注明出处。