一:什么是CAS?
CAS,在Java并发应用中通常指CompareAndSwap或CompareAndSet,即比较并交换。
CAS是一个原子操作 是用于实现多线程同步的原子指令,它比较一个内存位置的值并且只有相等时修改这个内存位置的值为新的值,保证了新的值总是基于最新的信息计算的,如果有其他线程在这期间修改了这个值则CAS失败。CAS返回是否成功或者内存位置原来的值用于判断是否CAS成功。
这里我们用一段短代码来说明:
这里我们用AtomicInteger类来做测试,其底层也是用CAS原理实现的 ,这个类中有一个compareAndSet(int except,int update)—>比较并交换 !,传入的参数一个是期望,一个是达到期望后所更新的值。
import java.util.concurrent.atomic.AtomicInteger;
/**
* @program: juc
* @description
* @author: 不会编程的派大星
* @create: 2021-04-28 20:48
**/
public class CasTest {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(1000);
//先比较是否达到期望值,达到就更新,这里达到1000,所以更新值为1200
System.out.println(atomicInteger.compareAndSet(1000, 1200));
System.out.println(atomicInteger.get());
//这里会打印false,因为执行到这里的时候,值已经为1200了,比较发现没达到1000的期望值,所以不更新
System.out.println(atomicInteger.compareAndSet(1000, 1200));
System.out.println(atomicInteger.get());
}
}
执行结果:
注:可以看到,当第二次调用compareAndSet方法的时候,这里打印的是false,即交换失败,因为执行到这里的时候,值已经为1200了,比较发现没达到1000的期望值,所以不更新,即值还是1200
CAS就可以这样理解:如果我期望的值达到了,那么就更新,否则, 就不更新, CAS 是CPU的并发原语!
透过其实现细节来看,我们可以看到原子类是由unsafe类的compareAndSwapInt方法实现的?
那么unsafe类究竟是什么呢?
二、unsafe类以及compareAndSwap方法
**unsafe:**我们都知道java是无法直接操作内存的,但是java可以调用c++去操作内存,也就是我们常说的native方法,这里的unsafe类可以说是java留的后门,可以通过这个类操作内存!
注:valueOffset:内存地址偏移值 ,这里value加了volatile,保证了其可见性,和禁止指令重排!
我们通过atomicInteger中的getAndAddInt方法来一探究竟吧
注:这里就让我们来详细说明一下各个参数吧
var1也就是当前的对象,var2也就是当前内存的偏移量,delta就是需要加的数值,再往下看,var5也就是当前对象var1偏移var2后的值,也就是说var5是获取当前对象内存中的值,再往下看,最重要的compareAndSwapInt来了,这里我们分隔一下这几个参数:
第一个参数1就是获取当前对象var1内存偏移var2的值,然后比较参数1和参数2,比较是否相等,也就是上面提到的能不能达到我们的except,如果期望达到了,就更新,更新为参数3,也就是在原来的值的基础上在加上一个新的数var4,所以归更到底还是 先比较再更新的思想 。还有一点,这里的do–while相当于一个自旋锁,一直旋转,直到成功为止。
这里是直接操作内存,所以效率极高!可比lock锁和synchronized锁高多了!
*嗯~~~~~妙不可言!*
讲到这里,我们来总结下看看CAS到底是什么吧?
CAS:比较当前工作内存中的值和主内存中的值,如果这个值是期望的,那么则执行操作!如果不是就
一直循环!
但还是存在缺点:
1.因为自旋锁的存在,循环胡耗时!
2.一次性只能保证一个共享变量的原子性
3.ABA问题
你以为面试官问你到这儿就不问了,那你可错了,都到unsafe了,不再问你两句ABA问题是怎么解决的?
三、ABA问题
ABA 问题(狸猫换太子)
图解:
注:这边B线程先通过CAS将1改为3,再将3改为1,此时A线程在去改,虽然能改成功,但是A这边是不知道B这边已经修改过数据了,而我们期望的效果是A在修改之前是已经知道B修改过值了
简单代码实现:
import sun.plugin2.message.GetAppletMessage;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @program: juc
* @description
* @author: 不会编程的派大星
* @create: 2021-04-29 19:37
**/
public class AbaTest {
public static void main(String[] args) {
//初始值为10
AtomicInteger atomicInteger = new AtomicInteger(10);
System.out.println(atomicInteger.get());
//模拟捣乱线程 先将10 改为15,在将15改回10
System.out.println(atomicInteger.compareAndSet(10, 15));
System.out.println(atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(15, 10));
System.out.println(atomicInteger.get());
//正常线程 将10改为15
System.out.println(atomicInteger.compareAndSet(10, 15));
System.out.println(atomicInteger.get());
}
}
注:这里即使正常线程CAS成功了,但实际开发中这种问题可能会造成很严重的问题!
那应该怎么解决呢?
四、原子引用解决ABA问题
原子引用对应的思想其实就是乐观锁!
接着上面的问题继续
简单代码实现:
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicStampedReference;
/**
* @program: juc
* @description
* @author: 不会编程的派大星
* @create: 2021-04-29 19:42
**/
public class LgAbaTest {
public static void main(String[] args) {
//这里的initialStamp可以理解为版本号,每成功修改一次,版本号变一次,我们这里设置+1
AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(10,1);
System.out.println(" 原始stamp == " +atomicStampedReference.getStamp());
new Thread(() -> {
System.out.println("A 刚拿到的stamp == "+atomicStampedReference.getStamp());
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(atomicStampedReference.compareAndSet(10, 15, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1));
System.out.println("A 第一次修改后的stamp == " +atomicStampedReference.getStamp());
System.out.println(atomicStampedReference.compareAndSet(15, 10, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1));
System.out.println("A 第二次修改后的stamp == " +atomicStampedReference.getStamp());
System.out.println("A 修改完后的stamp == " + atomicStampedReference.getStamp());
},"A").start();
new Thread(() -> {
int stamp = atomicStampedReference.getStamp();
System.out.println("B 刚拿到的stamp == "+stamp);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(atomicStampedReference.compareAndSet(10, 12,stamp, stamp+1));
System.out.println("B 修改完后的stamp == " + atomicStampedReference.getStamp());
},"B").start();
}
}
执行结果:
因为在B线程CAS的时候,此时stamp的版本已经来到3了,而它所期望的stamp为1 ,所以这里即使它期望的值为10没有错,但因为乐观锁的存在,stamp没到到期望值,set不了 ,返回false
各种乐观锁的实现中通常都会用版本戳version来对记录或对象标记,避免并发操作带来的问题,在Java中,AtomicStampedReference也实现了这个作用,它通过包装[E,Integer]的元组来对对象标记版本戳stamp,从而避免ABA问题,例如下面的代码分别用AtomicInteger和AtomicStampedReference来对初始值为100的原子整型变量进行更新,AtomicInteger会成功执行CAS操作,而加上版本戳的AtomicStampedReference对于ABA问题会执行CAS失败
正确情况下:把B线程里面的CAS改为:
System.out.println(atomicStampedReference.compareAndSet(10, 12,atomicStampedReference.getStamp(), atomicStampedReference.getStamp()+1));
执行结果:
在Java中,AtomicStampedReference类就实现了用版本号作比较额CAS机制。
1. java语言CAS底层如何实现?
利用unsafe提供的原子性操作方法。
2.什么事ABA问题?怎么解决?
当一个值从A变成B,又更新回A,普通CAS机制会误判通过检测。
利用版本号比较可以有效解决ABA问题。
这次的讨论就到这里,欢迎小伙伴们留言讨论!