CAS
Compare and Swap 比较并交换 [算法实现乐观锁 ]
它是指一种操作机制,而不是某个具体的类或方法。
在 Unsafe 类中这种机制在不阻塞其他线程的情况下避免了并发冲突,比独占锁的性能高很多。
整个AQS同步组件,Atomic原子类操作等等都是基于CAS实现的,甚至ConcurrentHashMap在JDK1.8版本中,也调整为CAS+synchronized。可以说,CAS是整个JUC的基石。
CAS算法的过程:
它包含3个参数CAS(V,E,N)。
V表示要更新的变量,E表示预期值,N表示新值。
仅当V 值等于E值时,才会将V的值设为N,如果V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。
最后,CAS返回当前V的真实值。
CAS操作是抱着乐观的态度进行的,它总是认为自己可以成功完成操作。当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。
基于这样的原理,CAS操作即时没有锁,也可以发现其他线程对当前线程的干扰,并进行恰当的处理。
实现原理
CAS 主要分三步,读取-比较-修改。其中比较是在检测是否有冲突,如果检测到没有冲突后,其他线程还能修改这个值,那么 CAS 还是无法保证正确性。所以最关键的是要保证比较-修改这两步操作的原子性。
CAS 底层是靠调用 CPU 指令集的 cmpxchg 完成的,它是 x86 和 Intel 架构中的 compare and exchange 指令。在多核的情况下,这个指令也不能保证原子性,需要在前面加上 lock 指令。lock 指令可以保证一个 CPU 核心在操作期间独占一片内存区域。那么 这又是如何实现的呢?
在处理器中,一般有两种方式来实现上述效果:总线锁和缓存锁。在多核处理器的结构中,CPU 核心并不能直接访问内存,而是统一通过一条总线访问。总线锁就是锁住这条总线,使其他核心无法访问内存。这种方式代价太大了,会导致其他核心停止工作。而缓存锁并不锁定总线,只是锁定某部分内存区域。当一个 CPU 核心将内存区域的数据读取到自己的缓存区后,它会锁定缓存对应的内存区域。锁住期间,其他核心无法操作这块内存区域。
CAS 就是通过这种方式实现比较和交换操作的原子性的。
值得注意的是, CAS 只是保证了操作的原子性,并不保证变量的可见性,因此变量需要加上 volatile 关键字。
缺点**
循环时间长开销大:
自旋CAS(不成功,就一直循环执行,直到成功)如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。
只能保证一个共享变量的原子操作:
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。
从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。
ABA问题
如果⼀个变量V初次读取是A值,并且在准备赋值的时候也是A值,那就能说明A值没有被修改过吗?其实是不能的,因为变量V可能被其他线程改回A值,结果就是会导致CAS操作误认为从来没被修改过,从⽽赋值给V
给变量加⼀个版本号即可,在⽐较的时候不仅要⽐较当前变量的值 还需要⽐较当前变量的版本号。
在java5中,已经提供了AtomicStampedReference来解决问题,检查当前引⽤是否等于预期引⽤,其次检查当前标志是否等于预期标志,如果都相等就会以原⼦的⽅式将引⽤和标志都设置为新值
CAS的ABA问题怎么解决
AtomicStampedReference,还可用带boolean版本戳的AtomicMarkableReference
CAS与Synchronized的使用情景:
1、cas 对于资源竞争较少(线程冲突较轻)的情况,
推荐CAS 基于硬件实现,不需要进入内核,操作自旋几率较少,因此可以获得更高的性能。
使用synchronized同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗cpu资源;
2、syn 对于资源竞争严重(线程冲突严重)的情况
CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于(推荐)synchronized。
原子类
Java中java.util.concurrent.atomic包下面的原子变量类就是使用了CAS实现的
AtomicInteger
- unsafe: 获取并操作内存的数据。
- valueOffset: 存储value在AtomicInteger中的偏移量。
- value: 存储AtomicInteger的int值,该属性需要借助volatile关键字保证其在线程间是可见的。
自增函数incrementAndGet()的源码时,发现自增函数底层调用的是unsafe.getAndAddInt()。但是由于JDK本身只有Unsafe.class
getAndAddInt()循环获取给定对象o中的偏移量处的值v,然后判断内存值是否等于v。如果相等则将内存值设置为 v + delta,否则返回false,继续循环进行重试,直到设置成功才能退出循环,并且将旧值返回。整个“比较+更新”操作封装在compareAndSwapInt()中,在JNI里是借助于一个CPU指令完成的,属于原子操作,可以保证多个线程都能够看到同一个变量的修改值。
后续JDK通过CPU的cmpxchg指令,去比较寄存器中的 A 和 内存中的值 V。如果相等,就把要写入的新值 B 存入内存中。如果不相等,就将内存值 V 赋值给寄存器中的值 A。然后通过Java代码中的while循环再次调用cmpxchg指令进行重试,直到设置成功为止。
unsafe进行自增操作的源码中的do-while循环就是一个自旋操作,如果修改数值失败则通过循环来执行自旋,直至修改成功。
自旋锁在JDK1.4.2中引入,使用-XX:+UseSpinning来开启。JDK 6中变为默认开启,并且引入了自适应的自旋锁(适应性自旋锁)。
Unsafe
非安全的操作,比如:
根据偏移量设置值
park()
底层的CAS操作
非公开API,在不同版本的JDK中, 可能有较大差异
因为其直接操作内存,有点不受JVM管束,所以官方建议除了JAVA自带的API内部使用,是不建议开发人员直接使用的。
LongAdder
LongAdder
类与AtomicLong
类的区别在于高并发时前者将对单一变量的CAS操作分散为对数组cells
中多个元素的CAS操作,取值时进行求和;而在并发较低时仅对base
变量进行CAS操作,与AtomicLong
类原理相同。不得不说这种分布式的设计还是很巧妙的。
AtomicReference
AtomicIntegerFieldUpdater
AtomicStampReference 解决ABA问题
AtomicIntegerArray 无锁数组
AtomicIntegerFieldUpdater
让普通变量也享受原子操作
https://blog.csdn.net/tiandao321/article/details/80811103
https://www.jianshu.com/p/ec045c38ef0c