CAS 是什么?优缺点
CAS 简单介绍:比较并交换(compareAndSet),将当前工作内存的值与主内存进行比较,如果一致则进行交换,不一致则继续读取主内存最新的值,下面给出 CAS 简单使用的示例代码:
/**
* Description
* @author vnjohn
* @version 1.0
* @date 2019-04-12 9:57
* 1.什么是CAS ? ===> compareAndSet
* 比较并交换
**/
public class CASDemo {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(5);
System.out.println(atomicInteger.compareAndSet(5, 2019)+"\t current"+atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(5, 2014)+"\t current"+atomicInteger.get());
}
}
atomicInteger.getAndIncrement()方法的源代码:
/**
* Atomically increments by one the current value.
* @return the previous value
*/
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
引出来一个问题:Unsafe 类是什么?
[
]alt_ _alt Unsafe 类
1) Unsafe
CAS 的核心类,由于 Java 方法无法直接访问底层,需要通过本地(native)方法来访问,基于该类可以直接操作特定的内存数据。Unsafe 类在于 sun.misc 包中,其内部方法操作可以向 C 的指针一样直接操作内存,因为 Java 中 CAS 操作的助兴依赖于 Unsafe 类的方法。
注意 Unsafe 类中所有的方法都是 native 修饰的,也就是说 Unsafe 类中的方法都是直接调用操作底层资源执行响应的任务。
2) 变量 ValueOffset
便是该变量在内存中的偏移地址,因为 Unsafe 就是根据内存偏移地址获取数据的
[
]altalt 1 _alt 1
变量 value 被 volatile 关键字修饰,保证了多线程之间的可见性。
volatile 关键字介绍文章:https://www.vnjohn.com/volatile-html/
3) CAS 是什么
CAS 全称为 Compare-And-Swap,它是一条 CPU 并发原语。
它的功能是判断内存某个位置的值是否为预期值,如果是则更新为新的值,这个过程是原子的。
CAS 并发原因体现在 Java 语言中就是 sun.misc.Unsafe 类中的各个方法,调用 Unsafe 类中的 CAS 方法,JVM 会帮我们实现 CAS 汇编指令。这是一种完全依赖于硬件功能,通过它实现了原子操作,再次强调,由于 CAS 是一种系统原语,原语属于操作系统用于范畴,是由若干指令组成,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许中断,也即是说 CAS 是一条原子指令,不会造成所谓的数据不一致问题。
num ++ 原子操作,CAS 相关的源代码如下:
[
][altalt 2 _alt 2][
][altalt 3 _alt 3][
]altalt 4 _alt 4
getAndAddInt() 方法里相关的变量介绍如下:
var1 AtomicInteger 对象本身. var2 该对象值的引用地址,所在位置偏移量 var4 需要变动的数值 var5 是用过var1与var2找出内存中共享变量的值 用该对象当前的值与var5比较 如果相同,更新var5的值并且返回true 如果不同,继续取值然后比较,直到更新完成 |
整体的流程介绍:
假设线程A和线程B两个线程同时执行getAndAddInt操作(分别在不同的CPU上)
①AtomicInteger 里面的 value 原始值为 3,即主内存中 AtomicInteger 的 value 为 3,根据 JMM 模型,线程 A 和线程 B 各自持有一份值为 3 的 value 的副本分别到各自的工作内存.
②线程 A 通过 getIntVolatile(var1,var2) 拿到 value 值 3,这是线程 A 被挂起.
③线程B也通过 getIntVolatile(var1,var2) 拿到 value 值 3,此时刚好线程 B 没有被挂起并执行 compareAndSwapInt 方法比较内存中的值也是 3 成功修改内存的值为 4 线程 B 打完收工 一切OK.
④这是线程 A 恢复,执行 compareAndSwapInt 方法比较,发现自己手里的数值和内存中的数字 4 不一致,说明该值已经被其他线程抢先一步修改了,那 A 线程修改失败,只能重新来一遍了.
⑤线程 A 重新获取 value 值,因为变量 value 是 volatile 修饰,所以其他线程对它的修改,线程 A 总是能够看到,线程 A 继续执行 compareAndSwapInt 方法进行比较替换,直到成功.
4) 底层汇编
[
]altalt 5 _alt 5
5) CAS 缺点
- 循环时间长开销很大
我们可以看到有个 getAndAddInt() 方法执行时,有个 do while
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
如果 CAS 失败,会一直进行尝试,如果 CAS 长时间不成功,可能会给 CPU 带来很大的开销。
- 只能保证一个共享变量的原子性
当对一个共享变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作,但是对多个共享变量操作时,循环 CAS 就无法保证操作的原子性,这个时候就可以用锁来保证原子性。
- 引出来 ABA 问题???
CAS 会导致“ABA 问题”。
CAS 算法实现一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差类会导致数据的变化。
比如说一个线程 one 从内存位置 V 中取出 A,这时候另一个线程 two 也从内存中取出 A,并且线程 two 进行了一些操作将值变成了 B,然后线程 two 又将 V 位置的数据变成 A,这时候线程 one 进行 CAS 操作发现内存中仍然是 A,然后线程 one 操作成功。
尽管线程 one 的 CAS 操作成功,但是不代表这个过程就是没有问题的。
6) ABA 问题的解决,时间戳+原子引用
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;
/**
* @author vnjohn
* @NAME: ABADemo
* @DATE: 2020/11/21
*/
public class ABADemo {
private static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
private static AtomicStampedReference<Integer> stampedReference = new AtomicStampedReference<>(100, 1);
public static void main(String[] args) {
System.out.println("===以下是ABA问题的产生===");
new Thread(() -> {
atomicReference.compareAndSet(100, 101);
atomicReference.compareAndSet(101, 100);
}, "t1").start();
new Thread(() -> {
//先暂停1秒 保证完成ABA
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(atomicReference.compareAndSet(100, 2019) + "\t" + atomicReference.get());
}, "t2").start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("===以下是ABA问题的解决===");
new Thread(() -> {
int stamp = stampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "\t 第1次版本号" + stamp + "\t值是" + stampedReference.getReference());
//暂停1秒钟t3线程 是为了让t4线程也拿到相同的版本号
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
stampedReference.compareAndSet(100, 101, stampedReference.getStamp(), stampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + "\t 第2次版本号" + stampedReference.getStamp() + "\t值是" + stampedReference.getReference());
stampedReference.compareAndSet(101, 100, stampedReference.getStamp(), stampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + "\t 第3次版本号" + stampedReference.getStamp() + "\t值是" + stampedReference.getReference());
}, "t3").start();
new Thread(() -> {
int stamp = stampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "\t 第1次版本号" + stamp + "\t值是" + stampedReference.getReference());
//保证t3线程完成1次ABA
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean result = stampedReference.compareAndSet(100, 2019, stamp, stamp + 1);
System.out.println(Thread.currentThread().getName() + "\t 修改成功否:" + result + "\t最新版本号" + stampedReference.getStamp());
System.out.println("最新的值\t" + stampedReference.getReference());
}, "t4").start();
}
}
还没有评论,来说两句吧...