Java:关于 CAS 笔记

本笔记是根据bilibili上 尚硅谷 的课程 Java大厂面试题第二季 而做的笔记

1. CAS 底层原理

概念

CAS 的全称是 Compare-And-Swap,它是 CPU 并发原语

它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的

CAS 并发原语体现在 Java 语言中就是 sun.misc.Unsafe 类的各个方法。调用 UnSafe 类中的 CAS 方法,JVM会帮我们实现出 CAS 汇编指令,这是一种完全依赖于硬件的功能,通过它实现了原子操作,再次强调,由于CAS 是一种系统原语,原语属于操作系统用于范畴,是由若干条指令组成,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说 CAS 是一条 CPU 的原子指令,不会造成所谓的数据不一致的问题,也就是说 CAS 是线程安全的

代码使用

首先调用 AtomicInteger 创建了一个实例, 并初始化为 5

// 创建一个原子类
AtomicInteger atomicInteger = new AtomicInteger(5);

然后调用CAS方法,企图更新成2019,这里有两个参数,一个是5,表示期望值,第二个就是我们要更新的值

atomicInteger.compareAndSet(5, 2019)

然后再次使用了一个方法,同样将值改成1024

atomicInteger.compareAndSet(5, 1024)

完整代码如下:

/**
* CASDemo
*
* 比较并交换:compareAndSet
*/
public class CASDemo {
public static void main(String[] args) {
// 创建一个原子类
AtomicInteger atomicInteger = new AtomicInteger(5); /**
* 一个是期望值,一个是更新值,但期望值和原来的值相同时,才能够更改
* 假设三秒前,我拿的是5,也就是expect为5,然后我需要更新成 2019
*/
System.out.println(atomicInteger.compareAndSet(5, 2019) + "\t current data: " + atomicInteger.get()); System.out.println(atomicInteger.compareAndSet(5, 1024) + "\t current data: " + atomicInteger.get());
}
}

上面代码的执行结果为:

true	 current data: 2019
false current data: 2019

这是因为我们执行第一个的时候,期望值和原本值是满足的,因此修改成功,但是第二次后,主内存的值已经修改成了 2019,不满足期望值,因此返回了 false,本次写入失败

这个就类似于 SVN 或者 Git 的版本号,如果没有人更改过,就能够正常提交,否则需要先将代码 pull 下来,合并代码后,然后提交。

CAS 底层原理

首先我们先看看 atomicInteger.getAndIncrement() 方法的源码

/**
* Atomically increments by one the current value.
*
* @return the previous value
*/
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}

从这里能够看到,底层又调用了一个 unsafe 类的 getAndAddInt 方法

1. unsafe 类

public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L; // setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset; static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
} private volatile int value;
}

Unsafe 是 CAS 的核心类,由于 Java 方法无法直接访问底层系统,需要通过本地(Native)方法来访问,Unsafe 相当于一个后门,基于该类可以直接操作特定的内存数据。Unsafe 类存在 sun.misc 包中,其内部方法操作可以像 C 的指针一样直接操作内存,因为 Java 中的 CAS 操作的执行依赖于 Unsafe 类的方法。

注意Unsafe类的所有方法都是native修饰的,也就是说unsafe类中的方法都直接调用操作系统底层资源执行相应的任务

为什么 Atomic 修饰的包装类,能够保证原子性,依靠的就是底层的 unsafe 类

2. 变量 valueOffset

表示该变量值在内存中的偏移地址,因为 Unsafe 就是根据内存偏移地址获取数据的。

public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
// this:指当前对象
// valueOffset:内存偏移量,也就是内存地址
}

从这里我们能够看到,通过 valueOffset,直接通过内存地址,获取到值,然后进行加1的操作

3. 变量 value 用 volatile 修饰

这保证了多线程之间的内存可见性

