java 高性能并发之二之 CAS
CAS:比较并交换(Compare-and-Swap)
1. CAS操作。
CAS虽然是看上去是两次操作,但其实际上是通过硬件来保证其只使用一条处理器指令就完成操作,所有CAS是一个原子操作。
CAS是一种乐观的并发策略,采用失败重试的方式。
CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。(可以将内存值V理解为该变量在主内存储存的值,旧的预期值A为该变量在线程工作内存储存的值)
2. jdk中的CAS
java.util.concurrent.atomic 下的整数原子类,其中的compareAndSet() ,incrementAndGet() 和 getAndIncrease() 等方法都使用了CAS操作。
这里贴一段increaseAndGet()方法的源码
public final int incrementAndGet(){
for( ; ; ){
int current = get();
int next = current + 1 ;
if(compareAndSet(current ,next))
return next ;
}
}
incrementAndGet() 方法使用一个无限循环,不断尝试将一个比当前值大于1的新值赋值给自己。如果失败了,那说明在执行“获取-设置”操作的时候值已经有了修改,于是再次循环进行下一次操作,直到设置成功为止。
3.CAS的ABA问题
尽管CAS已经很完美,但仍然有漏洞,比如ABA问题。
CAS算法实现一个重要前提需要取出内存中某时刻的数据,而在下时刻比较并替换,那么在这个时间差类会导致数据的变化。
ABA问题是指,当在一个线程K进行CAS操作读取到当前值A的时候,另一个线程V进入,首先将A值变为B值,然后又将B值变为A值。然后K线程再次读取时获取的仍然是值A。此时虽然线程K的CAS操作成功了,但不代表这个过程没有问题。如果链表的头在变化了两次后恢复了原值,但是不代表链表就没有变化。
我们假设有这样一个链栈
A |
B |
现在我们要对将A替换成B,首先当前线程T1获取到A值,
此时,T2进入,将A,B出栈,在push,D,C,A。(注意,此时未将B入栈,B处于未知状态)
A |
C |
D |
B |
然后,T1线程进行CAS操作,发现栈顶依然为A,然后将A替换为B,此时栈顶变为B,但此时B.next并不为D,可能为空,也可能为任意的值,但显然不是我们要的结果,相当于我们把D,C“丢了”。
4. 解决
以上就是由于ABA问题带来的隐患,各种乐观锁的实现中通常都会用版本戳version来对记录或对象标记,避免并发操作带来的问题。java中,使用AtomicStampedReference
和AtomicMarkableReference
来实现这个操作。jdk API中对其的描述如下:
其方法
compareAndSet() :
AtomicStampedReference 本质是有一个int 值作为版本号
。在进行CAS操作时,除了判断内存值和预期值外,还要比较预期标志跟当前标志是否相等,如果这两项比较都相等,则CAS操作成功,当前标志更新(一般是加1);如果这两项有任何一样不一致,则CAS操作失败。
AtomicMarkableReference则是将一个boolean值作是否有更改的标记,本质就是它的版本号只有两个,true和false。修改的时候在这两个版本号之间来回切换。