比较、交换(compareAndSet),它是一条cpu并发原语 它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的
一、AtomicInteger实例
public class CASDemo {
public static void main(String[] args) {
AtomicInteger i = new AtomicInteger(0);
// 先比较,如果是i等于期望值0,则将其置为2021
System.out.println(i.compareAndSet(0,2021));
System.out.println(i.getAndIncrement());
}
}
T1拿到了5,并改成了2019,想要写回主存,此时先将自己的期望值5,和主存的值进行比较,如果主存也是5,则对其进行更新。
二、CAS底层原理
思考i.getAndIncrement(),为什么没有加synchronized也能保证原子性
CAS并发原语体现在JAVA语言中就是sun.misc.Unsafe类中的各个方法。调用UnSafe类中的CAS方法,JVM会帮我们实现CAS汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。再次强调,由于CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。
接下来以i.getAndIncrement为例进行讲解
getAndIncrement()底层实现是用的unsafe.getAndAddInt。
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
// var1 当前对象本身,var2位移偏移量,var4需要增加的值,var5 内存中的真是值
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
- Unsafe
是CAS的核心类,由于java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为java中CAS操作的执行依赖于Unsafe类的方法 。
unsafe中所有的方法都是native修饰的,也就是说unsafe类中的方法都可以直接调用操作系统底层资源执行相应的任务。
- valueOffset
当前对象的内存偏移量,在static块中获取。
- value
AtomicInteger i= new AtomicInteger(value);
这里value被volatile修饰,对value的修改其他线程是可见的。
- var5 = this.getIntVolatile(var1, var2)
表示获取当前对象的所在位移偏移量的值,相当于拷贝主存中的值到自己的工作内存中。
- this.compareAndSwapInt(var1, var2, var5, var5 + var4)
当前对象的值(var1+var2 当前对象加上内存偏移量就能知道当前对象的值)如果和var5一样,则var5+var4,即当前值+1。
- while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4))
当前对象的值如果和期望值一样就跳出循环,否则不停的循环对比快照值和当前对象的值。
三、Unsafe
Unsafe类中的compareAndSwapInt,是一个本地方法,该方法的实现位于unsafe.cpp中,了解即可。
四、CAS的缺点
- 循环时间开销大
- 只能保证一个共享变量的原子操作,当需要对多个共享变量操作时,循环CAS就无法保证操作的原子性,需要加锁来保证。
- 引出来的ABA问题(什么是ABA问题,后面讨论)
五、AtomicInteger的ABA问题
狸猫换太子
CAS算法实现的一个重要前提需要取出内存中的某个时刻的数据并在当下时刻比较并替换,那么这个时间差类会导致数据的变化。
问题描述
以下两个流程结合一起看
- 线程A: 从内存中取出A ————————————->进行CAS操作,发现内存中还是A,然后one操作成功。
- 线程B: 从内存中取出A —> 将A变成了B —> 将B变成了A
上述线程A确实CAS操作成功了,但是此A非彼A了。
对于只关心结果不关心过程的场景,无所谓。但是对于注重过程的场景,就有问题了。
六、解决ABA问题
理解原子应用+新增一种机制,那就是修改版本号(类似于时间戳)
- 线程A: 值A version:1 ———————————————————->此时想要操作,先CAS,期望值A,符合。然后提交修改后的值C version:2,发现version的2弱于当前的version:3,提交失败
- 线程B: 值A varsion:1 ——->修改为值B version:2 —>修改为值A verison:3
1、ABA问题的产生演示(原子更新引用展示)
AtomicReference 原子更新引用
public class ABADemo {
static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
public static void main(String[] args) {
new Thread(()->{
System.out.println(atomicReference.compareAndSet(100,101)+"--"+atomicReference.get());
System.out.println(atomicReference.compareAndSet(101,100)+"--"+atomicReference.get());
},"A").start();
// B 线程并不知道A的过程发生了什么,只知道A的值是100,并不知道A中途变成过101
new Thread(()->{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(atomicReference.compareAndSet(100,2021)+"--"+atomicReference.get());
},"B").start();
}
}
}
2、ABA问题的解决
public class ABADemo2 {
static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1);
public static void main(String[] args) {
new Thread(() -> {
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "-第一次版本号:" + stamp);
// 暂停一秒A线程,等B线程拿到这个版本号
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicStampedReference.compareAndSet(100, 101, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + "-第二次版本号:" + atomicStampedReference.getStamp());
atomicStampedReference.compareAndSet(101, 100, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + "-第三次版本号:" + atomicStampedReference.getStamp());
}, "A").start();
new Thread(() -> {
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "-第一次版本号:" + stamp);
// 暂停三秒B线程,保证上面的A线程完成一次ABA操作
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean result = atomicStampedReference.compareAndSet(100, 2021, stamp, stamp + 1);
System.out.println(Thread.currentThread().getName() + "修改成功与否:" + result + "第二次版本号:" + atomicStampedReference.getStamp());
System.out.println(Thread.currentThread().getName() + "-当前实际最新值:"+atomicStampedReference.getReference());
}, "B").start();
}
}
输出结果:
A-第一次版本号:1
B-第一次版本号:1
A-第二次版本号:2
A-第三次版本号:3
B修改成功与否:false,第二次版本号:3
B-当前实际最新值:100
可以看到,上述代码解决了ABA问题,加入版本号(时间戳)控制之后,当B的更新后提交时(100改成2021,时间戳2),没有提交成功,因为此时时间戳已经是3了,2小于3,不给执行。所以当前的公共资源值atomicStampedReference还是100。
当修改上诉代码如下:
修改代码如下,期望的时间戳是stamp+2,即3(与上面的最新时间戳保持一致)时,即可需改成功。
boolean result = atomicStampedReference.compareAndSet(100, 2021, stamp + 2, stamp + 1);
七、总结
1、为什么用cas而不用synchronized
synchronized:同一时段只允许有一个线程来访问,一直性得到了保证,但并发性下降。
cas:没有加锁,通过比较来确认是否修改数据,即提供 了一致性,也提高了并发性。
2、Unsafe类+CAS思想是什么
自旋,什么是自旋???后期学习。
3、CAS简单理解
比较当前工作内存中的值和主内存中的值,如果相同则执行操作,否则继续比较直到主内存和工作内存中的值一致为止。
4、CAS应用
CAS有三个操作数,内存值V,旧的预期值A,需要修改的更新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
5、CAS的缺点
见上面,只能保证一个共享变量的原子操作,循环消耗大,ABA问题。
6、CAS产生的ABA问题
ABA问题的解决可以使用版本号(时间戳)解决。