// AtomicInteger类源码
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe(); public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
} // Unsafe类源码:
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
// 不断循环拿到主内存中的值
var5 = this.getIntVolatile(var1, var2);
// var1:this, AtomicInteger 对象本身
// var2:偏移量
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5;
}
  • var5:就是我们从主内存中拷贝到工作内存中的值(每次都要从主内存拿到最新的值到自己的本地内存,然后执行 compareAndSwapInt()和主内存的值进行比较。因为线程不可以直接越过高速缓存,直接操作主内存,所以执行上述方法需要比较一次,再执行加1操作)

    那么操作的时候,需要比较工作内存中的值,和主内存中的值进行比较

    假设执行 compareAndSwapInt() 返回 false,那么就一直执行 while 方法,直到期望的值和真实值一样

  • val1:AtomicInteger 对象本身

  • var2:该对象值得引用地址

  • var4:需要变动的数量

  • var5:用 var1 和 var2 找到的内存中的真实值

    • 用该对象当前的值与 var5 比较
    • 如果相同,更新 var5 + var4 并返回 true
    • 如果不同,继续取值然后再比较,直到更新完成

这里没有用 synchronized,而用 CAS,这样提高了并发性,也能够实现一致性,是因为每个线程进来后,进入的 do while 循环,然后不断的获取内存中的值,判断是否为最新,然后在进行更新操作。

假设线程 A 和线程 B 同时执行 getAndInt 操作(分别跑在不同的CPU上)

  1. AtomicInteger 里面的 value 原始值为3,即主内存中 AtomicInteger 的 value 为 3,根据 JMM 模型,线程A 和 线程B 各自持有一份值为3的副本,分别存储在各自的工作内存
  2. 线程A 通过getIntVolatile(var1 , var2) 拿到 value 值3,这是 线程A 被挂起(该线程失去CPU执行权)
  3. 线程B 也通过getIntVolatile(var1, var2)方法获取到 value 值也是3,此时刚好线程B没有被挂起,并执行了compareAndSwapInt()方法,比较内存的值也是3,成功修改内存值为4,线程B 打完收工,一切OK
  4. 这时 线程A 恢复,执行 CAS 方法,比较发现自己手里的数字3和主内存中的数字4不一致,说明该值已经被其它线程抢先一步修改过了,那么 线程A 本次修改失败,只能够重新读取后在来一遍了,也就是在执行do while
  5. 线程A 重新获取 value 值,因为变量 value 被 volatile 修饰,所以其它线程对它的修改,线程A 总能够看到,线程A 继续执行compareAndSwapInt()进行比较替换,直到成功。

Unsafe类 + CAS思想: 也就是自旋,自我旋转

底层汇编

Unsafe 类中的 compareAndSwapInt() 是一个本地方法,该方法的实现位于 unsafe.cpp

  • 先想办法拿到变量 value 在内存中的地址
  • 通过 Atomic::cmpxchg 实现比较替换,其中 参数X 是即将更新的值,参数e 是原内存的值

CAS缺点

CAS不加锁,保证一致性,但是需要多次比较

  • 循环时间长,开销大(因为执行的是 do while,如果比较不成功一直在循环,最差的情况,就是某个线程一直取到的值和预期值都不一样,这样就会无限循环)
  • 只能保证一个共享变量的原子操作
    • 当对一个共享变量执行操作时,我们可以通过循环 CAS 的方式来保证原子操作
    • 但是对于多个共享变量操作时,循环 CAS 就无法保证操作的原子性,这个时候只能用锁来保证原子性
  • 引出来 ABA 问题

总结

CAS 是 compareAndSwap,比较当前工作内存中的值和主物理内存中的值,如果相同则执行规定操作,否者继续比较直到主内存和工作内存的值一致为止

CAS有3个操作数,内存值V,旧的预期值A,要修改的更新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否者什么都不做。

2. 原子类AtomicInteger的ABA问题

连环套路

从 AtomicInteger 引出下面的问题:

