CAS详解
- 一 简介
- 二 CAS底层原理
- 2.1.AtomicInteger内部的重要参数
- 2.2.AtomicInteger.getAndIncrement()分析
- 2.2.1.getAndIncrement()方法分析
- 2.2.2.举例分析
- 三 CAS缺点
- 四 CAS会导致"ABA问题"
- 4.1.AtomicReference 原⼦引⽤。
- 4.2.ABA问题的解决(AtomicStampedReference 类似于时间戳)
- 4.2.1 ABA问题的产生演示
- 4.2.2.ABA问题的解决
一 简介
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的原⼦指令,不会造成所谓的数据不⼀致问题。
二 CAS底层原理
2.1.AtomicInteger内部的重要参数
- Unsafe
是CAS的核⼼类,由于Java⽅法⽆法直接访问底层系统,需要通过本地(native)⽅法来访问,Unsafe 相当于⼀个后⾯,基于该类可以直接操作特定内存的数据。Unsafe类存在于sum.misc包中,其内部⽅ 法操作可以像C的指针⼀样直接操作内存,因为Java中CAS操作的执⾏依赖于Unsafe类的⽅法。
注意Unsafe类中的所有⽅法都是native修饰的,也就是说Unsafe类中的⽅法都直接调⽤操作系统底层 资源执⾏相应任务 - 变量valueOffset,表示该变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数 据的。
- 变量value⽤volatile修饰,保证了多线程之间的内存可⻅性。
2.2.AtomicInteger.getAndIncrement()分析
2.2.1.getAndIncrement()方法分析
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
2.2.2.举例分析
⽐如有A、B两个线程,⼀开始都从主内存中拷⻉了原值为3,
A线程执⾏到 var5=this.getIntVolatile,即var5=3。
此时A线程挂起,B修改原值为4,B线程执⾏完毕,由于加了volatile,所以这个修改是⽴即可⻅的。
A线程被唤醒,执⾏ this.compareAndSwapInt()⽅法,发现这个时候主内存的值不等于快照值3,所以继续循环,重新从主内存获取。
三 CAS缺点
CAS实际上是⼀种⾃旋锁,
- ⼀直循环,开销⽐较⼤。我们可以看到getAndAddInt⽅法执⾏时,有个do while,如果CAS失 败,会⼀直进⾏尝试。如果CAS⻓时间⼀直不成功,可能会给CPU带来很⼤的开销。
- 对⼀个共享变量执⾏操作时,我们可以使⽤循环CAS的⽅式来保证原⼦操作,但是,对多个共享变 量操作时,循环CAS就⽆法保证操作的原⼦性,这个时候就可以⽤锁来保证原⼦性。
- 引出了ABA问题。
四 CAS会导致"ABA问题"
所谓ABA问题,就是CAS算法实现需要取出内存中某时刻的数据并在当下时刻⽐较并替换,这⾥存在⼀ 个时间差,那么这个时间差可能带来意想不到的问题。
⽐如,⼀个线程A 从内存位置Value中取出3,这时候另⼀个线程B 也从内存位置Value中取出3,并且线程B进⾏了⼀些操作将值变成了4,然后线程C⼜再次将值变成了3,这时候线程A进⾏CAS操作发现 内存中仍然是3,然后线程A操作成功。
尽管线程A的CAS操作成功,但是不代表这个过程就是没有问题的。
有这样的需求,⽐如CAS,只注重头和尾,只要⾸尾⼀致就接受。
但是有的需求,还看重过程,中间不能发⽣任何修改,这就引出了
4.1.AtomicReference 原⼦引⽤。
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<User> 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)
}
}
4.2.ABA问题的解决(AtomicStampedReference 类似于时间戳)
使⽤ AtomicStampedReference
类可以解决ABA
问题。这个类维护了⼀个“版本号”Stamp
,在进⾏CAS
操作的时候,不仅要⽐较当前值,还要⽐较版本号。只有两者都相等,才执⾏更新操作。
解决ABA问题的关键⽅法:
参数说明:
V expectedReference, 预期值引⽤
V newReference, 新值引⽤
int expectedStamp,预期值时间戳
int newStamp, 新值时间戳
4.2.1 ABA问题的产生演示
public class AtomicReferenceDemo2 {
static AtomicReference<Integer> 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(() -> {
//CAS
System.out.println(atomicReference.compareAndSet(100, 2020) + "\t" + atomicReference.get());
}, "t2").start();
}
4.2.2.ABA问题的解决
package thread;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicStampedReference;
/**
* ABA问题
*/
public class ABADemo {
//带有时间戳的原子引用 (共享内存值100, 版本号为1)
static AtomicStampedReference<Integer> 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,时间戳为3
try {
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
}
}