1.CAS 介绍 什么是CAS?
CAS,全称:Compare and Swap,即比较-替换;CAS是一种无锁算法, 通过无锁的方式实现了多个线程间变量的同步;CAS 是乐观锁的一种实现方式,是一种轻量级锁,JUC 中很多工具类的实现就是基于 CAS 的。
注意:JVM中的CAS操作正是利用了处理器提供的CMPXCHG指令实现 的。
具体内容:
假设有三个操作数:内存值V、旧的预期值A、要修改的值B,当且仅当预期值A和内存值V相同时,才会将内存值修改为B并返回true,否则什么都不做并返回false。当然CAS一定要volatile变量配合 ,这样才能保证每次拿到的变量是主内存中最新的那个值,否则旧的预期值A对某条线程来说,永远是一个不会变的值A,只要某次CAS操作失败,永远都不可能成功。
意思代码如下:
1 2 3 4 5 6 if (this .value == A){ this .value = B; return true ; } else { return false ; }
这是一种乐观策略,认为并发操作并不总会发生。
还是不明白?那我再说明下,就比如我现在要修改数据库的一条数据,修改之前我先拿到他原来的值,然后在SQL里面还会加个判断,原来的值和我手上拿到的他的原来的值是否一样,一样我们就可以去修改了,不一样就证明被别的线程修改了你就return错误就好了。
其在SQL 中的伪代码如下:
1 update a set value = newValue where value = #{oldValue} // oldValue 就是我们执行前查询出来的值
底层实现:
留空,后面补上!
2.CAS 存在的问题 CAS虽然高效地解决了原子操作,但是还是存在一些缺陷的,主要表现在三个方法:循环时间太长、只能保证一个共享变量原子操作、ABA问题。
循环时间太长 :如果CAS一直不成功呢?这种情况绝对有可能发生,如果自旋CAS长时间地不成功,则会给CPU带来非常大的开销。在JUC中有些地方就限制了CAS自旋的次数,例如BlockingQueue的SynchronousQueue。
只能保证一个共享变量原子操作 :看了CAS的实现就知道这只能针对一个共享变量,如果是多个共享变量就只能使用锁了,当然如果你有办法把多个变量整成一个变量,利用CAS也不错。例如读写锁中state的高地位
ABA问题 :就是说来了一个线程把值改回了B,又来了一个线程把值又改回了A,对于这个时候判断的线程,就发现他的值还是A,所以他就不知道这个值到底有没有被人改过,其实很多场景如果只追求最后结果正确,这是没关系的。但是实际过程中还是需要记录修改过程的,比如资金修改什么的,你每次修改的都应该有记录,方便回溯。
主要问题:怎么解决ABA问题?
用版本号去保证就好了,就比如说,我在修改前去查询他原来的值的时候再带一个版本号,每次判断就连值和版本号一起判断,判断成功就给版本号加1。判断原来的值和版本号是否匹配,中间有别的线程修改,值可能相等,但是版本号100%不一样。
Java提供了AtomicStampedReference来解决 。AtomicStampedReference通过包装[E,Integer]
的元组来对对象标记版本戳stamp,从而避免ABA问题。
下面详细讲讲AtomicStampedReference 中的compareAndSet方法:
compareAndSet有四个参数,分别表示:预期引用、更新后的引用、预期标志、更新后的标志。源码部门很好理解:预期的引用 == 当前引用,预期的标识 == 当前标识,如果更新后的引用和标志和当前的引用和标志相等则直接返回true,否则通过Pair生成一个新的pair对象与当前pair CAS替换。Pair为AtomicStampedReference的内部类,主要用于记录引用和版本戳信息(标识),定义如下:
Pair记录着对象的引用和版本戳,版本戳为int型,保持自增。同时Pair是一个不可变对象,其所有属性全部定义为final,对外提供一个of方法,该方法返回一个新建的Pari对象。pair对象定义为volatile,保证多线程环境下的可见性。在AtomicStampedReference中,大多方法都是通过调用Pair的of方法来产生一个新的Pair对象,然后赋值给变量pair。如set方法:
使用atomicStampedReference进行解决ABA示例代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 public class ABAdemo { static AtomicReference<Integer> atomicReference = new AtomicReference <>(100 ); static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference <>(100 ,1 ); public static void main (String[] args) { new Thread(() -> { atomicReference.compareAndSet(100 , 101 ); atomicReference.compareAndSet(101 , 100 ); }, "t1" ).start(); new Thread(() -> { try { TimeUnit.SECONDS.sleep(1 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(atomicReference.compareAndSet(100 , 2019 ) + "\t" + atomicReference.get()); }, "t2" ).start(); try { TimeUnit.SECONDS.sleep(2 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("=====================以下是 ABA 的解决============================================" ); new Thread(() -> { int stamp = atomicStampedReference.getStamp(); System.out.println(Thread.currentThread().getName()+"\t 第一次版本号:" +atomicStampedReference.getStamp()); try { TimeUnit.SECONDS.sleep(1 ); } catch (InterruptedException e) { e.printStackTrace(); } atomicStampedReference.compareAndSet(100 ,101 ,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1 ); System.out.println(Thread.currentThread().getName()+"\t 第二次版本号:" +atomicStampedReference.getStamp()); atomicStampedReference.compareAndSet(101 ,100 ,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1 ); System.out.println(Thread.currentThread().getName()+"\t 第三次版本号:" +atomicStampedReference.getStamp()); }, "t3" ).start(); new Thread(() -> { int stamp = atomicStampedReference.getStamp(); System.out.println(Thread.currentThread().getName()+"\t 第一次版本号:" +atomicStampedReference.getStamp()); try { TimeUnit.SECONDS.sleep(3 ); } catch (InterruptedException e) { e.printStackTrace(); } boolean b = atomicStampedReference.compareAndSet(100 ,2019 ,stamp,stamp+1 ); System.out.println(Thread.currentThread().getName()+" 修改成功否:" +b); System.out.println(Thread.currentThread().getName()+"以为的版本号 :" +stamp+" 当前最新的版本号 :" +atomicStampedReference.getStamp()); System.out.println("当前实际最新的值 :" +atomicStampedReference.getReference()); }, "t4" ).start(); } }
输出如下
1 2 3 4 5 6 7 8 9 true 2019 =====================以下是 ABA 的解决============================================ t3 第一次版本号:1 t4 第一次版本号:1 t3 第二次版本号:2 t3 第三次版本号:3 t4 修改成功否:false t4以为的版本号 :1 当前最新的版本号 :3 当前实际最新的值 :100