CAS和ABA问题
引言:乐观锁和悲观锁的概念
悲观锁:悲观锁悲观地认为,自己执行操作的过程中一定有人修改过自己操作的值,所以在自己操作之前会加上一把锁,synchronized就是一个悲观锁。
乐观锁:乐观锁则乐观地认为,当自己执行操作时不会有人修改自己操作的值,所以采用不加锁的机制,只是在操作完成的那一刻发现产生冲突,则会重新执行操作,直到成功为止。CAS算法就是乐观锁的一种实现。
一、CAS算法
CAS(CompareAndSet):顾名思义了,十分满足乐观锁的理念,比较后,再set。
CAS算法通常有三个操作数,一个是内存中的V,一个是自己预计的A(表示操作前内存中的值),和自己要设置的值B,在设置B时,比较A和V,如果两者相等,则直接设置,如果不等,则重试操作。
二、AtomicInteger源码实现机制
第一部分,它使用volatile关键字来修饰value,此操作保证了value在各线程之间可见:
private volatile int value;
第二部分,获取操作,由于value在各线程之间可见,所以无需加锁,直接就可以返回一个值:
public final int get() {
return value;
}
第三部分,实现i++操作:
public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}
这里就是一个CAS实现了,通过一个死循环,每一次循环,都会去获取当前内存中的value,再对其进行i++操作,最后使用compareAndSet方法比较并设置value的值,如果成功会返回一个成功的值,退出循环,如果失败则继续重试。
看看compareAndSet方法,是JNI(Java本地方法接口)的一个实现:
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
相比起加锁操作,他显然更加高效。
三、ABA问题
看上去无懈可击,且十分高效,满足并发的CAS,同样也存在有问题,ABA问题。
设想一个场景,当线程1对值为A的value正在进行操作,但是,线程2这时进来,将value改为B,并在线程1结束之前,又将value改为了A,看上去一切都没变,这时,线程1完成了CAS操作。但是,如果value是一个链表呢?虽然其链表头没有变,但是不能保证,他的链表内容没有发生变化。
通常,为了避免ABA问题的隐患,各种CAS实现会使用版本戳。AtomicStampedReference便提供了版本戳的支持,看以下的代码,使用AtomicStampedReference进行CAS,发生ABA问题,CAS会返回false,但是使用AtomicInteger进行CAS,发生ABA,CAS还是会成功:
package concur.lock;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicStampedReference;
public class ABA {
private static AtomicInteger atomicInt = new AtomicInteger(100);
private static AtomicStampedReference<Integer> atomicStampedRef =
new AtomicStampedReference<Integer>(100, 0);
public static void main(String[] args) throws InterruptedException {
Thread intT1 = new Thread(new Runnable() {
@Override
public void run() {
atomicInt.compareAndSet(100, 101);
atomicInt.compareAndSet(101, 100);
}
});
Thread intT2 = new Thread(new Runnable() {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean c3 = atomicInt.compareAndSet(100, 101);
System.out.println(c3); //true
}
});
intT1.start();
intT2.start();
intT1.join();
intT2.join();
Thread refT1 = new Thread(new Runnable() {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicStampedRef.compareAndSet(100, 101,
atomicStampedRef.getStamp(), atomicStampedRef.getStamp()+1);
atomicStampedRef.compareAndSet(101, 100,
atomicStampedRef.getStamp(), atomicStampedRef.getStamp()+1);
}
});
Thread refT2 = new Thread(new Runnable() {
@Override
public void run() {
int stamp = atomicStampedRef.getStamp();
System.out.println("before sleep : stamp = " + stamp); // stamp = 0
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("after sleep : stamp = " + atomicStampedRef.getStamp());//stamp = 1
boolean c3 = atomicStampedRef.compareAndSet(100, 101, stamp, stamp+1);
System.out.println(c3); //false
}
});
refT1.start();
refT2.start();
}
}