CAS -> Unsafe -> CAS底层思想 -> ABA -> 原子引用更新 -> 如何规避ABA问题

ABA问题是什么

狸猫换太子

假设现在有两个线程,分别是 T1 和 T2,然后T1执行某个操作的时间为10秒,T2执行某个时间的操作是2秒,最开始AB两个线程,分别从主内存中获取A值,但是因为B的执行速度更快,他先把A的值改成B,然后在修改成A,然后执行完毕,T1线程在10秒后,执行完毕,判断内存中的值为A,并且和自己预期的值一样,它就认为没有人更改了主内存中的值,就快乐的修改成B,但是实际上可能中间经历了 ABCDEFA 这个变换,也就是中间的值经历了狸猫换太子。

所以ABA问题就是,在进行获取主内存值的时候,该内存值在我们写入主内存的时候,已经被修改了N次,但是最终又改成原来的值了

CAS导致ABA问题

CAS 算法实现了一个重要的前提,需要取出内存中某时刻的数据,并在当下时刻比较并替换,那么这个时间差会导致数据的变化。

比如说一个 线程1 从内存位置V中取出A,这时候另外一个 线程2 也从内存中取出A,并且 线程2 进行了一些操作将值变成了B,然后 线程2 又将V位置的数据变成A,这时候 线程1 进行CAS操作发现内存中仍然是A,然后 线程1 操作成功

尽管线程1 的CAS操作成功,但是不代表这个过程就是没有问题的

ABA问题

CAS 只管开头和结尾,也就是头和尾是一样,那就修改成功,中间的这个过程,可能会被人修改过

原子引用

原子引用其实和原子包装类是差不多的概念,就是将一个 Java 类,用原子引用类进行包装起来,那么这个类就具备了原子性

/**
* 原子引用
*/
class User {
String userName;
int age; public User(String userName, int age) {
this.userName = userName;
this.age = age;
} public String getUserName() {
return userName;
} public void setUserName(String userName) {
this.userName = userName;
} public int getAge() {
return age;
} public void setAge(int age) {
this.age = age;
} @Override
public String toString() {
return "User{" +
"userName='" + userName + '\'' +
", age=" + age +
'}';
}
} public class AtomicReferenceDemo { public static void main(String[] args) { User z3 = new User("z3", 22);
User l4 = new User("l4", 25);
// 创建原子引用包装类
AtomicReference<User> atomicReference = new AtomicReference<>();
// 现在主物理内存的共享变量,为z3
atomicReference.set(z3); // 比较并交换,如果现在主物理内存的值为z3,那么交换成l4
System.out.println(atomicReference.compareAndSet(z3, l4) + "\t " + atomicReference.get().toString()); // 比较并交换,现在主物理内存的值是l4了,但是预期为z3,因此交换失败
System.out.println(atomicReference.compareAndSet(z3, l4) + "\t " + atomicReference.get().toString());
}
}

基于原子引用的ABA问题

我们首先创建了两个线程,然后 T1线程,执行一次 ABA 的操作,T2线程 在 1s 后修改主内存的值

/**
* ABA问题代码体现
*/
public class ABADemo { /**
* 普通的原子引用包装类
*/
static AtomicReference<Integer> atomicReference = new AtomicReference<>(100); public static void main(String[] args) { new Thread(() -> {
// 把 100 改成 101 然后在改成100,也就是ABA
atomicReference.compareAndSet(100, 101);
atomicReference.compareAndSet(101, 100);
}, "t1").start(); new Thread(() -> {
try {
// 睡眠一秒,保证t1线程,完成了ABA操作
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 把100 改成 101 然后在改成100,也就是ABA
System.out.println(atomicReference.compareAndSet(100, 2019) + "\t" + atomicReference.get()); }, "t2").start();
}
}

我们发现,它能够成功的修改,这就是 ABA 问题

解决ABA问题

新增一种机制,也就是修改版本号,类似于时间戳的概念

T1: 100 1 ---> 2019 2

T2: 100 1 ---> 101 2 ---> 100 3

如果T1修改的时候,版本号为2,落后于现在的版本号3,所以要重新获取最新值,这里就提出了一个使用时间戳版本号,来解决ABA问题的思路

AtomicStampedReference

时间戳原子引用,来这里应用于版本号的更新,也就是每次更新的时候,需要比较期望值和当前值,以及期望版本号和当前版本号

public class ABADemo {

