写点什么

CAS 都不了解,你还怎么看 J-U-C,下载量瞬秒百万

用户头像
极客good
关注
发布于: 刚刚

上面的代码其实就是为了初始化内存值对应的内存地址偏移量 valueOffset,方便后续执行 CAS 操作时使用。因为这个值一旦初始化,就不会更改,所以使用 static final 修饰。


我们可以看到 value 使用了 volatile 修饰,其中也说了 volatile 的语义。


我们都知道如果进行 value++操作,并发下是不安全的。上一篇中我们也通过例子证明了 volatile 只能保证可见性,不能保证原子性。因为 value++本身不是原子操作,value++分了三步,先拿到 value 的值,进行+1,再赋值回 value。

2.2、compareAndSwapXxx

我们先看一看 AtomicInteger 提供的 CAS 操作。


/**


  • 原子地将 value 设置为 update,如果 valueOffset 对应的值与 expect 相等时

  • @param expect 期待值

  • @param update 更新值

  • @return 如果更新成功,返回 true;在 valueOffset 对应的值与 expec


【一线大厂Java面试题解析+核心总结学习笔记+最新架构讲解视频+实战项目源码讲义】
浏览器打开:qq.cn.hn/FTf 免费领取
复制代码


t 不相等时返回 false*/public final boolean compareAndSet(int expect, int update) {return unsafe.compareAndSwapInt(this, valueOffset, expect, update);}


我们已经知道 CAS 的原理,那来看看下面的测试。**你知道输出的结果是多少吗?**评论区给出你的答案吧。


public class AtomicIntegerTest {public static void main(String[] args) {AtomicInteger atomicInteger = new AtomicInteger();atomicInteger.compareAndSet(0, 1);atomicInteger.compareAndSet(2, 1);atomicInteger.compareAndSet(1, 3);atomicInteger.compareAndSet(2, 4);System.out.println(atomicInteger.get());}}


Unsafe 提供了三个原子更新的方法。


关于 Unsafe 类,因为 java 不支持直接操作底层硬件资源,如分配内存等。如果你使用 unsafe 开辟的内存,是不被 JVM 垃圾回收管理,需要自己管理,容易造成内存泄漏等。

2.3、AtomicInteger 的原子自增方法

我们上面说了,value++不是原子操作,不能在并发下使用。我们来看看 AtomicInteger 提供的原子++操作。


/**


  • 原子地对 value 进行+1 操作

  • @return 返回更新后的值*/public final int incrementAndGet() {return unsafe.getAndAddInt(this, valueOffset, 1) + 1;}


/**


  • unsafe 提供的方法

  • var1 更改的目标对象

  • var2 目标对象的共享字段对应的内存地址偏移量 valueOffset

  • var4 需要在原 value 上增加的值

  • @return 返回未更新前的值*/public final int getAndAddInt(Object var1, long var2, int var4) {//期待值 int var5;do {//获取 valueOffset 对应的 value 的值,支持 volatile loadvar5 = this.getIntVolatile(var1, var2);//如果原子更新失败,则一直重试,直到成功。} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));


return var5;}


**我们看到 CAS 只能原子的更新一个值,如果我们要原子更新多个值,CAS 可以做到吗?**答案是可以的。

2.4、AtomicReference

如果要原子地更新多个值,就需要使用 AtomicReference。其使用的是 compareAndSwapObject 方法。可以将多个值封装到一个对象中,原子地更换对象来实现原子更新多个值。


public class MultiValue {private int value1;private long value2;private Integer value3;


public MultiValue(int value1, long value2, Integer value3) {this.value1 = value1;this.value2 = value2;this.value3 = value3;}}


public class AtomicReferenceTest {public static void main(String[] args) {MultiValue multiValue1 = new MultiValue(1, 1, 1);MultiValue multiValue2 = new MultiValue(2, 2, 2);MultiValue multiValue3 = new MultiValue(3, 3, 3);AtomicReference<MultiValue> atomicReference = new AtomicReference<>();//因为构造 AtomicReference 时,没有使用有参构造函数,所以 value 默认值是 nullatomicReference.compareAndSet(null, multiValue1);System.out.println(atomicReference.get());atomicReference.compareAndSet(multiValue1, multiValue2);System.out.println(atomicReference.get());atomicReference.compareAndSet(multiValue2, multiValue3);System.out.println(atomicReference.get());}}//输出结果//MultiValue{value1=1, value2=1, value3=1}//MultiValue{value1=2, value2=2, value3=2}//MultiValue{value1=3, value2=3, value3=3}


我们再看一看 AtomicReference 的 compareAndSet 方法。


注意:这里的比较都是使用==而非 equals 方法。所以最好封装的 MultiValue 不要提供 set 方法。


public final boolean compareAndSet(V expect, V update) {return unsafe.compareAndSwapObject(this, valueOffset, expect, update);}

2.5、CAS 的 ABA 问题

假设你的账户上有 100 块钱,你要给女票转 50 块钱。


我们使用 CAS 进行原子更新账户余额。由于某种原因,你第一次点击转账出现错误,你以为没有发起转账请求,这时候你又点击了一次。系统开启了两个线程进行转账操作,第一个线程进行 CAS 比较,发现你的账户上预期是 100 块钱,实际也有 100 块钱,这时候转走了 50,需要设置为 100 - 50 = 50 元,这时账户余额为 50


第一个线程操作成功了,第二个线程由于某种原因阻塞住了;这时候,你的家人又给你转了 50 块钱,并且转账成功。那你账户上现在又是 100 块钱;


**太巧了,第二个线程被唤醒了,发现你的账户是 100 块钱,跟预期的 100 是相等的,这时候又 CAS 为 50。大兄弟,哭惨了,你算算,正确的场景你要有多少钱?**这就是 CAS 存在的 ABA 问题。


public class AtomicIntegerABA {


private static AtomicInteger atomicInteger = new AtomicInteger(100);


public static void main(String[] args) {ExecutorService executorService = Executors.newFixedThreadPool(3);


//线程 1executorService.execute(() -> {System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get());atomicInteger.compareAndSet(100, 50);System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get());});


//线程 2executorService.execute(() -> {try {TimeUnit.MILLISECONDS.sleep(300);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get());atomicInteger.compareAndSet(50, 100);System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get());});


//线程 3executorService.execute(() -> {try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get());atomicInteger.compareAndSet(100, 50);System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get());});


executorService.shutdown();}}//输出结果//pool-1-thread-1 - 100//pool-1-thread-1 - 50//pool-1-thread-2 - 50//pool-1-thread-2 - 100//pool-1-thread-3 - 100//pool-1-thread-3 - 50


大家心想,靠,这不是坑吗?那还用。。。。。。。。。。。。。。冷静,冷静。你能想到的问题,jdk 都能想到。atomic 包提供了一个 AtomicStampedReference

2.6、AtomicStampedReference

看名字是不是跟 AtomicReference 很像啊,其实就是在 AtomicReference 上加上了一个版本号,每次操作都对版本号进行自增,那每次 CAS 不仅要比较 value,还要比较 stamp,当且仅当两者都相等,才能够进行更新。


public AtomicStampedReference(V initialRef, int initialStamp) {pair = Pair.of(initialRef, initialStamp);}//定义了内部静态内部类 Pair,将构造函数初始化的值与版本号构造一个 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);}}


//所以我们之前的 value 就对应为现在的 pair

用户头像

极客good

关注

还未添加个人签名 2021.03.18 加入

还未添加个人简介

评论

发布
暂无评论
CAS都不了解,你还怎么看J-U-C,下载量瞬秒百万