一 概念
在计算机中比较和交换(Compare And Swap 即 CAS)是用于实现多线程同步的原子指令,当多个线程对某个资源进行CAS操作,只能有1个线程成功,其他线程并不会阻塞而是返回失败信号。
二 基本原理
每个CAS包含3个运算符
1 内存地址 V
2 旧的预期值A
3 新值B
基本思路,如果V地址上的值和传入的旧的期望值A相等,就给地址赋个新值B,否则不做任何处理
if(V.value.equals(A)) { V.value = B; return true; }else { return false; }
为了确保CAS操作的成功,会使用死循环内执行CAS,直到其成功为止
常用 原子操作类型
基本类型 AtomicInteger,AtomicLong,AtomicBoolean;
数组类型 AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray;
引用类型 AtomicReference,AtomicMarkableReference,AtomicStampedReference
三 常见用法和ABA问题
3.1 常见用法
原子操作基本类型(Integer)
//integer 原子操作对象 AtomicInteger atomicInteger = new AtomicInteger(10); //基本类型原子操作测试 public void atomicIntegerTest() { //自增1 并返回新值 System.out.println("自增 并返回新值="+atomicInteger.incrementAndGet()); //返回旧值,再加1 System.out.println("返回 ="+ atomicInteger.getAndIncrement()+" 并自增"); }
运行结果
自增 并返回新值=11
返回=11 并自增
用途 比如可以用于多线程的计数等,
如下面incrementAndGet()的源码内部,就是使用了死循环,用CAS去增加值,一直到成功为止
//incrementAndGet() 的部分源码 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; }
我们也可以自己实现自己的 incrementAndGet()方法,暂命名为 myIncrementAndGet()
//自己的 incrementAndGet 方法 自增1并获取到新值 public int myIncrementAndGet(){ //循环更新,直到CAS成功 for (; ; ) { int current = atomicInteger.get(); int newValue = current + 1; if (atomicInteger.compareAndSet(current, newValue)) { return newValue; } } }
原子操作类型数组
//数组 int [] arr = new int[]{1,2,3,4,5,6,7,8}; //数组类型原子操作对象 private AtomicIntegerArray atomicArray = new AtomicIntegerArray(arr); @Test public void test() { //获取索引为0的值,并修改索引0里的值为3 System.out.println(atomicArray.getAndSet(0,3)); System.out.println("原数组索引0的值="+arr[0]); System.out.println("原子类型数组里索引0的值="+atomicArray.get(0)); }
运行结果
1
原数组索引0的值=1
原子类型数组里索引0的值 = 3;
3.2 ABA问题
原来V地址上值为A
1. 时刻1 线程1 执行读取操作,获取原值A,然后线程被切换走了
2. 时刻2 线程2 更新了V上的值为B
3. 时刻3 线程2 更新了V上的值为A
4. 时刻4 线程1恢复运行,将比较值(compareValue)与原值(oldValue)进行比较,发现两个值相等(都是A)。
然后用新值(newValue)B 写入内存中,完成 CAS 操作
我们可以发现,其实V值,在线程1修改之前,已经被修改过2次了(改为B,又改回A)
但线程1并不知道原值被修改,所以继续执行流程。
处理ABA问题
通常是对每一次CAS操作设置版本戳,目前java.util.concurrent.atomic包下提供2种原子类
类名 | AtomicMarkableReference | AtomicStampedReference |
版本戳特点 | 版本戳是 boolean 类型 | 版本戳是number类型 |
数据 | 只关心数据是否被动过 | 关心数据被动过几次 |
看具体示例
public class AtomicStampedRefernceTest { //带版本戳的原子操作 private static AtomicStampedReference<String> atomicStampedReference = new AtomicStampedReference<>("作者", 1); //旧的版本戳 final int oldstamp = atomicStampedReference.getStamp(); //旧值 final String oldValue = atomicStampedReference.getReference(); @Test public void mainTest() { Thread right = new Thread(new rightThread()); //正确版本号线程 //错误版本号线程 Thread error = new Thread(new errorThread()); try { right.start(); right.join(); error.start(); error.join(); }catch (InterruptedException ex) { ex.printStackTrace(); } } //正确使用了版本号进行了CAS的线程 public class rightThread implements Runnable { @Override public void run() { String value = atomicStampedReference.getReference(); int stamp = atomicStampedReference.getStamp(); Thread.currentThread().setName("正确使用了版本号线程"); System.out.println(Thread.currentThread().getName() + ":值=" + value + ",版本戳=" + stamp + " CAS结果=" + atomicStampedReference.compareAndSet(value, value + "-帅哥", stamp, stamp + 1)); } } //错误使用了版本号进行了CAS的线程 public class errorThread implements Runnable { @Override public void run() { String value = atomicStampedReference.getReference(); int stamp = atomicStampedReference.getStamp(); Thread.currentThread().setName("错误使用了版本号线程"); System.out.println(Thread.currentThread().getName() + ":值=" + value + ",版本戳=" + stamp + " CAS结果=" + atomicStampedReference.compareAndSet(value, value + "-老者", oldstamp, oldstamp + 1)); } } }
输出结果
正确使用了版本号线程:值=作者,版本戳=1 CAS结果=true
错误使用了版本号线程:值=作者-帅哥,版本戳=2 CAS结果=false
总结:
CAS和synchronized有什么区别?
synchronized是java的内置锁,目的是用于锁住某块资源,确保这块资源只能被一个线程访问(即确保资源的原子性),是悲观锁。
CAS是计算机指令级别的一个原子操作
使用synchronized 有什么不方便的地方或者弊端?
1 synchronized 是基于阻塞机制,如果某个线程拿不到锁会阻塞在那里,当被阻塞的线程优先级很高时,我们无法中断
2 拿到锁的线程一直不释放怎么办
3 大量竞争锁,会出现死锁或活锁,而且会消耗CPU,以及其他一些安全问题
使用CAS的弊端
为了确保CAS操作成功,你要死循环调用,直到成功为止,造成CPU开销问题
什么情况下用CAS?
当觉得用锁不方便,或者太笨重时,可以考虑用CAS