    // 传递两个值,一个是初始值,一个是初始版本号
static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1); public static void main(String[] args) { System.out.println("============以下是ABA问题的解决==========");
new Thread(() -> {
// 获取版本号
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "\t 第一次版本号" + stamp);
// 暂停t3一秒钟
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 传入4个值,期望值,更新值,期望版本号,更新版本号
atomicStampedReference.compareAndSet(100, 101, atomicStampedReference.getStamp(), atomicStampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName() + "\t 第二次版本号" + atomicStampedReference.getStamp());
atomicStampedReference.compareAndSet(101, 100, atomicStampedReference.getStamp(), atomicStampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName() + "\t 第三次版本号" + atomicStampedReference.getStamp());
}, "t3").start(); new Thread(() -> {
// 获取版本号
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "\t 第一次版本号" + stamp);
// 暂停t4 3秒钟,保证t3线程也进行一次ABA问题
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean result = atomicStampedReference.compareAndSet(100, 2019, stamp, stamp+1);
System.out.println(Thread.currentThread().getName() + "\t 修改成功否:" + result + "\t 当前最新实际版本号:" + atomicStampedReference.getStamp());
System.out.println(Thread.currentThread().getName() + "\t 当前实际最新值" + atomicStampedReference.getReference());
}, "t4").start();
}
}

运行结果为:

============以下是ABA问题的解决==========
t3 第一次版本号1
t4 第一次版本号1
t3 第二次版本号2
t3 第三次版本号3
t4 修改成功否:false 当前最新实际版本号:3
t4 当前实际最新值100

我们能够发现,线程t3,在进行 ABA 操作后,版本号变更成了3,而线程t4在进行操作的时候,就出现操作失败了,因为版本号和当初拿到的不一样

最新文章

  1. PHP 使用分页方法修改多数据字段
  2. char_dev.c 添加中文注释
  3. Activity packagename has leaked window android.widget.PopupWindow$PopupDecorView{4f92660 V.E...... .......D 0,0-455,600} that was originally added here
  4. 快捷键的记录,Word和PPT的一些常用操作
  5. Atitit.如何选择技术职业方向
  6. css-高度自适应的问题(body高度问题)
  7. 蓝牙4.0LED灯控方案
  8. [改善Java代码] 避免instanceof非预期结果
  9. ajax和jsonp的封装
  10. AFNetworking的原理与基本使用-b
  11. android的数据存储方式
  12. Delphi的指针(转)
  13. asp.net web api 版本控制
  14. IntelliJ IDEA~gradle环境配置
  15. vue swiper中的大坑
  16. Pandas基本操作
  17. 最长连续子序列(dp,分而治之递归)
  18. windows下面配合thinkphp5.1.7的nginx.conf内容
  19. URL编码规则
  20. win10 搭建react-native开发环境

热门文章

  1. Java的参数传递是值传递还是引用传递?
  2. spark集群的构建,python环境
  3. 处理burp log 小脚本
  4. 尚硅谷Java基础学习笔记
  5. vue-router路由钩子
  6. python matplotlib.pyplot 散点图详解(1)
  7. 在PHP中操作文件的扩展属性
  8. Mysql将其他表中的数据更新到指定表中
  9. YbtOJ#832-鸽子饲养【凸包,Floyd】
  10. Spring Cloud Gateway 雪崩了,我 TM 人傻了