什么是CAS
Compare And Set 比较并交换
CAS的全称为Compare-And-Swap,它是一条CPU并发原语。
它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。
package com.lxj.interview2;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 1、CAS是什么--》compareAndSet 比较并交换
*
* */
public class CASDemo {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(5);
System.out.println(atomicInteger.compareAndSet(5,2020)+"\t current data: "+atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(5,1024)+"\t current data: "+atomicInteger.get());
atomicInteger.getAndIncrement(); //底层思想就是CAS,比较并交换
}
}
输出结果为:
true current data: 2020
false current data: 2020
CAS底层原理
Cas底层原理?如果知道,谈谈你对UnSafe的理解
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
//getIntVolatile用来找主内存中的值
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
//compareAndSwapInt这个方法的作用是,用var1和var2获取线程工作内存中的值,然后与var5(主内存中的值)比较,如果相同更新var5 = var5+var4,并返回TRUE,也即while中为FALSE,退出do--while循环
//如果获取的工作内存中的值和主内存中的值不一样,返回FALSE,也即while中为TRUE,继续执行do中的代码块,重新去取主内存中的值,再进行比较。
return var5;
}
UnSafe.getAndAddInt()源码解释:
var1 AtomicInteger对象本身。
var2 该对象值得引用地址。
var4 需要变动的数量。
var5是用过var1,var2找出的主内存中真实的值。
用该对象当前的值与var5比较:
如果相同,更新var5+var4并且返回true,
如果不同,继续取值然后再比较,直到更新完成。
假设线程A和线程B两个线程同时执行getAndAddInt操作(分别跑在不同CPU上) :
Atomiclnteger里面的value原始值为3,即主内存中Atomiclnteger的value为3,根据JMM模型,线程A和线程B各自持有一份值为3的value的副本分别到各自的工作内存。
线程A通过getIntVolatile(var1, var2)拿到value值3,这时线程A被挂起。
线程B也通过getintVolatile(var1, var2)方法获取到value值3,此时刚好线程B没有被挂起并执行compareAndSwapInt方法比较内存值也为3,成功修改内存值为4,线程B打完收工,一切OK。
这时线程A恢复,执行compareAndSwapInt方法比较,发现自己手里的值数字3和主内存的值数字4不一致,说明该值己经被其它线程抢先一步修改过了,那A线程本次修改失败,只能重新读取重新来一遍了。
线程A重新获取value值,因为变量value被volatile修饰,所以其它线程对它的修改,线程A总是能够看到,线程A继续执行compareAndSwaplnt进行比较替换,直到成功。
CAS的缺点
1、循环时间长开销很大
// ursafe.getAndAddInt
public final int getAndAddInt(Object var1, long var2, int var4){
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
}while(!this.compareAndSwapInt(varl, var2, var5,var5 + var4));
return var5;
}
可以看到getAndAddInt方法执行时,有个do while,如果CAS失败,会一直进行尝试。如果CAS长时间一直不成功,可能会给CPU带来很大的开销。
2、只能保证一个共享变量的原子操作
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是,对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性。
3、引出来ABA问题
ABA问题
CAS会导致“ABA问题”。
CAS算法实现一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差类会导致数据的变化。
比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且线程two进行了一些操作将v位置的数据变成了B,然后线程two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后线程one操作成功。
原子引用AtomicReference
package com.lxj.interview2;
import java.util.concurrent.atomic.AtomicReference;
public class AtomicReferenceDemo {
public static void main(String[] args) {
User z3 = new User("z3",25);
User l4 = new User("l4",22);
AtomicReference<User> atomicReference = new AtomicReference<>();
atomicReference.set(z3);
System.out.println(atomicReference.compareAndSet(z3,l4)+"\t"+atomicReference.get().toString());
System.out.println(atomicReference.compareAndSet(z3,l4)+"\t"+atomicReference.get().toString());
}
}
class User{
String userName;
int age;
public User(String userName, int age) {
this.userName = userName;
this.age = age;
}
@Override
public String toString() {
return "User{" +
"userName='" + userName + '\'' +
", age=" + age +
'}';
}
}
运行结果:
true User{userName='l4', age=22}
false User{userName='l4', age=22}
AtomicStampedReference版本号原子引用(ABA的解决方法)
原子引用 + 新增一种机制,那就是修改版本号(类似时间戳),它用来解决ABA问题。
ABA问题的解决代码
package com.lxj.interview2;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;
public class ABADemo {
/**
* 普通的原子引用包装类
* */
static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
/**
* 带有版本号的原子引用包装类
* */
static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100,1);
public static void main(String[] args) {
System.out.println("======以下是ABA问题的产生=======");
new Thread(()->{
//把主内存中的100改成101然后改成100,也就是ABA操作
atomicReference.compareAndSet(100,101);
atomicReference.compareAndSet(101,100);
},"t1").start();
new Thread(()->{
try {
//睡眠1秒保证t1完成ABA操作
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(atomicReference.compareAndSet(100,2022)+"\t"+atomicReference.get());
},"t2").start();
//暂停2s,让上面两个线程执行完毕
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("======以下是ABA问题的解决=======");
new Thread(()->{
//当前版本号
System.out.println(Thread.currentThread().getName()+"\t 第一次版本号:"+atomicStampedReference.getStamp());
//暂停t3一秒,使t4拿到初始版本号
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
//传入四个值:期望值、更新值、期望版本号、更新版本号
atomicStampedReference.compareAndSet(100,101,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName()+"\t 第二次版本号:"+atomicStampedReference.getStamp());
//修改回去
atomicStampedReference.compareAndSet(101,100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName()+"\t 第三次版本号:"+atomicStampedReference.getStamp());
},"t3").start();
//目标,t4线程在期望版本号不一致的情况下无法修改主内存中的值
new Thread(()->{
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName()+"\t 第一次版本号:"+stamp);
//暂停t4线程3秒,让t3完成ABA
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean result = atomicStampedReference.compareAndSet(100,2022,stamp,stamp+1);
System.out.println(Thread.currentThread().getName()+"\t 是否修改成功:"+result+"\t 当前实际版本号:"+atomicStampedReference.getStamp());
System.out.println(Thread.currentThread().getName()+"\t 当前实际值:"+atomicStampedReference.getReference());
},"t4").start();
}
}
运行结果:
======以下是ABA问题的产生=======
true 2022
======以下是ABA问题的解决=======
t3 第一次版本号:1
t4 第一次版本号:1
t3 第二次版本号:2
t3 第三次版本号:3
t4 是否修改成功:false 当前实际版本号:3
t4 当前实际值:100