并发编程之CAS

写在前面

上一篇我们分析了volatile变量对于内存可见性的保证以及抑制指令重排的特性,了解到在多线程对volatile变量的读写不会发生线程阻塞,但是volatile变量只能保证可见性和有序性,并不能保证原子性,即i++之后的数据仍然是不正确的。目前保证原子性的方式只有加锁,无论是内置锁(synchronized),还是显示锁(ReentrantLock),加锁意味着线程阻塞,如果竞争激烈,很可能导致频繁的线程上下文切换,从而大大降低了性能。鉴于此,我们希望有一种机制,既可以保证原子性,又能够使得线程不发生阻塞,幸运的是,java的先行者已经为我们做好了这些,这就是今天的主角——CAS。

CAS分析

CAS,即compare and swap,比较更新机制。它涉及到三个参数,内存值V,旧值A,新值B。当且仅当V=A时,我们才去将变量的值更新成B并返回true,否则返回false。(这个比较再更新的原子性是由操作系统底层来实现的,我们并不需要关心)

JUC下的atomic类都是通过CAS来实现的,我们以AtomicInteger为例来说明。

AtomicInteger i = new AtomicInteger(0);
i.increasementAndGet();
并发编程之CAS_第1张图片
01.png

如图所示,我们开启了三个线程并发对AtomicInteger类型的变量i进行increasementAndGet的自增操作,i的值初始化为0,首先,每个线程都会先去读取i的值,然后进行院子的CAS操作,在CAS操作里,如果此时i的值还是之前读到的值,则更新为新值;如果CAS操作里i的值不少刚刚获取的值,说明有其他线程先于本线程修改了i,CAS失败,失败之后自旋,再次读取i的值,再次CAS操作。

对应于上图,我们假设线程1先于线程2,3执行,线程2,3同时执行,完整的流程就是:

  • 线程1首先拿到变量id值为0,此时就它一个线程,直接进行CAS操作,将0改成1
  • 线程2,线程3同时读取到变量id值为1,假设线程2抢先一步发起CAS操作,在CAS操作里,它会检查此时变量i的值是否还是1,如果是,则更新为2。
  • 线程3发起CAS操作,它去检查变量i的值,发现不是1而是2,CAS失败。然后进入自旋,再次读取变量i的值为2,紧接着进入CAS操作,检查变量i是否还是2,此时是2,则更新为3。

上述过程,就是Atomic原子类的实现原理,并没有基于加锁串行化处理,通过CAS方式,无锁地进行更新,而CAS是由操作系统底层保证了其操作的原子性。

CAS实现原子操作的三大问题

  1. ABA问题
    CAS操作需要检查当前内存的变量值是否和刚刚读取的值相同,假设一个变量是A,变成了B,之后又变成了A,所以在进行CAS操作的时候检查到它的值没有发生变化。ABA的解决方案就是加上版本号1A->2B->3A,类似这种。java先行者们也给我们提供了AtomicStampedReference来解决ABA的问题,其中的预期标志也类似于版本号的功能。
  2. 循环时间长开销大
    多线程竞争激烈的情况下进行CAS操作,会导致某些线程长时间空循环,也就是说它什么都没做,只是不停地在浪费处理器的处理时间而已。
  3. 只能保证一个共享变量的原子操作
    一个共享变量的操作可以用CAS保证其原子性,多个共享变量的操作,循环CAS是无法保证其原子性的。有个取巧的办法,java先行者设计的AtomicReference类,我们可以通过将多个变量放到一个对象里面,然后由AtomicReference进行原子性地更新。

参考文档

方腾飞《并发编程的艺术》

你可能感兴趣的:(并发编程之CAS)