java CAS原理分析
基本过程分析
在并发中,锁是最简单的方式,但是代价也是很高昂的。无锁算法一直是技术人员的最大追求,java中cas(compareAndSet)是著名的无锁算法。
以AtomicInteger
为例,使用其方法有两个参数,一个是预期的旧值expect,另一个是要更新的值update,当且仅当预期的旧值expect和当前真实的值一致时,将内存值修改为update,否则什么都不做返回false。
进入到AtomicInteger 类中,有private volatile int value
; 这个能够保证数据在多线程中可见,在前一篇文章中已经讲到过了。打开看compareAndSet方法,
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
会发现调用unsafe.compareAndSwapInt(this, valueOffset, expect, update);
这个方法,该方法声明为public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
是一个native的方法。到这里基本明白了,这是借助了JNI调用CPU的cas指令,只有一步原子操作,因此非常快。
再看看getAndSet方法:
public final int getAndSet(int newValue) {
for (;;) {
int current = get();
if (compareAndSet(current, newValue))
return current;
}
}
主要还是依靠CAS操作,不过加入了重试机制,直到成功为止。
可能存在的问题
CAS带来两个常见的问题:
1.ABA的问题,如线程1检查操作值A,需要和预期值相等的情况设置为Y,这时候线程2也取出操作值A,然后赋值为B,接着又把B改回成A,这时候线程1进行CAS操作的时候发现得到的值还是A,然后接着进行操作,这其实有一些问题,在线程1取值和操作之间发生了翻天覆地的变化,虽然最终结果一致,但是不代表没有问题。解决这个问题通用的解决方法是弄一个版本号(version),如1A-2B-3A这样来区分,想起来以前在实验室实现的一个带版本号的算法了。
2.CAS操作中有很多重试的机制,如getAndSet(newValue);不成功的时候会返回重试,直到成功为止,如果并发竞争较多,经常导致重试,其实开销会很大。因为其中有volatile修饰的变量,根据之前讲的volatile的底层原理,编译后会有lock前缀的内存屏障指令,会锁住缓存区域,之后会使所有的其他CPU的对应缓存失效,这个代价是比较高的。