原文链接:什么叫做CAS – 编程屋
1 什么叫做CAS?
CAS(compare and swap)比较并交换,在平时开发中其实很多底层都是用cas来实现的,像原子类的底层原理就是cas,乐观锁的底层原理也是cas。原子类的用法可见下面这篇博客
cas的特点:当多个线程同时使用cas去更新一个变量的时候,只有其中一个线程能够操作成功,其他的线程都能够操作失败,但是更新失败的线程不会阻塞,但失败的线程会自旋尝试更新(默认都有尝试更新的次数)。
CAS中的核心操作:内存值V、预期值A、要修改的值B。当且仅当预期值A和当前的内存值V相等时才会将内存值修改为B。
2 CAS带来了什么问题?
CAS带来的ABA问题
比如线程1已经将利用CAS修改变量值A,但是在修改之前其他线程已经将A变成了B,然后又变成了A,即A->B->A问题。因为线程1在操作的时候发现值依然为A,所以根据CAS的机制会操作成功,但是其实这个值已经被其他线程修改过了,只是线程1不知道而已,这就导致了ABA问题。
demo实例:
private static AtomicReference<Integer> atomicInteger = new AtomicReference(100);
public static void main(String[] args) throws InterruptedException {
System.out.println("=================以下是ABA问题产生==========================");
new Thread(() -> {
atomicInteger.compareAndSet(100, 101);
atomicInteger.compareAndSet(101, 100);
},"t1").start();
new Thread(() -> {
try {
//暂停1s中t2线程,保证了上面的t1线程完成了一次ABA操作(当main
// 线程启动之后,t1线程和t2线程不知道谁先执行,所以让t2线程先睡1秒,这样即使t2线程先执行的情况下。睡1s时,也会被t1线程抢到完成一次ABA的操作)
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean result = atomicInteger.compareAndSet(100, 2022);
System.out.println("是否修改成功:" + result +"\t"+"修改之后的值为"+atomicInteger.get());
},"t2").start();
}
由demo可以看到两个线程,第一个线程将100改为101后,又将101改为了100,线程2依然可以将100改为其他的值(2022)。所以说CAS的确带来了ABA问题。
3 如何解决ABA问题?
在java中,AtomicStampedReference通过包装[E,Integer]的元组来对象对象标记版本戳stamp,从而避免ABA问题。
demo示例:
private static AtomicReference<Integer> atomicInteger = new AtomicReference(100);
private static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<Integer>(100,1);
public static void main(String[] args) throws InterruptedException {
System.out.println("=================以下是ABA问题产生==========================");
new Thread(() -> {
atomicInteger.compareAndSet(100, 101);
atomicInteger.compareAndSet(101, 100);
},"t1").start();
new Thread(() -> {
try {
//暂停1s中t2线程,保证了上面的t1线程完成了一次ABA操作(当main
// 线程启动之后,t1线程和t2线程不知道谁先执行,所以让t2线程先睡1秒,这样即使t2线程先执行的情况下。睡1s时,也会被t1线程抢到完成一次ABA的操作)
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean result = atomicInteger.compareAndSet(100, 101);
System.out.println("是否修改成功:" + result +"\t"+"修改之后的值为"+atomicInteger.get());
},"t2").start();
try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) {}
System.out.println("=================以下是ABA问题的解决==========================");
new Thread(() -> {
//拿到此时版本号
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName()+"\t第1次版本号为"+stamp);
//暂停1s钟t3线程,让cpu去调度t4线程拿到同一个初始版本号
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) {}
//将100设置成101并增加版本号
atomicStampedReference.compareAndSet(100, 101, atomicStampedReference.getStamp(),
atomicStampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName()+"\t第2次版本号为"+atomicStampedReference.getStamp());
//将101设置成100并增加版本号
atomicStampedReference.compareAndSet(101, 100, atomicStampedReference.getStamp(),
atomicStampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName()+"\t第3次版本号为"+atomicStampedReference.getStamp());
},"t3线程").start();
new Thread(() -> {
//拿到此时版本号
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName()+"\t第1次版本号为"+stamp);
//暂停t4线程3秒钟,保证t3线程完成一次ABA操作
try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { }
//将100设置成101并增加版本号
boolean refResult = atomicStampedReference.compareAndSet(100, 2021, stamp, stamp + 1);
System.out.println("是否修改成功:" + refResult +"\t"+"实际最新值为"+atomicStampedReference.getReference()+"\t"+"当前实际版本号为:"+atomicStampedReference.getStamp());
},"t4线程").start();
控制台输出:
可以看出,加入了版本号控制,解决了ABA的问题
4 CAS有什么缺点?
1)ABA问题
2)循环时间长,开销较大
以上只是部分内容,为了维护方便,本文已迁移到新地址:什么叫做CAS – 编程屋