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