CAS
compare and swap
一个特殊的 CPU 指令 完成的工作,就是 "比较和交换"
这个 cpu 指令本身就是"原子的";
所以基于 cas 指令:给编写线程安全的代码打开了新的大门;
之前线程安全都是靠 加锁 ;导致的结果就是 阻塞===> 性能降低;
使用 CAS ,不仅不涉及加锁,也不会阻塞,合理使用也能保证线程安全(无锁编程)
CAS 本身是 CPU 指令;
操作系统 对 指令 进行了 封装;
jvm 又对 操作系统提供的 api 又封装了一层;
java 中 cas 的 api 放到了 unsafe包(不安全的)里(涉及到一些底层内容,使用不当会带来风险);
java标准库,对于 CAS 又进行了进一步的封装,提供了一些工具类;最主要的一个工具类,叫做 "原子类"

上述代码就处于 "线程不安全"的情况;
解决这个问题的第一个方法就是 给 写操作 加锁;
第二个方法就是 使用原子类;

这里的内容,就没有加锁,也能保证线程安全;
之前的 count++ 是cpu的 三个指令;
多个线程的写操作是三个指令,会相互穿插执行,引起线程不安全;
此处的 getAndIncrement 对变量的修改,是一个 CAS 指令;
一个指令就是原子的;加锁就是为了将三个指令 变成 一个指令 让它成为原子的;
不使用 原子类 的 写操作;

使用了 原子类 的写操作

CAS 中的 ABA 问题
CAS 在使用的时候 关键要点是要 判定当前内存是否和寄存器中的值一样,如果一样就进行修改,不一样就什么都不做
假设有两个线程 A 和 B 同时对同一个共享变量执行 CAS 操作:
线程 A 执行 CAS 操作:
线程 A 从内存读取当前值A,并设定预期值E为A(假设A的值为 10)。
线程 A 计算出新值N(比如N = E + 1,即 11)。
线程 A 执行 CAS 操作,比较内存处的值(也就是A)是否等于预期值E。如果相等,就将内存的值修改为新值N(即把 10 改成 11),操作成功。
线程 B 执行 CAS 操作:
线程 B 同样从内存地址V读取当前值,假设线程 B 读取时,线程 A 还未完成 CAS 操作,所以线程 B 读取到的值也是 10,它也设定预期值E为 10,并计算出新值N(同样为 11)。
当线程 B 执行 CAS 操作时,如果此时线程 A 已经完成了 CAS 操作,那么内存地址V处的值已经变为 11,而线程 B 的预期值E还是 10,两者不相等,线程 B 的 CAS 操作失败,不会修改内存地址V的值。
线程 B 失败后,根据具体的业务逻辑,可以选择重试,即重新读取内存值作为预期值,重新计算新值,再次执行 CAS 操作;也可以选择放弃本次操作,执行其他逻辑。
ABA 问题
存在一种情况:内存 V 存储的数值是 0;
执行CAS 之前,另一个线程把这个值从 0 --> 10 又从 10--> 0;
在其他前程穿插的过程中,将 内存v的值 从 A 改到 B 又改回 A;
此时 CAS 并不知道 修改了值,这种情况就是 : ABA 问题
一般来说是不会出现问题
但对于 ABA 问题来说,什么时候会出现 BUG 呢?
假设,我在银行向另一个账户转账 100元 ,当我按下转账按钮时,没有反应;然后又按了一次;此时产生了两个线程,去尝试进行扣款操作(假定按照 CAS 的方式进行扣款):
线程B 先扣款成功,线程A 发现钱对不上,就不进行操作;
但如果此时在 线程B 扣款成功的同时,线程C 向 我的账户转账了 100元;此时 线程A 就不知道 账户没有转账成功 还是 转账成功,又到账了;
对于 ABA 问题的解决方案:
1)约定数据变化时单向的(只能增加或者只能减少),不能时双向的(又是双向又是单向);
2)对于本身就必须要双向变化的数据(账户转账收款),可以引入一个 版本号-->版本号这个数字就是只能增加,不能减少;(转账1-->收款2--->转账3)
CAS本质上时 JVM 帮我们封装好的;