CAS失败—什么都不做
这个我就不再画图,说白了就是Z线程进来后,发现预期值和内存值不一样的时候,就什么都不做,就CAS失败,直接结束掉线程了。这个有些场景也会这么去干
CAS为什么是原子的呢?
有的人可能会问:CAS明明就有多部操作,但什么就是原子的呢?
解释如下:
Unsafe底层实际上是调用C代码,C代码调用汇编,最后生成出一条CPU指令cmpxchg,完成操作。这也就为啥CAS是原子性的,因为它是一条CPU指令,不会被打断。
CAS是原子性的,虽然你可能看到比较后再修改(compare and swap)觉得会有两个操作,但终究是原子性的!
CAS带来的ABA问题
什么是ABA问题呢?结束上面的例子
1.线程A和线程C同时读到count变量,所以线程A和线程C的内存值和预期值都为10
2.此时线程A使用CAS将count值修改成100
3.修改完后,就在这时,线程B进来了(因为CPU随机,所以是有可能先执行B再执行C的),读取得到count的值为100(内存值和预期值都是100),将count值修改成10
4.线程C拿到执行权,发现内存值是10,预期值也是10,将count值修改成11
产生的问题是:线程C无法得知线程A和线程B修改过的count值,这样是有风险的。,如下:
场景:蛋糕店回馈客户,对于会员卡余额小于20的客户一次性赠送20,刺激消费,每个客户只能赠送一次
public static void main(String[] args) { //在这里使用AtomicReference 里面装着用户的余额 初始卡余额小于20 final AtomicReference<Integer> money = new AtomicReference<>(19); //模拟一个生产者消费者模型 // 模拟多个线程更新数据库,为用户充值 for (int i = 0; i < 3; i++) { new Thread(() -> { while (true) { while (true) { Integer m = money.get(); if (m < 20) { if (money.compareAndSet(m, m + 20)) { System.out.println("余额小于20,充值成功。余额:" + money.get() + "元"); break; } } else { System.out.println("余额大于20,无需充值!"); break; } } try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); } // 用户消费进程,模拟消费行为 new Thread(() -> { //在这里的for循环,太快很容易看不到结果 for (int i = 0; i < 1000; i++) { while (true) { Integer m = money.get(); if (m > 10) { System.out.println("大于10元"); if (money.compareAndSet(m, m - 10)) { System.out.println("成功消费10,卡余额:" + money.get()); break; } } else { System.out.println("余额不足!"); break; } } try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); }
输出:
余额小于20,充值成功。余额:39元 余额大于20,无需充值! 余额大于20,无需充值! 大于10元 成功消费10,卡余额:29 大于10元 成功消费10,卡余额:19 大于10元 成功消费10,卡余额:9 余额小于20,充值成功。余额:29元 余额大于20,无需充值! 余额大于20,无需充值! 大于10元 成功消费10,卡余额:19 大于10元 成功消费10,卡余额:9 余额不足! 余额大于20,无需充值! 余额大于20,无需充值! 余额小于20,充值成功。余额:29元
我们看到,这个帐号先后反复多次进行充值。,怎么回事呢?
原因是帐户余额被反复修改,修改后的值等于原来的值,使得CAS操作无法正确判断当前的数据状态。这在业务上是不允许的(只有高并发下才可能会出现哦,并不是说记录下赠送次数就能简单解决的哦)。
ABA问题如何解决
其实java也考虑到了这个问题,所以提供给予我们解决方案了
我们可以使用JDK给我们提供的AtomicStampedReference和AtomicMarkableReference类。
用代码解决上面的充值问题:该动起来也是非常的简单
public static void main(String[] args) { //在这里使用AtomicReference 里面装着用户的余额 初始卡余额小于20 final AtomicStampedReference<Integer> money = new AtomicStampedReference<>(19, 0); for (int i = 0; i < 3; i++) { //拿到当前的版本号 final int timestamp = money.getStamp(); new Thread(() -> { while (true) { while (true) { Integer m = money.getReference(); if (m < 20) { //注意此处:timestamp版本号做了+1操作 if (money.compareAndSet(m, m + 20, timestamp, timestamp + 1)) { System.out.println("余额小于20,充值成功。余额:" + money.getReference() + "元"); break; } } else { System.out.println("余额大于20,无需充值!"); break; } } try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); } // 用户消费进程,模拟消费行为 new Thread(() -> { for (int i = 0; i < 100; i++) { while (true) { //拿到当前的版本号 int timestamp = money.getStamp(); Integer m = money.getReference(); if (m > 10) { System.out.println("大于10元"); if (money.compareAndSet(m, m - 10, timestamp, timestamp + 1)) { System.out.println("成功消费10,卡余额:" + money.getReference()); break; } } else { System.out.println("余额不足!"); break; } } try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); }
运行看输出结果为:
余额小于20,充值成功。余额:39元 余额大于20,无需充值! 余额大于20,无需充值! 大于10元 成功消费10,卡余额:29 大于10元 成功消费10,卡余额:19 大于10元 成功消费10,卡余额:9 余额不足! 余额不足! 余额不足! 余额不足!
我们发现,只为他充值了一次,之后一直消费都是余额不足的状态了。因此当高并发又可能存在ABA的情况下,这样就能彻底杜绝问题了
简单来说就是在给为这个对象提供了一个版本,并且这个版本如果被修改了,是自动更新的。原理大概就是:维护了一个Pair对象,Pair对象存储我们的对象引用和一个stamp值。每次CAS比较的是两个Pair对象
private static class Pair<T> { final T reference; final int stamp; private Pair(T reference, int stamp) { this.reference = reference; this.stamp = stamp; } static <T> Pair<T> of(T reference, int stamp) { return new Pair<T>(reference, stamp); } }
Atomic原子变量类的使用
java.util.concurrent.atomic原子操作包为我们提供了四类原子操作:
提供类如下截图:
1.原子更新基本类型
AtomicBoolean:布尔型
AtomicInteger:整型
AtomicLong:长整型
2.原子更新数组
AtomicIntegerArray:数组里的整型
AtomicLongArray:数组里的长整型
AtomicReferenceArray:数组里的引用类型
3.原子更新引用
AtomicReference<V>:引用类型
AtomicStampedReference:带有版本号的引用类型(可以防止ABA问题)
AtomicMarkableReference:带有标记位的引用类型
4.原子更新字段
AtomicIntegerFieldUpdater:对象的属性是整型
AtomicLongFieldUpdater:对象的属性是长整型
AtomicReferenceFieldUpdater:对象的属性是引用类型
5.JDK8新增
DoubleAccumulator、LongAccumulator、
DoubleAdder、LongAdder
是对AtomicLong等类的改进。比如LongAccumulator与LongAdder在高并发环境下比AtomicLong更高效。