CAS的全称为Compare-And-Swap,⽐较并交换,是⼀种很重要的同步思想。它是⼀条CPU并发原语。
它的功能是判断主内存某个位置的值是否为跟期望值⼀样,相同就进⾏修改,否则⼀直重试,直到⼀致为⽌。这个过程是原⼦的。
看下⾯这段代码,思考运⾏结果是
import java.util.concurrent.atomic.AtomicInteger;/*** CAS的全称为Compare-And-Swap,比较并交换,是一种很重要的同步思想。它是一条CPU并发原语。* 它的功能是判断主内存某个位置的值是否为跟期望值一样,相同就进行修改,否则一直重试,直到一致为止。这个过程是原子的。*/
public class CASDemo {public static void main(String[] args) {AtomicInteger atomicInteger = new AtomicInteger(5);//CAS操作System.out.println(atomicInteger.compareAndSet(5, 2000) + "\t最终值:" + atomicInteger.get());System.out.println(atomicInteger.compareAndSet(5, 1024) + "\t最终值:" + atomicInteger.get());//true 最终值:2000//false 最终值:2000}
}
分析
第⼀次修改,期望值为5,主内存也为5,修改成功,为2000。
第⼆次修改,期望值为5,主内存为2000,修改失败,需要重新获取主内存的值 。
CAS并发原语体现在JAVA语⾔中就是sum.misc.Unsafe类中的各个⽅法。看⽅法源码,调⽤UnSafe类中的CAS⽅法,JVM会帮我们实现出CAS汇编指令。这是⼀种完全依赖于硬件的功能,通过它实现了原⼦操作。再次强调,由于CAS是⼀种系统原语,原语属于操作系统⽤语范畴,是由若⼲条指令组成的,⽤于完成某个功能的⼀个过程,并且原语的执⾏执⾏是连续的,在执⾏过程中不允许被中断,也就是说 CAS是⼀条CPU的原⼦指令,不会造成所谓的数据不⼀致问题。
AtomicInteger.getAndIncrement()
调⽤了 Unsafe.getAndAddInt()
⽅法。 Unsafe
类的⼤部分 ⽅法都是 native
的,⽤来像C语⾔⼀样从底层操作内存。
C语句代码JNI,对应java⽅法 public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5)
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset,jint e, jint x))
UnsafeWrapper("Unsafe_CompareAndSwapInt");
oop p = JNIHandles::resolve(obj);
jint* add = (jint *)index_oop_from_field_offset_long(p, offset); return (jint)(Atomic::cmpxchg(x,addr,e))==e;
UNSAFE_END
//先想办法拿到变量value在内存中的地址addr。
//通过Atomic::cmpxchg实现⽐较替换,其中参数x是即将更新的值,参数e是原内存的值
这个⽅法的var1和var2,就是根据对象和偏移量得到在主内存的快照值var5。然 后
compareAndSwapInt⽅法通过var1和var2得到当前主内存的实际值。如果这个实际值跟快照值相
等,那么就更新主内存的值为var5+var4。如果不等,那么就⼀直循环,⼀直获取快照,⼀直对⽐,直 到实际值和快照值相等为⽌。
参数介绍
var1
AtomicInteger对象本身
var2
该对象值的引⽤地址
var4
需要变动的数量
var5
是通过var1和var2,根据对象和偏移量得到在主内存的快照值var5
⽐如有A、B两个线程,⼀开始都从主内存中拷⻉了原值为3,
A线程执⾏到 var5=this.getIntVolatile,即var5=3。
此时A线程挂起,B修改原值为4,B线程执⾏完毕,由于加了volatile,所以这个修改是⽴即可⻅的。
A线程被唤醒,执⾏ this.compareAndSwapInt()⽅法,发现这个时候主内存的值不等于快照值3,所以继续循环,重新从主内存获取。
CAS实际上是⼀种⾃旋锁,
所谓ABA问题,就是CAS算法实现需要取出内存中某时刻的数据并在当下时刻⽐较并替换,这⾥存在⼀ 个时间差,那么这个时间差可能带来意想不到的问题。
⽐如,⼀个线程A 从内存位置Value中取出3,这时候另⼀个线程B 也从内存位置Value中取出3,并且线程B进⾏了⼀些操作将值变成了4,然后线程C⼜再次将值变成了3,这时候线程A进⾏CAS操作发现 内存中仍然是3,然后线程A操作成功。
尽管线程A的CAS操作成功,但是不代表这个过程就是没有问题的。
有这样的需求,⽐如CAS,只注重头和尾,只要⾸尾⼀致就接受。
但是有的需求,还看重过程,中间不能发⽣任何修改,这就引出了
AtomicInteger对整数进⾏原⼦操作,如果是⼀个POJO呢?可以⽤ AtomicReference来包装这个 POJO,使其操作原⼦化。
public class AtomicReferenceDemo {public static void main(String[] args) {User user1 = new User("Jack",25);User user2 = new User("Tom",21);User user3 = new User("Ros",28);AtomicReference atomicReference = new AtomicReference<>();atomicReference.set(user1);//CAS操作 主内存中的原始值user1和期望值user1比较相等,返回值为true且将主内存中的原始值修改为user2;System.out.println(atomicReference.compareAndSet(user1,user2)+"\t"+atomicReference.get());//CAS操作 主内存中的原始值user2和期望值user1比较不相等,返回值为false,不更新期望值;System.out.println(atomicReference.compareAndSet(user1,user3)+"\t"+atomicReference.get());//true User(username=Tom, age=21)//false User(username=Tom, age=21)}
}
使⽤ AtomicStampedReference
类可以解决ABA
问题。这个类维护了⼀个“版本号”Stamp
,在进⾏CAS
操作的时候,不仅要⽐较当前值,还要⽐较版本号。只有两者都相等,才执⾏更新操作。
解决ABA问题的关键⽅法:
参数说明:
V expectedReference, 预期值引⽤
V newReference, 新值引⽤
int expectedStamp,预期值时间戳
int newStamp, 新值时间戳
public class AtomicReferenceDemo2 {static AtomicReference atomicReference = new AtomicReference<>(100);public static void main(String[] args) {System.out.println("========ABA问题的产生=========");new Thread(() -> {//CAS 主内存中的原始值100和期望值100比较相等,返回值为true且将主内存中的原始值修改为111;atomicReference.compareAndSet(100, 111);//CAS 主内存中的原始值111和期望值111比较相等,返回值为true且将主内存中的原始值修改为100;atomicReference.compareAndSet(111, 100);}, "t1").start();new Thread(() -> {//CASSystem.out.println(atomicReference.compareAndSet(100, 2020) + "\t" + atomicReference.get());}, "t2").start();}
package thread;import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicStampedReference;/*** ABA问题*/
public class ABADemo {//带有时间戳的原子引用 (共享内存值100, 版本号为1)static AtomicStampedReference atomicStampedReference = new AtomicStampedReference<>(100, 1);public static void main(String[] args) {System.out.println("=========ABA问题的解决===========");new Thread(() -> {//获取第一次的版本号int stamp = atomicStampedReference.getStamp();System.out.println(Thread.currentThread().getName() + "\t第一次版本号" + stamp);//CAS 共享内存值100和期望值100比较相等,且共享内存时间戳和预期值时间戳相等;返回值为true且将共享内存值修改为111时间戳为2;try {//休眠一秒,模拟并发,给ThreadA预留时间启动。TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}atomicStampedReference.compareAndSet(100,111,atomicStampedReference.getStamp(),atomicStampedReference.getStamp() + 1);System.out.println(Thread.currentThread().getName() + "\t第二次版本号:" + atomicStampedReference.getStamp());//CAS 共享内存值111和期望值111比较相等,且共享内存时间戳和预期值时间戳相等;返回值为true且将共享内存值修改为100时间戳为3;atomicStampedReference.compareAndSet(111,100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp() + 1);System.out.println(Thread.currentThread().getName() + "\t第三次版本号:" + atomicStampedReference.getStamp());}, "ThreadB").start();new Thread(() -> {//获取第一次的版本号int stamp = atomicStampedReference.getStamp();System.out.println(Thread.currentThread().getName() + "\t第一次版本号" + stamp);//CAS 休眠3秒,与ThreadB时间差。模拟挂起;让ThreadB先执行,经过线程B的操作当前共享内存值为100,时间戳为3try {TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace();}//共享内存值100和期望值100比较相等,但是共享内存时间戳3和预期值时间戳1不相等;返回值为false,不修改共享内存值和时间戳;boolean result = atomicStampedReference.compareAndSet(100,2020,stamp,stamp + 1);System.out.println(Thread.currentThread().getName()+ "\t修改是否成功:" + result+ "\t当前最新的版本号:" + atomicStampedReference.getStamp()+ "\t当前最新的值:" + atomicStampedReference.getReference());}, "ThreadA").start();//=========ABA问题的解决===========//ThreadB 第一次版本号1//ThreadA 第一次版本号1//ThreadB 第二次版本号:2//ThreadB 第三次版本号:3//ThreadA 修改是否成功:false 当前最新的版本号:3 当前最新的值:100}
}