什么是CAS(Compare and swap - 比较并交换)
CAS的全称为 compare and swap 它是一条CPU 并发原语
什么叫做CPU并发原语
原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU原子指令,不会造成所谓的数据不一致问题
CAS的功能是什么
判断内存某个位置的值是否为预期值,如果是 则更改为新的值,这个过程是原子的 是通过unsafe类来保证原子性
CAS的底层原理是什么, 为什么可以保证原子性
- 自旋锁
- unsafe类
什么是UnSafe类,UnSafe类的作用是什么
是什么
:是CAS的核心类,Unsafe相当于一个后门,基于该类可以直接操作操作系统中特定内存的数据。UnSafe类存在于 sun.misc 包中,
为什么出现
:java方法无法直接访问底层系统(只要是native修饰的方法,java都无能为力)
作用
:通过UnSafe类操作操作系统中特定内存的数据 (其内部方法操作可以像 C 的指针一样直接操作内存)
因为 java 中 CAS 操作的执行依赖于 UnSafe 类的方法
UnSafe类有什么特点
UnSafe类中的所有方法都是native修饰的,也就是说 Unsafe类中的方法都直接调用操作系统底层资源执行相应任务
使用atomic包中AtomicInteger下的 2个方法理解CAS原理
1、详解:AtomicInteger中的 compareAndSet ( int expect, int update ) 方法
现在有A,B,C 个线程操作主物理内存中的共享变量,当 A 线程操作这个共享变量的时候,
- 情况1:对 A 来说最开心的事情就是这个共享变量没有被 B, C 线程修改过,可以直接将在自己工作内存中操作后的值替换掉主内存中的值
- 情况2:对 A 线程不开心的事情是 B,C 线程 已经把主内存中的共享变量修改了,那么此时 A 线程发现共享变量的值和我预期的初始值不同,就不能修改共享变量中的值了
compareAndSet实现了上面的2中情况,返回的是 boolean类型,
参数意义:
- expect是我预期的值(共享变量的原始值,A的工作线程中没有操作时的值)
- update 如果共享变量中的值没有被 B,C线程修改,那么就将共享变量中的值替换成 update这个值
方法执行情况:
1. 如果expect和我预料中的一样,那么 compareAndSet操作`执行成功`,主内存中最终的值 是这个update
2. 如果expect和无预料中的不一样,那么 compareAndSet 操作`不会执行`,主内存中的值是其他线程修改后的那个值
public class CASTest {
public static void main(String[] args) {
//主内存中 atomicInteger 的初始值为 5
AtomicInteger atomicInteger = new AtomicInteger(5);
// 如果初始值是5,那么将初始值修改为1024,然后得修改后的值
System.out.println(atomicInteger.compareAndSet(5, 1024) + "主内存中的最终值为: " + atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(5, 2048) + "主内存中的最终值为: " + atomicInteger.get());
}
}
可以看到 最终的操作结果
2、详解AtomicInteger中 getAndIncrement() 方法
this:当前对象
valueOffSet:当前对象的内存地址偏移量,就是this的内存地址
什么叫做内存偏移量:
类似于在一个班级中,某个同学的座位 有具体的列,具体的行,那么这一列 这一行就一定存在一个人,我不管这个人是谁,我就是可以操作他,UnSafe类可以通过内存偏移地址获取数据 极度精确
这个方法的意思就是
:例如:对第三排第四列的这个同学进行 +1 的操作,我不管这个同学是谁,我只管对这个同学进行 + 1的操作
getAndIncrement() 中的 getAndAddInt(Object var1, long var2, int var4) 方法
- 创建一个返回对象
- getIntVolatile(var1, var2) 表示的是 this对象在 valueOffset上的值是多少,将这个值赋给var5
- var1:this
- var2:valueOffset
- 情况1. this.compareAndSwapInt(var1, var2, var5, var5 + var4) 表示的是:this对象中 var2 地址的值 和 var5相同的话 就将var5 + 1
- 取反是因为 如果修改成功的话 compareAndSwapInt方法就会返回 true 取反 为false 就跳出循环
- 情况2. 如果valueOffSet的地址值不是最新的,被其他线程修改过,那么就再取一次这个地址的最新的值,和var2 进行比较,如果不相等,就一直取,直到取到了最新的值,然后将这个值进行修改操作
生活中的例子:
在一个鸡蛋槽中有一个纸条 A ,现在多个人来操作这张纸条,取出这个鸡蛋槽中 第5个槽位()的纸条,将纸条用复印机复印一份,自己修改后,在将自己修改后的纸条替换上去,每个人都执行相同的操作 ------------ 前提条件:只有自己的复印的那一份和槽位中的纸条完全一致才可以进行替换 (先判断是否一致,然后执行替换操作)
情况1:线程 A 复印的和槽位中的一致:直接进行替换操作
情况2:线程 A 复印的和槽位中的不一致,那么就将这一份新的先复印一份,然后判断这份新的纸条和槽位中的纸条是否一致(多个线程修改,可能还是不一致),突然有一次,发现复印的纸条和槽位上的纸条一致,那么 就将自己修改好的纸条替换成槽位上的纸条
CAS的缺点是什么
- getAndInt方法中 如果比较的时候2个值一直不相等的话,就会给cpu带来很大的开销 — 上述例子中,如果我取鸡蛋槽中的纸条的时候,每次都和我复印的那个值不同 就会一直循环这个操作
- 只能保证一个共享变量的原子性 – 上述例子中 鸡蛋槽中每次只能盛放一个共享变量
- ABA问题
什么是ABA问题
现象:
背景:2个线程 A, B 操作内存中的共享变量 val,A操作这个 val 花费10s中,B操作这个 val 花费2s中,现在val的初始值是1A和B同时取出共享变量中的值
1、首先B线程 将这个1先修改成2,然后又将2修改回1,因为B线程的执行速度要快
2、然后A线程执行,发现共享变量中的数据和我取出时的数据一致,然后 A 线程将这个val 修改成了5
整体看下来,对A线程来说,共享变量中的值 看似没有变化,其实已经换了好几次了
举个生活中的例子:
A,B 2个人共同解一道数学题,A解这一题 花费10s中,B解这一题 花费2s中
1、首先 B 来操作这一题,然后将答案写在了试卷上,看了看答案,发现不对,然后有把刚刚写的答案删了,现在试卷上还是一片空白
2、A 发现 哎呦 B 线程还没有把这一题解出来呢,就自己把正确的答案写在了试卷上
虽然对B来说这一题并没有解出来,虽然最终的结果是 A解出来了这一题,但是试卷上是有 B 操作过的痕迹的
如果我不在意其中B线程是否修改过这个共享变量,那么就没问题,但是如果我介意这个值被修改过,那就不行
怎样解决ABA问题
使用时间戳原子引用
什么是原子引用
用法和 AtomicInteger都一样
AtomicInteger -- 对应的是 AtomicReference<Integer>
原子引用可以包装 类
什么是时间戳原子引用
思想:在修改的过程中 添加一个版本号,类似于git ,如果先修改为 A 然后 B 然后 A 虽然最终的值没有变化,但是我的版本号变了
代码示例:
public class AtomicStampedReferenceTest {
/**
* 加 static 是在main方法中使用
*/
static AtomicReference<Integer> atomicReference = new AtomicReference<>(5);
/**
* 初始值为5 初始版本为1
*/
static AtomicStampedReference<Integer> stampedReference = new AtomicStampedReference<>(5, 1);
public static void main(String[] args) {
//aa 线程实现 ABA问题
new Thread(() -> {
atomicReference.compareAndSet(5, 10);
atomicReference.compareAndSet(10, 5);
}, "aa").start();
//bb 线程对这个值进行修改操作
new Thread(() -> {
//等待1s种 是为了让aa 线程执行完成
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
//对共享变量进行修改
System.out.println(atomicReference.compareAndSet(5, 100) + ", atomicReference中的值为" + atomicReference.get());
}, "bb").start();
//上面是 ABA问题的产生###############3###############3###############3###############3###############3###############3
// 下面是ABA问题的解决, 使用时间戳原子引用
new Thread(() ->{
// 等2s 是上面的ABA 都执行完了 执行后续操作
try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
stampedReference.compareAndSet(5,10,1,2);
stampedReference.compareAndSet(10,5,2,3);
}, "cc").start();
new Thread(() ->{
//等3s 使用 stampedReference演示原子引用操作
try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println(stampedReference.compareAndSet(5, 100, 1, 2)
+ ", stampedReference的值为:" + stampedReference.getReference()
+ ", stampedReference的版本号为:" + stampedReference.getStamp()
);
}, "dd").start();
}
}