在Java并发中,首先应该接触synchronized关键字,但是synchronized是一个重量级的锁,往往会导致性能问题。volatile也是一个不错的选择,但是volatile不能保证原子性,只能在特定的情况下使用。像synchronized这样的独占锁属于悲观锁。它假定肯定会发生冲突,所以锁定只是有用的。此外,还有乐观锁。乐观锁的意思就是,如果没有冲突,那么我就可以执行某个操作,如果有冲突,那么我就重试,直到成功。最常见的乐观锁是CAS。我们在阅读Concurrent包下类的源码时发现,无论是ReenterLock内部的AQS,还是以Atomic开头的各种原子类,都应用了CAS。最常见的情况就是我们在并发编程中遇到的i++。.传统的方法肯定是在方法中加上synchronized关键字:publicclassTest{publicvolatileinti;publicsynchronizedvoidadd(){i++;}}复制代码但是这个方法在性能上可能会差一些,我们还有AtomicInteger可以用于保证第i个原子++。公共类测试{publicAtomicIntegeri;publicvoidadd(){i.getAndIncrement();}}复制代码让我们看看getAndIncrement的内部结构:publicfinalintgetAndIncrement(){returnunsafe.getAndAddInt(this,valueOffset,1);}复制代码并深入了解getAndAddInt():(var1,var2,var5,var5+var4));returnvar5;}复制代码这里我们看到函数compareAndSwapInt,这也是CAS缩写的由来。那么仔细分析一下这个函数是做什么的呢?首先我们在compareAndSwapInt前面找到了这个,那么它属于哪个类呢?再看看上一步的getAndAddInt,前面是不安全的。这里我们进入Unsafe类。这是Unsafe类的解释。结合AtomicInteger的定义说:publicclassAtomicIntegerextendsNumberimplementsjava.io.Serializable{privatestaticfinallongserialVersionUID=6214790243416807050L;//setuptouseUnsafe.compareAndSwapIntforupdatesprivatestaticfinalUnsafeunsafe=Unsafe.getUnsafe();privatestaticfinallongvalueOffset;static{try{valueOffset=unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));}catch(Exceptionex){thrownewError(ex);}}privatevolatileintvalue;...复制代码在AtomicInteger数据定义的部分,我们可以看到实际存储的值实际上是放在value中的。此外,我们还获取了unsafe实例,并定义了valueOffset。再来看静态块,了解类加载过程的都知道,静态块的加载发生在类加载和初始化的时候。这时候我们调用不安全的objectFieldOffset从Atomic类文件中获取value的偏移量。那么valueOffset其实就是记录了value的偏移量。回到上面的函数getAndAddInt,我们看看var5得到了什么。通过调用不安全的getIntVolatile(var1,var2),这是一个本地方法。它在JDK源代码中实现。其实就是获取var1,var2偏置处的值。var1是AtomicInteger,var2是我们前面提到的valueOffset,所以我们可以从内存中获取valueOffset处的当前值。现在重要的一点是,compareAndSwapInt(var1,var2,var5,var5+var4)实际上被compareAndSwapInt(obj,offset,expect,update)代替了,也就是说如果obj中的值等于expect,就证明没有其他线程改变了,传递这个变量后,更新为update。如果这一步CAS不成功,则使用spin方法继续CAS操作。乍一看,好像这是两步。其实在JNI中,它是用一个CPU指令完成的。所以它仍然是一个原子操作。CAS底层原理CAS底层使用JNI调用C代码,如果你有Hotspot源码,可以在Unsafe.cpp中找到它的实现:staticJNINativeMethodmethods_15[]={//省略一堆代码...{CC"compareAndSwapInt",CC"("OBJ"J""I""I"")Z",FN_PTR(Unsafe_CompareAndSwapInt)},{CC"compareAndSwapLong",CC"("OBJ"J""J""J"")Z",FN_PTR(Unsafe_CompareAndSwapLong)},//省略一堆代码...};复制代码可以看到compareAndSwapInt的实现是在Unsafe_CompareAndSwapInt中,再深入Unsafe_CompareAndSwapInt:UNSAFE_ENTRY(jboolean,Unsafe_CompareAndSwapInt(JNIEnv*env,jobjectunsafe,jobjectobj,jlong??offset,jinte,jintx))UnsafeWrapper("Unsafe_CompareAndSwapInt");oopp=JNIHandles::resolve(obj);jintaddr=(jint)index_oop_from_field_offset_long(p,offset);return(jint)(Atomic::cmpxchg(x,addr,e))==e;UNSAFE_END拷贝代码p为提取出来的对象,addr为p中偏移处的地址,最后调用Atomic::cmpxchg(x,addr,e),其中参数x是要更新的值,参数e是原始内存的值。在代码中可以看到cmpxchg有基于各种平台的实现。这里我选择LinuxX86平台下的源码分析:inlinejintAtomic::cmpxchg(jintexchange_value,volatilejint*dest,jintcompare_value){intmp=os::is_MP();asmvolatile(LOCK_IF_MP(%4)"cmpxchgl%1,(%3)":"=a"(exchange_value):"r"(exchange_value),"a"(compare_value),"r"(dest),"r"(mp):"cc","memory");returnexchange_value;}复制代码这是一个小程序集,__asm__表示是ASM程序集,__volatile__禁止编译器优化//给MPmachinedefine上的一条指令加锁前缀LOCK_IF_MP(mp)"cmp$0,"#mp";je1f;lock;1:"复制代码os::is_MP判断当前系统是否为多核系统,如果是则锁定总线,所以同一芯片计算机上的其他处理器暂时无法通过总线访问内存,保证了多处理器环境下指令的原子性。在正式解释这个汇编之前,先了解一下嵌入式汇编的基本格式:asm(汇编模板:输出操作数/*可选*/:输入操作数/*可选*/:破坏寄存器列表/*可选*/);copycodetemplate为cmpxchgl%1,(%3)表示汇编模板outputoperands表示输出操作数,=a对应eaxregisterinputoperand表示输入参数,%1表示exchange_value,%3表示dest,%4表示mp,r表示anyregister,aoreaxregisterlistofclobberedregisters都是一些额外的参数,cc表示编译器cmpxchgl的执行会影响标志寄存器,memory告诉编译器再次从内存中读取变量的最新值,实现了挥发性的感觉。那么表达式其实就是cmpxchglexchange_value,dest,我们会发现%2,也就是compare_value是没有用的。这里需要分析一下cmpxchgl的语义。cmpxchgl末尾的l表示操作数长度为4,如前所述。cmpxchgl默认会比较eax寄存器的值,即compare_value和exchange_value。如果相等,则将dest的值赋给exchange_value,否则,将exchange_value赋给eax。具体的汇编说明请参考Intel手册CMPXCHG。最后,JDK通过CPU的cmpxchgl指令的支持,实现了AtomicInteger的CAS操作的原子性。CAS问题ABA问题CAS在操作值的时候需要检查值是否发生变化,没有变化就更新,但是如果一个值本来是A,变成B,再变成A,那么用CAS检查的时候,它的值不会改变,但它确实会改变。这就是CAS的ABA问题。一个常见的解决方案是使用版本号。在变量前面加上版本号,变量每更新一次版本号就加一,那么A-B-A就变成1A-2B-3A。目前JDK的atomic包中提供了一个类AtomicStampedReference来解决ABA问题。该类的compareAndSet方法的作用是首先检查当前引用是否等于期望引用,当前标志是否等于期望标志,如果都相等则设置引用的值和以原子方式标记给定的更新值。循环时间长,开销大我们上面说了,如果CAS不成功,就会原地自旋。如果自旋很长,会给CPU带来非常大的执行开销。
