CAS是一种什么样的同步机制?
在高并发的业务场景下,线程安全问题是必须考虑的,在JDK5之前,可以通过synchronized或Lock来保证同步,从而达到线程安全的目的。但synchronized或Lock方案属于互斥锁的方案,比较重量级,加锁、释放锁都会引起性能损耗问题。
而在某些场景下,我们是可以通过JUC提供的CAS机制实现无锁的解决方案,或者说是它基于类似于乐观锁的方案,来达到非阻塞同步的方式保证线程安全。
CAS是Compare And Swap的缩写,直译就是比较并交换。CAS是现代CPU广泛支持的一种对内存中的共享数据进行操作的一种特殊指令,这个指令会对内存中的共享数据做原子的读写操作。其作用是让CPU比较内存中某个值是否和预期的值相同,如果相同则将这个值更新为新值,不相同则不做更新。
本质上来讲CAS是一种无锁的解决方案,也是一种基于乐观锁的操作,可以保证在多线程并发中保障共享资源的原子性操作,相对于synchronized或Lock来说,是一种轻量级的实现方案。
Java中大量使用了CAS机制来实现多线程下数据更新的原子化操作,比如AtomicInteger、CurrentHashMap当中都有CAS的应用。但Java中并没有直接实现CAS,CAS相关的实现是借助C/C++调用CPU指令来实现的,效率很高,但Java代码需通过JNI才能调用。比如,Unsafe类提供的CAS方法(如compareAndSwapXXX)底层实现即为CPU指令cmpxchg。
CAS的基本流程:
在上图中涉及到三个值的比较和操作:修改之前获取的(待修改)值A,业务逻辑计算的新值B,以及待修改值对应的内存位置的C。
整个处理流程中,假设内存中存在一个变量i,它在内存中对应的值是A(第一次读取),此时经过业务处理之后,要把它更新成B,那么在更新之前会再读取一下i现在的值C,如果在业务处理的过程中i的值并没有发生变化,也就是A和C相同,才会把i更新(交换)为新值B。如果A和C不相同,那说明在业务计算时,i的值发生了变化,则不更新(交换)成B。最后,CPU会将旧的数值返回。而上述的一系列操作由CPU指令来保证是原子的。
在《Java并发编程实践》中对CAS进行了更加通俗的描述:我认为原有的值应该是什么,如果是,则将原有的值更新为新值,否则不做修改,并告诉我原来的值是多少。
在上述路程中,我们可以很清晰的看到乐观锁的思路,而且这期间并没有使用到锁。因此,相对于synchronized等悲观锁的实现,效率要高非常多。
CAS的缺点:
- 循环时间长,开销大;
- 只能保证一个共享变量的原子操作;
- ABA问题;
循环时间长开销大
在分析Unsafe源代码的时候我们已经提到,在Unsafe的实现中使用了自旋锁的机制。在该环节如果CAS操作失败,就需要循环进行CAS操作(do while循环同时将期望值更新为最新的),如果长时间都不成功的话,那么会造成CPU极大的开销。如果JVM能支持处理器提供的pause指令那么效率会有一定的提升。
只能保证一个共享变量的原子操作
在最初的实例中,可以看出是针对一个共享变量使用了CAS机制,可以保证原子性操作。但如果存在多个共享变量,或一整个代码块的逻辑需要保证线程安全,CAS就无法保证原子性操作了,此时就需要考虑采用加锁方式(悲观锁)保证原子性,或者有一个取巧的办法,把多个共享变量合并成一个共享变量进行CAS操作。
ABA问题
虽然使用CAS可以实现非阻塞式的原子性操作,但是会产生ABA问题,ABA问题出现的基本流程:
- 进程P1在共享变量中读到值为A;
- P1被抢占了,进程P2执行;
- P2把共享变量里的值从A改成了B,再改回到A,此时被P1抢占;
- P1回来看到共享变量里的值没有被改变,于是继续执行;
虽然P1以为变量值没有改变,继续执行了,但是这个会引发一些潜在的问题。ABA问题最容易发生在lock free的算法中的,CAS首当其冲,因为CAS判断的是指针的地址。如果这个地址被重用了呢,问题就很大了(地址被重用是很经常发生的,一个内存分配后释放了,再分配,很有可能还是原来的地址)。
维基百科上给了一个形象的例子:你拿着一个装满钱的手提箱在飞机场,此时过来了一个火辣性感的美女,然后她很暖昧地挑逗着你,并趁你不注意,把用一个一模一样的手提箱和你那装满钱的箱子调了个包,然后就离开了,你看到你的手提箱还在那,于是就提着手提箱去赶飞机去了。
ABA问题的解决思路就是使用版本号:在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么A->B->A就会变成1A->2B->3A。
另外,从Java 1.5开始,JDK的Atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。