CAS--Compare And Swap
Compare And Swap(比较并替换)
一、作用
我们的CAS是什么,用来干嘛的呢??
首先CAS就是一个值替换的操作,可以类比于java中的“=”号赋值操作,相当于修改变量的值!可是,既然“=”号可以实现,CAS又有什么意义呢?
正常单线程的情况下,确实没有必要使用CAS,可是,如果是高并发情况,如果不仅仅只是修改这个值,而是做一个“++“”的累加操作,用于计数(先跳过所谓的volatile,这里不引入这个话题,后续会写一下volatile)!
在“++”的计数过程中,我希望达到的效果是,无论同时有多少个线程来执行这个++的操作,我都能够保证,其结果是原子性的(就是有多少次++,最后结果就是多少)!
public class MyCasExample {
private static int state=0;
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(100);
for (int i = 0; i <100 ; i++) {
new Thread(()->{
for (int j = 0; j <100 ; j++) {
MyCasExample.state++;
}
// CountDownLatch类,可以理解成火箭发射,需要等待其他准备工作完成,完成一个任务(线程), 每次countDown就会减一,直到为0后,再统一释放所有线程!
countDownLatch.countDown();
}).start();
}
//这里为等待上面countDown收集线程,等上面减完,则会同时释放所有线程
countDownLatch.await();
System.out.println(MyCasExample.state);
}
}
代码介绍:对静态属性state进行计数操作!为了模仿高并发场景,使用了工具类CountDownLatch(倒计时器),最后会呈现出100个线程,同时释放执行100次++的场景!不过并非每次结果都不对,需要多试试!
通过这段代码,我们了解到了高并发场景下,++计数的操作,会导致什么问题!不过肯定有人说,那synchronized或者Lock加锁,不是也能解决哇?
我这里补充一句:CAS是无锁的方式,没有锁竞争(阻塞)带来的开销,也没有线程间频繁调度切换(与内核线程之间的切换执行)带来的开销,它比基于锁的方式有更优越的性能!
二、定义:
在JUC中,很多地方都用到的CAS,比如AQS中,更是处处存在,使用到了极致!同时原子包中atomic包的原子类也有很多基于CAS实现的方法!
有一句话解释这个:在高并发情况下,对一个变量用不加锁的方式来实现原子性操作!
1、什么是CAS机制
CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值N,要修改的新值B。
更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。
当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其他线程都失败,失败的线程并不会被挂起,而是通过自旋等待,同时被告知竞争失败,并可再次尝试!它是基于硬件平台的汇编指令。
2、CAS图解
CAS是非常非常重要的,如果CAS搞不清楚,那么AQS源码也必然看不懂!因为CAS常见于AQS(AbstractQueuedSynchronizer)和原子类中,同时CAS是Unsafe魔术类中的native修饰的方法!
如果有下载Openjdk源码,可以在,Hotspot中查看到具体的C代码!CAS底层使用JNI调用C代码实现的,可以在Unsafe.cpp里可以找到它的实现:
(以下部分可以不看!后方有链接,感兴趣可以去看看!)—————————————————————————————————————————————
static JNINativeMethod methods_15[] = {
//省略一堆代码...
{CC"compareAndSwapInt", CC"("OBJ"J""I""I"")Z", FN_PTR(Unsafe_CompareAndSwapInt)},
{CC"compareAndSwapLong", CC"("OBJ"J""J""J"")Z", FN_PTR(Unsafe_CompareAndSwapLong)},
//省略一堆代码...
};
我们可以看到compareAndSwapInt实现是在Unsafe_CompareAndSwapInt里面,再深入到Unsafe_CompareAndSwapInt:
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* addr = (jint *) index_oop_from_field_offset_long(p, offset);
return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END
p是取出的对象,addr是p中offset处的地址,最后调用了Atomic::cmpxchg(x, addr, e), 其中参数x是即将更新的值,参数e是原内存的值。
(以上代码可以不用细看)—————————————————————————————————————————————
作为java开发,没必要研究更深入,我们用图形理解CAS的原理即可!
我们来看看这个图:我们用简单易懂的话来讲:首先,我们这个CAS的操作,会优先从内存中拿到V=4,也就是我们想要操作的那个初始值!复制给N变量,N=4,然后执行我的业务代码,计算后,比如‘’++‘’2次后,得到6的结果,我们赋值给B,则B=6;此时,我们就需要把这个B=6,重新写回内存中,修改V=6!
此时!!!注意了,CAS会多一步判断,写这个代码的人认为,我这中间执行计算方法的时候,也就是那0.01ms的时间内,万一内存中的值被别的线程先改了呢?也就是我们内存中的初始值,已经发生了改变,变成了V=5;
那么,我们的V到底是更新呢??还是更新呢??还更新呢?
肯定,必然,是不能更新的!
那么如何判断这个值变没有?
那就是用最开始复制过来的旧的预期值N=4,和内存的值V=4进行比较,如果说,他们两个值一样,说明运气好,没人动它!
否则,如果不相等……说明运气不太好!那不相等怎么办?
当前CAS自旋,持续去执行,直到成功为止!
(额外内容:cmpxchg 不具有原子性,lock指令在执行后面指令的时候锁定一个北桥电信号,当执行cmpxchg 其他cpu不允许做修改,所以lock cmpxchg具有原子性。
所以cas还是会上锁,不过锁定北桥信号(不采用锁总线的方式)比锁定总线轻量,这就很好的解释了同时写入问题。)CAS详细底层C语言源代码,访问这个链接去研究吧:https://blog.csdn.net/AAA17864308253/article/details/105524056)。
3、ABA问题
这里是展示ABA的问题,也就是说,实际上我的原子类数据 1 ,已经改变过了!从1变为2 ,又改成1!可是,我们的cas代码还是正常执行,判断为原子类没有发生改变!产生了ABA的问题!对于数据来说,没什么,但是作为业务严谨性,明明发生了改变了,我们不能把这个1当成最初的1!也就是在开发中,不能说1一定等于1!(以下代码需要多执行几次,并非一次就能出现ABA问题,可能还会有CAS修改失败的问题!)
public class AtomicAbaProblemTest {
static AtomicInteger atomicInteger = new AtomicInteger(1);
public static void main(String[] args) {
Thread mainThread = new Thread(new Runnable() {
@Override
public void run() {
int a = atomicInteger.get();
System.out.println("操作---"+Thread.currentThread().getName()+"--修改前原子数值:"+a);
try {
//把自己睡着,给别人机会
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
//这里执行cas方法,看运气,如果原子类没变成1,则修改失败
boolean isCasSuccess = atomicInteger.compareAndSet(a,3);
if(isCasSuccess){
System.out.println("操作---"+Thread.currentThread().getName()+"--Cas修改后原子数值:"+atomicInteger.get());
}else{
System.out.println("CAS修改失败");
}
}
},"主线程");
Thread interfThread = new Thread(new Runnable() {
@Override
public void run() {
atomicInteger.incrementAndGet();// 1+1 = 2;
System.out.println("操作---"+Thread.currentThread().getName()+"--increase增加后的值:"+atomicInteger.get());
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicInteger.decrementAndGet();// atomic-1 = 2-1;
System.out.println("操作---"+Thread.currentThread().getName()+"--decrease减少后的值:"+atomicInteger.get());
}
},"干扰线程");
mainThread.start();
interfThread.start();
}
}
那么我们如何判断出这个1不等于原来的1呢?
这里要用一个新的原子类!AtomicStampedReference,如果想知道详细的,延伸学习!
public class AtomicStampedRerenceTest {
private static AtomicStampedReference<Integer> atomicStampedRef =
new AtomicStampedReference<>(1, 0);
public static void main(String[] args){
Thread mainThread = new Thread(() -> {
//获取当前标识也就是0,用来识别是否产生ABA的问题!
int stamp = atomicStampedRef.getStamp();
System.out.println("操作主线程:" + Thread.currentThread()+
"stamp="+stamp + ",初始值 a = " + atomicStampedRef.getReference());
try {
Thread.sleep(1000); //等待1秒 ,以便让干扰线程执行
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean isCASSuccess = atomicStampedRef.compareAndSet(1,2,stamp,stamp +1); //此时expectedReference未发生改变,但是stamp已经被修改了,所以CAS失败
System.out.println("操作主线程:" + Thread.currentThread() +
"stamp="+stamp + ",CAS操作结果: " + isCASSuccess+",值a="+atomicStampedRef.getReference());
},"主操作线程");
Thread interfThread = new Thread(() -> {
int stamp = atomicStampedRef.getStamp();
//第一个参数为初始值,第二个为新值,第三个为标识,第四个参数为新标识
atomicStampedRef.compareAndSet(1,2,stamp,stamp+1);
System.out.println("操作干扰线程:" + Thread.currentThread() + "stamp="+atomicStampedRef.getStamp() +"," +
"【increment】" +
" ,值a = "+ atomicStampedRef.getReference());
//每操作一遍,stamp+1
stamp = atomicStampedRef.getStamp();
atomicStampedRef.compareAndSet(2,1,stamp,stamp+1);
System.out.println("操作干扰线程:" + Thread.currentThread() + "stamp="+atomicStampedRef.getStamp() +"," +
"【decrement】" +
" ,值a = "+ atomicStampedRef.getReference());
},"干扰线程");
mainThread.start();
interfThread.start();
}
这段代码,加了标识后的原子类,可以通过额外的标识进行标记!只要标识不同,则不能进行CAS计算!