CAS

概述

compareAndSet,也有(Compare And Swap的说法),它必须时原子操作

是由硬件指令集支持的原子操作,原子行是由CPU保证的,JVM只是封装了汇编调用,这个操作需要输入两个数值,一个旧值(期望操作执行的值)和一个性质,在操作期间先比较旧值有没有改变,如果没有发生变化,才交换成新值

JDK正式利用这些CAS指令,可以实现并发的数据结构,比如AtomicInteger等原子类

volatile:

  • 获取共享变量时,为了保证该变量的可见性,需要volatile修饰
  • 它可以用来修饰成员变量和静态变量,它可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作volatile变量都是直接操作主存的。即一个线程对volatile变量的修改,对另一个线程可见
  • **注意:**volatile仅仅保证了共享变量的可见性,让其他线程能够看到最新值,但不能解决指令交错问题(不能保证原子性)
  • CAS必须借助volatile才能读取到共享变量的最新值来实现“比较并交换”的效果

为什么无锁效率高

  • 无锁情况下,即使重试失败,线程始终再高速运行,没有停歇,而synchronized会让线程再没有获得锁的时候,发生上下文切换,进入阻塞
  • 打个比喻:线程好像高速跑道上的赛车,高速运行时,速度超快,一旦发生上下文切换,就好像比赛车要减速、熄火,等待被唤醒又重新打火、启动、加速。。。恢复到高速运行,代价比较大
  • 但无所情况下,线程要保持运行,需要额外CPU的支持,CPU在这里好比高速赛道,没有额外的跑道,线程想高速运行也无从谈起,虽然不会进入阻塞,但由于没有分配到时间片,仍然会进入可运行状态,还是会导致上下文切换

CAS特点

结合CAS和volatile可以实现无所并发,适用于线程数少、多核CPU的场景

  • CAS
  • 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没有关系,我吃亏点再重试呗
  • synchronized是基于悲观锁的思想:最悲观的估计,得防着其他线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会
  • CAS体现的是无锁并发、无阻塞并发
    • 因为没有使用synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一
    • 但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响

乐观锁问题

ABA问题

决定CAS是否进行swap的判断标准是“当前值是否和预期值相等”,如果一致,大多数情况下可以认为在此期间这个数值没有发生过变动。但是在此期间发生变动也是有可能的,比如A->B->A,这个情况下是无法捕获到的CAS只能检查出现在的值和最初的值是不是一样

问题解决方案:添加版本号

在atomic包中提供了AtomicStampedReference这个类,他是专门用来解决ABA问题的,解决思路正是利用版本号,AtomicStampedReference会维护一个类似<Object,int>的数据结构,其中int就是用来计数的,也就是版本号,它可以对这个对象和int版本号进行原子更新,从而解决了ABA问题

较大的循环开销

CAS的第二个缺点就是自旋时间过长

由于单次CAS不一定执行成功,所以CAS往往配合着循环来实现,有时候甚至是死循环

如果我们的应用场景就是高并发的场景,就有可能导致CAS一直操作不成功,这样的话,CPU资源也是一直在被消耗的,这会对性能产生很大的影响。所以这就要求我们,要根据实际情况来选择是否使用CAS,在高并发的场景下,通常CAS效率是不高的

只能是单个变量的原子操作

对于CAS,我们不能针对多个共享变量同时进行CAS操作,因为这多个变量之间是相互独立的,简单的把原子操作组合到一起,并不具备原子性

我们想对多个对象进行CAS操作并想保证线程安全的话,是比较困难的。但是,我们可以使用AtomicReference来封装实现

1
2
3
4
5
6
7
8
9
10
11
12
public class State{
Integer state;
Long count;
Double value;
//需要重写equals方法
public static void main(){
State oldState=new State(1,10L,10.0);
State newState=new State(2,20L,20.0);
AtomicReference<State> atomicReference=new AtomicReference<>();
atomicReference.compareAndSet(oldState,newState);
}
}