Java原子操作CAS原理解析
一、CAS(CompareAndSet)
CompareAndSet(或CompareAndSwap),CAS是解决多线程并行情况下使用锁造成性能损耗的一种机制,CAS操作包含三个操作数——内存位置(V)、预期原值(A)、新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在CAS指令之前返回该位置的值。CAS有效地说明了“我认为位置V应该包含值A;如果包含该值,则将B放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。
在java中可以通过锁和循环CAS的方式来实现原子操作。Java中java.util.concurrent.atomic包相关类就是CAS的实现,atomic包里包括以下类:
AtomicBoolean | 可以用原子方式更新的 boolean 值。 |
AtomicInteger | 可以用原子方式更新的 int 值。 |
AtomicIntegerArray | 可以用原子方式更新其元素的 int 数组。 |
AtomicIntegerFieldUpdater |
基于反射的实用工具,可以对指定类的指定 volatileint 字段进行原子更新。 |
AtomicLong | 可以用原子方式更新的 long 值。 |
AtomicLongArray | 可以用原子方式更新其元素的 long 数组。 |
AtomicLongFieldUpdater |
基于反射的实用工具,可以对指定类的指定 volatilelong 字段进行原子更新。 |
AtomicMarkableReference |
AtomicMarkableReference 维护带有标记位的对象引用,可以原子方式对其进行更新。 |
AtomicReference |
可以用原子方式更新的对象引用。 |
AtomicReferenceArray |
可以用原子方式更新其元素的对象引用数组。 |
AtomicReferenceFieldUpdater |
基于反射的实用工具,可以对指定类的指定 volatile 字段进行原子更新。 |
AtomicStampedReference |
AtomicStampedReference 维护带有整数“标志”的对象引用,可以用原子方式对其进行更新。 |
二、AtomicInteger
AtomicInteger可以用原子方式更新的int值。AtomicInteger可用在应用程序中(如以原子方式增加的计数器),并且不能用于替换Integer。但是,此类确实扩展了Number,允许那些处理基于数字类的工具和实用工具进行统一访问。我们拿AtomicInteger为例来学习下CAS操作是如何实现的。
通常情况下,在Java中,i++等类似操作并不是线程安全的,因为i++可分为三个独立的操作:获取变量当前值,为该值+1,然后写回新的值。在没有额外资源可以利用的情况下,只能使用加锁才能保证读-改-写这三个操作时“原子性”的。但是利用加锁的方式来实现该功能的话,代码将非常复杂及难以维护,如:
synchronized(lock){ i++; }
相关类中还需要增加Objectlock等额外标志,这样就带来了很多麻烦,增加了很多业务无关代码,给开发与维护带来了不便。
然而利用atomic包中相关类型就可以很简单实现此操作,以下是一个计数程序实例:
publicclassCounter{ privateAtomicIntegerai=newAtomicInteger(); privateinti=0; publicstaticvoidmain(String[]args){ finalCountercas=newCounter(); Listthreads=newArrayList (); //添加100个线程 for(intj=0;j<100;j++){ threads.add(newThread(newRunnable(){ publicvoidrun(){ //执行100次计算,预期结果应该是10000 for(inti=0;i<100;i++){ cas.count(); cas.safeCount(); } } })); } //开始执行 for(Threadt:threads){ t.start(); } //等待所有线程执行完成 for(Threadt:threads){ try{ t.join(); }catch(InterruptedExceptione){ e.printStackTrace(); } } System.out.println("非线程安全计数结果:"+cas.i); System.out.println("线程安全计数结果:"+cas.ai.get()); } /**使用CAS实现线程安全计数器*/ privatevoidsafeCount(){ for(;;){ inti=ai.get(); //如果当前值==预期值,则以原子方式将该值设置为给定的更新值 booleansuc=ai.compareAndSet(i,++i); if(suc){ break; } } } /**非线程安全计数器*/ privatevoidcount(){ i++; } } /** 非线程安全计数结果:9942 线程安全计数结果:10000 */
其中非线程安全计数器所计算的结果每次都不相同且不正确,而线程安全计数器计算的结果每次都是正确的。
三、存在的问题
CAS虽然很高效的解决原子操作,但是CAS仍然存在三大问题:ABA问题、循环时间长开销大、只能保证一个共享变量的原子操作。
ABA问题:因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A就会变成1A-2B-3A。从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
循环时间长开销大:自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memoryorderviolation)而引起CPU流水线被清空(CPUpipelineflush),从而提高CPU的执行效率。
只能保证一个共享变量的原子操作:当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。