前言排他锁是悲观锁,synchronized是排他锁的一种,会导致其他所有需要锁的线程挂掉,等待持有锁的线程释放锁;另一种更有效的锁是乐观锁。所谓乐观锁,就是在假设没有冲突的情况下,每次不加锁就完成一个操作。如果由于冲突而失败,请重试,直到成功。乐观锁使用的机制是CAS;今天我们来介绍一下cas机制;一、CAS简介1、什么是CAS?CAS指令在IntelCPU上称为CMPXCHG指令。它的作用是将指定内存地址的内容与给定值进行比较。如果它们相等,则将内容替换为指令中提供的新值。如果它们不相等,则更新失败。从内存域的角度来看,这是一个乐观锁,因为它在更新共享变量之前,会先比较当前值和更新前的值是否一致。对于自旋),直到当前值与更新前的值一致后才进行更新;CAS操作包含三个操作数——内存位置(V)、预期的原始值(A)和新值(B)。如果内存位置的值与预期的原始值匹配,处理器会自动用新值更新该位置的值。否则,处理器什么都不做。在任何一种情况下,它都会返回CAS指令之前该位置的值;通常使用CAS进行同步的方式是从地址V读取值A,经过多步计算得到新的值B,然后使用CAS将V的值从A改变为B。CAS操作成功如果V处的值没有同时改变;类似CAS的指令允许算法执行读-修改-写操作,而不用担心其他线程同时修改变量,因为如果其他线程修改变量,那么CAS将检测到它(并失败),算法可以重新计算操作;2、CAS机制用在什么地方在java.util.concurrent.atomic包下,一系列以Atomic开头的包装类。比如AtomicBoolean、AtomicInteger、AtomicLong等,都是典型的CAS机制实现的原子操作类;Lock系列类的底层实现和Java1.6在synchronized转换为重量级锁之前也会采用CAS机制;3、synchronized与CAS和synchronized的区别在于CPU悲观锁机制,即线程获得独占锁。独占锁意味着其他线程只能依靠阻塞等待线程释放锁。当CPU转换线程阻塞时,会引起线程上下文切换。当有很多线程竞争锁时,会造成频繁的CPU上下文切换,导致效率低下。Java1.6虽然对synchronized进行了优化,增加了从偏向锁到轻量级锁再到重量级锁的过渡,但最终转换为重量级锁后,性能还是很低;synchronized(优化前)最主要的问题是:在线程竞争的情况下,会出现线程阻塞和唤醒锁导致的性能问题,因为这是一种互斥同步(阻塞同步)。CAS不是任意的线程间挂起。当CAS操作失败时,它会进行一定的尝试,而不是进行耗时的挂起和唤醒操作,因此也称为非阻塞同步。这是两者之间的主要区别;使用CAS时非阻塞同步,也就是说线程不会被挂起,它会自旋(无非是死循环)等待下一次尝试,如果这里的自旋时间过长,就会对性能有害消耗大。如果JVM能够支持处理器提供的暂停指令,效率会有一定的提升;CAS使用3个基本操作数:内存地址V、旧期望值A、待修改的新值B。它采用乐观锁机制,不会阻塞任何线程,因此会比synchronized更高效。所谓乐观锁就是:每次没有锁但假设没有冲突完成某个操作,如果因为冲突而失败,则重试,直到成功为止;4、为什么需要CAS机制?我们经常用volatile关键字修饰某个变量,表示这个变量是一个全局共享变量,兼具可见性和有序性。但它不是原子的。比如一个普通的操作a++。这个操作其实可以细分为三个步骤:(1)从内存中读取a(2)将a加1(3)将a的值改写到内存中。问题,但是在多线程中会出现各种问题。因为一个线程可能对a加1,其他线程在将旧值写入内存之前读取旧值。导致线程不安全;Volatile关键字可以保证线程间共享变量的可见性和有序性,可以防止CPU指令重排序(DCL单例),但不能保证操作的原子性,所以jdk1.5之后,引入了CAS,使用CPU原语来保证线程操作的完整性;CAS操作由处理器支持,这是一个原语。原语是操作系统或计算机网络术语类别。它由几条指令组成,用于完成一定的功能。它是不可分割的,即原语的执行必须是连续的,在执行过程中不允许中断。例如在Intel处理器中,比较和交换是通过cmpxchg系列指令实现的;二、cas的底层实现1、底层依赖Unsafe的CAS操作来保证原子性;CAS的实现主要在JUC中的atomic包中。我们以AtomicInteger类为例:{intcurrent=get();intnext=current+1;if(compareAndSet(current,next))returnnext;}}privatevolatileintvalue;publicfinalintget(){returnvalue;}代码是一个无限循环,这是CAS的自旋。循环体中做了三件事:获取当前值;当前值+1,计算目标值;进行CAS操作,成功则跳出循环(当前值等于目标值),失败则重复上述步骤;2.Unsafe.classpublicfinalintgetAndAddInt(Objectvar1,longvar2,intvar4){intvar5;do{var5=this.getIntVolatile(var1,var2);}while(!this.compareAndSwapInt(var1,var2,var5,var5+var4));//nativemethodreturnvar5;}********publicfinalnativebooleancompareAndSwapInt(Objectvar1,longvar2,intvar4,intvar5);//底层c++实现publicfinalnativebooleancompareAndSwapInt(Objectvar1,longvar2,intvar4,intvar5);//底层c++实现3.compareAndSwapInt为native方法,对应热点虚拟机底层unsage.cppUNSAFE_ENTRY(jboolean,Unsafe_CompareAndSwapInt(JNIEnv*env,joobjectunsafe,jobjectobj,jlong??offset,jinte,jintx))UnsafeWrapper("Unsafe_CompareAndresolveInt");oopp=JNIHandlesobj);jint*addr=(jint*)index_oop_from_field_offset_long(p,offset);return(jint)(Atomic::cmpxchg(x,addr,e))==e;UNSAFE_END***这里可以看到最后使用Atomic::cmpxchg来保证原子性,可以继续跟进代码4.Atomic::cmpxchg有不同的实现n不同平台的方法***//AddingalockprefixtoaninstructiononMPmachine#defineLOCK_IF_MP(mp)"cmp$0,"#mp";je1f;lock;1:"***inlinejintAtomic::cmpxchg(jintexchange_value,volatilejint*dest,jintcompare_value){intmp=os::is_MP();__asm__volatile(LOCK_IF_MP(%4))"cmpxchgl%1,(%3)":"=a"(exchange_value):"r"(exchange_value),"a"(compare_value),"r"(dest),"r"(mp):"cc","memory");returnexchange_value;}最重要的指令是LOCK_IF_MP,MP指的是multi-CPU(多处理器),最终的意思是在多CPU的情况下,需要加锁,保证原子性通过锁;锁解释:保证后续指令执行的原子性;在奔腾及之前的处理器中,带有锁前缀的指令会在执行过程中锁定总线,使得其他处理器暂时无法通过总线访问内存。显然,这个开销在新的处理器中是非常大的,Intel使用缓存锁来保证指令执行的原子性。缓存锁会大大降低锁前缀指令的执行开销;禁止该指令与之前和之后的读写指令重新排序;将bufferRefresh中的所有数据写入内存;简而言之:在JAVA中,我们使用CAS操作相关的底层实现,在对应平台的虚拟机中实现c++代码(lock指令),保证原子性;3、CAS的缺点及解决方案虽然CAS非常高效的解决了原子操作,但是CAS仍然存在三大问题。ABA问题,循环时间长,开销大,只能保证共享变量的原子操作;1.ABA问题:因为CAS在操作值的时候需要检查这个值是否发生了变化,如果没有变化就更新,但是如果一个值原来是A,变成了B,又转回来了toA。然后你用CAS查的时候,你会发现它的值没有变,但实际上变了;ABA问题的解决方案是使用版本号。在变量前加上版本号,每次更新变量时版本号加1,则A-B-A变为1A-2B-3A;从Java1.5开始,JDK的原子包提供了一个类AtomicStampedReference来解决ABA问题。该类的compareAndSet方法的作用是先检查当前引用是否等于期望引用,当前标志是否等于期望标志,如果都相等则设置引用的值,以原子方式给定更新值的标志;2.循环时间长,开销大:在高并发的情况下,如果很多线程反复尝试更新某个变量,即自旋CAS长时间失败,会给线程带来非常大的执行开销中央处理器;如果JVM能够支持使用处理器提供的暂停指令的话,效率会得到一定程度的提升。暂停指令有两个功能。首先,它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源。延迟时间取决于具体的实现版本。在某些处理器上,延迟时间为零。第二,可以避免在退出循环时由于内存顺序违规(memoryorderviolation)导致CPU流水线被清空(CPUpipelineflush),从而提高CPU的执行效率;代码层面,破坏for无限循环,当自旋一定时间或一定次数后,return退出;使用类似于ConcurrentHashMap的方法。多线程竞争时,降低粒度,将一个变量拆成多个变量,达到多线程访问多个资源的效果。最后调用sum将它们合并,可以减少CPU消耗,但不是临时解决方案。3、只能保证对一个共享变量的原子操作:在对一个共享变量进行操作时,我们可以使用循环CAS的方式来保证原子操作,但是在对多个共享变量进行操作时,循环CAS不能保证操作原子性;这时候可以使用锁,或者有一个取巧的方法,将多个共享变量组合成一个共享变量进行操作。比如有两个共享变量i=2,j=a,合并ij=2a,然后用CAS操作ij;从Java1.5开始,JDK提供了AtomicReference类来保证被引用对象之间的原子性,可以把多个变量放在一个对象中进行CAS操作;4、CAS使用的线程数少,等待时间短。可以对CAS使用自旋锁来尝试获取锁,比synchronized效率更高;线程数多,等待时间短。long,不推荐使用自旋锁,占用CPU大;综上所述,CAS可以保证多线程写入数据时数据的一致性;CAS的思想:三个参数,一个是当前内存值V,一个是旧的期望值A,一个是要更新的值B,当且仅当期望值A和内存值V相同时,修改内存值到B并返回true,否则什么都不做并返回false;
