背景在高并发业务场景下,必须考虑线程安全问题。在JDK5之前,可以通过synchronized或者Lock来保证同步,从而达到线程安全的目的。但是synchronized或者Lock方案是mutex方案,比较重量级。加锁和释放锁都会造成性能损失。在某些场景下,我们可以通过JUC提供的CAS机制来实现无锁方案,或者是基于类似乐观锁的方案实现非阻塞同步,保证线程安全。CAS机制不仅是面试中经常出现的面试题,也是高并发实践中必须掌握的知识点。如果你对CAS还不是很了解,也许你只有一个模糊的印象,这篇文章一定值得你花时间学习。什么是CAS?CAS是CompareAndSwap的缩写,直译就是比较交换。CAS是现代CPU广泛支持的一种特殊指令,用于对内存中的共享数据进行操作。该指令将对内存中的共享数据执行原子读写操作。它的作用是让CPU比较内存中的某个值是否与期望值相同,如果相同则更新该值为新值,如果不相同则将不被更新。从本质上讲,CAS是一种无锁的解决方案,是一种基于乐观锁的操作,可以保证多线程并发下共享资源的原子操作。与synchronized或Lock相比,是轻量级的实现。Java中广泛使用CAS机制来实现多线程下数据更新的原子操作。比如AtomicInteger、CurrentHashMap都有CAS的应用。但是,CAS并不是直接用Java实现的。CAS相关的实现是通过C/C++调用CPU指令来实现的,效率很高,但是Java代码需要通过JNI来调用。例如Unsafe类提供的CAS方法(如compareAndSwapXXX)的底层实现就是CPU指令cmpxchg。CAS的基本流程下面我们用一张图来了解一下CAS运行的基本流程。上图涉及三个值的比较运算:修改前(待修改)得到的值A,业务逻辑计算出的新值B,待修改值内存位置对应的C。在整个处理流程中,假设内存中有一个变量i,它在内存中对应的值为A(第一次读取)。这时候业务处理完之后,需要更新到B,然后在更新前再次更新。读取i的当前值C。如果在业务过程中i的值没有变化,即A和C相同,则i会被更新(交换)为新的值B。如果A和C不一样,说明i的值在业务计算时发生了变化,不会更新(交换)给B,最后CPU返回旧值。CPU指令保证上述一系列操作是原子的。《Java并发编程实践》比较通俗的CAS描述:我觉得原值应该是多少,如果是就把原值更新为新值,否则不修改,告诉我原值是多少。在上面的旅程中,我们可以清楚的看到乐观锁的思想,这期间没有使用锁。因此,相对于synchronized等悲观锁的实现,效率要高很多。基于CAS的AtomicInteger使用CAS的实现。最经典和常用的是AtomicInteger。我们来看看AtomicInteger是如何使用CAS实现原子操作的。为了形成一个更新更鲜明的对比,我们先来看看在不使用CAS机制的情况下,我们通常是如何处理线程安全的。当不使用CAS机制时,为了保证线程安全,基于synchronized的实现如下:publicclassThreadSafeTest{publicstaticvolatileinti=0;publicsynchronizedvoidincrease(){i++;}}至于上面这个例子的具体实现,这里就不展开了,很多相关的文章专门来讲解。我们只需要知道,为了保证i++的原子操作,在increase方法上使用了重量级锁synchronized,这会导致方法性能低下。所有调用该方法的操作都需要同步挂起处理。那么,如果使用基于CAS的AtomicInteger类,上述方法的实现就变得简单轻量:publicclassThreadSafeTest{privatefinalAtomicIntegercounter=newAtomicInteger(0);publicintincrease(){返回counter.addAndGet(1);}}之所以能够安全方便的实现安全操作,是因为AtomicInteger类采用了CAS机制。接下来,我们看一下AtomicInteger的功能和源码实现。CASAtomicInteger的AtomicInteger类是java.util.concurrent.atomic包下的一个原子类。这个包下还有AtomicBoolean、AtomicLong、AtomicLongArray、AtomicReference等原子类。主要用于保证高并发环境下的线程安全。AtomicInteger常用APIAtomicInteger类提供了以下常用API函数::get获取当前值并自增publicfinalintgetAndDecrement():获取当前值并自减publicfinalintgetAndAdd(intdelta):获取当前值并添加期望值voidlazySet(intnewValue):finallyIt将被设置为新值。使用lazySet设置值后,其他线程可能仍然可以在短时间内读取旧值。以上方法中,getAndXXX格式的方法都实现了原子操作。具体使用方法可以参考上面的addAndGet案例。AtomicInteger核心源码我们来看看AtomicInteger代码中的核心实现代码:私有静态最终长值偏移量;static{try{//用于获取value字段相对于当前对象“起始地址”的偏移valueOffset=unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));}catch(Exceptionex){thrownewError(ex);}}privatevolatileint值;//返回当前值publicfinalintget(){returnvalue;}//增加detlapublicfinalintgetAndAdd(intdelta){//1、this:当前实例//2、valueOffset:value实例变量的偏移量//3、delta:要加到当前的数价值(价值+增量)。返回unsafe.getAndAddInt(this,valueOffset,delta);}//增加1publicfinalintincrementAndGet(){returnunsafe.getAndAddInt(this,valueOffset,1)+1;}...}以上代码以AtomicInteger#incrementAndGet方法为例,展示了AtomicInteger的基本实现。其中,在static静态代码块中,基于Unsafe类,获取value字段相对于当前对象“起始地址”的偏移量,用于Unsafe类的后续处理。在处理自增原子操作时,使用了Unsafe类中的getAndAddInt方法,Unsafe类的该方法提供了CAS的实现,从而保证了自增操作的原子性。同时,在AtomicInteger类中,可以看到该值被volatile修饰,保证了属性值的线程可见性。在多并发的情况下,一个线程的修改可以保证其他线程可以立即看到修改后的值。从源码可以看出,AtomicInteger底层通过volatile变量和CAS的结合保证了更新数据的原子性。其中,Unsafe类对CAS的实现,下面会详细介绍。CAS的工作原理CAS的实现原理简单来说就是由Unsafe类和其中的自旋锁来完成的。下面就为源码看看这两块的内容吧。UnSafe类在AtomicInteger的核心源码中。已经看到CAS的实现是通过Unsafe类完成的。我们先了解下Unsafe类的作用。在之前的文章《各大框架都在使用的Unsafe类,到底有多神奇?》中也有对Unsafe类的详细介绍,大家可以参考一下,这里简单总结一下。sun.misc.Unsafe是JDK内部使用的一个工具类。它将一些Java意义上的“不安全”函数暴露给Java层代码,使得JDK可以使用更多的Java代码来实现一些平台相关的需要使用原生语言(如C或C++)的功能).实现的功能。这个类不应该在JDK核心类库之外使用,这就是它被命名为Unsafe的原因。JVM的实现可以自由选择如何实现Java对象的“布局”,即把Java对象的各个部分放在内存中的什么位置,包括对象的实例字段和一些元数据。Unsafe中的对象字段访问方法抽象了对象布局。它提供了objectFieldOffset()方法来获取一个字段相对于Java对象“起始地址”的偏移量,还提供了getInt、getLong、getObject。类的方法可以使用前面获得的偏移量来访问Java对象的字段。AtomicInteger的静态代码块中使用了objectFieldOffset()方法。Unsafe类的功能主要分为内存操作、CAS、Class相关、对象操作、数组相关、内存屏障、系统相关、线程调度等功能。这里我们只需要知道它的作用,方便理解CAS的实现。注意日常开发中不建议使用。Unsafe和CASAtomicInteger调用Unsafe#getAndAddInt方法:publicfinalintincrementAndGet(){returnunsafe.getAndAddInt(this,valueOffset,1)+1;}上面的代码相当于AtomicInteger调用UnSafe类的CAS方法,JVM帮我们实现汇编指令实现原子操作。Unsafe中的getAndAddInt方法实现如下:publicfinalintgetAndAddInt(Objectvar1,longvar2,intvar4){intvar5;做{var5=this.getIntVolatile(var1,var2);}while(!this.compareAndSwapInt(var1,var2,var5,var5+var4));返回var5;}getAndAddInt方法有三个参数:第一个参数表示当前对象,即新的AtomicInteger对象;第二个表示内存地址;第三个表示自增步长,AtomicInteger#incrementAndGet中默认的自增步长为1。在getAndAddInt方法中,先将当前对象主存中的值赋值给val5,然后进入while循环。判断此时主存中当前对象的值是否等于val5,若是,自增(交换值),否则继续循环,重新获取val5的值。上述逻辑中的核心方法是compareAndSwapInt方法,它是一个native方法。汇编后,这个方法就是一条CPU原语指令。原语指令是连续执行的,不会被打断,因此可以保证原子性。getAndAddInt方法中还涉及自旋锁。所谓自旋其实就是上面getAndAddInt方法中的dowhile循环操作。当期望值不等于主存中的值时,重新获取主存中的值,即自旋。这里我们可以看到CAS实现的一个缺点:内部自旋方法用于CAS更新(while循环执行CAS更新,如果更新失败,循环重试)。如果长时间失败,会造成巨大的CPU开销。此外,Unsafe类还支持其他CAS方法,如compareAndSwapObject、compareAndSwapInt、compareAndSwapLong。更多关于Unsafe类的功能就不展开了,可以看这篇文章《各大框架都在使用的Unsafe类,到底有多神奇?》。CAS的缺点CAS高效地实现了原子操作,但在以下三个方面还存在不足:循环时间长,开销大;只能保证共享变量的原子操作;ABA问题;下面对三个问题进行详细的讨论。周期长,开销大我们在分析Unsafe的源码时已经提到,Unsafe的实现使用了自旋锁机制。如果该环节CAS操作失败,则需要循环进行CAS操作(dowhile循环将期望值更新为最新)。如果长时间失败,会造成巨大的CPU开销。如果JVM能够支持处理器提供的暂停指令,效率会得到一定程度的提升。只有共享变量的原子操作才能得到保证。在最初的例子中,可以看出CAS机制是针对共享变量使用的,可以保证原子操作。但是如果有多个共享变量,或者整个代码块的逻辑需要保证线程安全,CAS就无法保证原子操作。这时候就需要考虑使用锁(悲观锁)来保证原子性,或者有一个trick方法是将多个共享变量组合成一个共享变量进行CAS操作。ABA问题虽然可以使用CAS实现非阻塞原子操作,但是会导致ABA问题。ABA题的基本流程:进程P1读取共享变量中的值A;P1被抢占,进程P2执行;P2把共享变量中的值从A改成B,再改回A,此时被P1抢占;P1回来看到共享变量中的值没有改变,就继续执行;虽然P1认为变量值没有变化,继续执行,但是这样会造成一些潜在的问题。ABA问题最容易出现在无锁算法中,CAS首当其冲,因为CAS判断的是指针的地址。如果这个地址被复用,问题会很大(地址复用的次数非常多,一块内存分配释放,再分配,很可能是原来的地址)。维基百科举了一个形象的例子:你提着一个装满钱的行李箱在机场,这时候走过来一个火辣性感的美女,然后很暧昧的挑逗你,趁你不注意的时候,调换了一个一模一样的行李箱,你的行李箱里装满了钱,走了,你看到你的行李箱还在,你就拿着行李箱去赶飞机。ABA问题的解决方法是使用版本号:在变量前面加上版本号,变量每更新一次版本号就加1,那么A->B->A就会变成1A->2B->3A。另外,从Java1.5开始,JDK的Atomic包中提供了一个类AtomicStampedReference来解决ABA问题。该类的compareAndSet方法的作用是首先检查当前引用是否等于期望引用,并检查当前标志是否等于期望标志,如果都相等,则原子地设置值给定更新值的引用和标志。小结本文从基本使用场景、基本流程、实现类AtomicInteger源码分析、CASUnsafe实现分析、CAS不足及解决方案等方面对CAS进行了全面的了解,通过本文的学习,你一定会有更深入的了解CAS机制。如果对你有帮助,记得关注,继续输出干货。
