目录
1. 由CAS展开的知识点
2. CAS简介
2.1 概念
2.2 操作过程
2.3 DEMO案例
3 CAS底层原理
3.1 Unsafe类(源码地址 jvm rt.jar\sun\misc\Unsafe)
3.2 Unsafe定义
3.3 CAS为什么保证原子性?
3.4 Unsafe类getAndAddInt 底层源码解析
3.4.1源码:
3.4.2 解析:
3.4.3 源码套入案例解析:
3.5 底层汇编
3.6 小结(为了加深印象)
4 CAS缺点
5 ABA问题
5.1 什么是ABA问题
5.2 如何解决ABA问题
5.2.1 原子引用(AtomicReference)说明
5.2.2 DEMO AtomicReference的使用
5.2.3 存在ABA问题的DEMO
5.2.4 原子引用版本号(类似时间戳)AtomicStampedReference
5.2.5 利用AtomicStampedReference解决ABA问题的DEMO
CAS->Unsafe->CAS底层思想->ABA->原子引用更新->如何规避ABA问题
CAS(compareAndSet,compareAndExchange,compareAndSwap), 比较并交换。
CAS 是 compareAndSwap 的缩写,它是一条CPU并发原语。
CAS并发原语体现在JAVA语言中就是sun.misc.Unsafe类中的各个方法。调用UnSafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。再次强调,由于CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。(原子性)
CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器(在 Intel 处理器中,比较并交换通过指令的 cmpxchg 系列实现)会自动将该位置值更新为新值。否则,处理器不做任何操作。这个过程是原子的。
再说的直白点就是先进行比较,如果相比较的两个值是相等的,那么就进行更新操作
/**
* CAS:compareAndSet,比较并交换
* AtomicInteger
*/
public class CASDemo {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(5);
System.out.println(atomicInteger.compareAndSet(5, 2019)+"\t current data: "+atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(5, 1024)+"\t current data: "+atomicInteger.get());
}
}
//==============================下面是源码==============================
/**
* Atomically sets the value to the given updated value
* if the current value {@code ==} the expected value.
*
* @param expect the expected value
* @param update the new value
* @return {@code true} if successful. False return indicates that
* the actual value was not equal to the expected value.
*/
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
输出:
true current data: 2019
false current data: 2019
第一次打印,compareAndSet(期望值,要更新的值), 主内存值和期望值都是5,比较成功,内存值更新为2019
第二次打印,主内存值2019,期望值5,比较失败,无法更新值
自旋锁,Unsafe类
AtomicInteger i++ 源码:
参数定义:
this是当前对象, valueOffset是对象内存地址偏移量,1是增加数值
AtomicInteger类 源码
注意源码中value是被volatile修饰的,保证了多线程之间的内存可见性(即该值变更其他线程都知道)
Unsafe是CAS核心类,由于JAVA方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为JAVA中CAS操作的执行依赖于Unsafe类的方法。
CAS底层就是根据valueOffset内存中的偏移地址(相当于坐标),修改坐标所在的value值。 因为Unsafe就是根据内存偏移地址获取数据的。
注意Unsafe类中的所有方法都是native修饰的,也就是Unsafe类中的方法都直接调用操作系统底层资源执行相应任务。可以保证线程安全
因为CAS汇编指令发出CPU原语,这是依赖硬件的功能,原语执行具有原子性,一定是连续执行,不会打断的。
do中的操作:get,获取内存值
while中的操作:比较成功则交换值,跳出循环;失败则重新do操作
var1 是AtomicInteger对象本身
var2 该对象值的内存偏移量(即对象的引用地址)
var4 需要变动的数量, i++时,值为1,代表每次加1
var5 是用过var1 var2找出的主内存的真实值
用该对象当前的值与var5比较(即va1对象在var2地址的值是否是var5)
相同,则更新var5+var4并且返回4
不同,继续取值再比较,直至更新完成
do while这里就相当于自旋锁,属于乐观锁的思想
假设线程A和线程B两个线程同时执行了getAndAddInt(var1:当前对象, var2:地址偏移量, var4:1)操作(分别跑在不同的CPU上)
1. AtomicInteger里面的value原始值为3,即主内存中AtomicInteger的value为3
2. 线程A和线程B拷贝主内存value值3的副本到各自的工作内存。
3. 线程A通过getIntVolatile(var1, var2)拿到主内存value值3,这时线程A被挂起。
4. 线程B通过getIntVolatile(var1, var2)获取到value值3并赋值给var5,此时刚好线程B没有被挂起并执行compareAndSwapInt方法,比较var5(期望值)和主内存值(真实值)是否相等,当前值均为3,相等。修改内存值为4,线程B结束
5. 这时线程A恢复,执行compareAndSwapInt方法比较,发现自己手里的值数字3和主内存的值数字4不一致,说明该值已经被其他线程抢先一步修改过,故线程A本次修改失败,只能重新获取内存值,再次比较。
6. 线程A重新获取value值,因为变量value被volatile修饰,所以线程A可以看到其它线程对它的修改,线程A继续执行compareAndSwapInt进行比较替换,直至成功。
Unsafe类中的compareAndSwapInt,是一个本地方法(native),该方法实现位于unsafe.cpp中
//Atomic 原子的
//cmp(compare比较)x(值)chg(并替换change)
CAS 比较当前工作内存中的值和主内存中的值,相同执行规定操作,否则继续比较直到工作内存和主内存数值一致为止。
CAS 三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)
预期值A和内存位置V的值相同时,内存位置V的值修改为新值B,否则不处理。
synchonized 一致性保证(加锁,同一时间段只有一个线程进行操作),并发量下降.
synchronized是悲观锁,这种线程一旦得到锁,其他需要锁的线程就挂起的情况就是悲观锁
CAS操作的就是乐观锁,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。
CAS 不加锁,保证了一致性,但是需要多次比较,故:
一句话表示:狸猫换太子
CAS算法实现一个重要前提,是需要去除内存中某时刻的数据,并在当下时刻进行比较并替换,在这个时间差可能会导致数据的变化
举例:
线程A从内存位置V中取出X值, 线程B也从内存位置V中取出X值,且线程B进行了操作,将X值改为Y,然后又再次操作将Y数据改为X, 这是线程A进行CAS操作,发现内存中仍然是X,线程A操作成功。
ABA问题指的是多个线程同时执行,那么开始时其获得的值都是A,当一个线程修改了A为B,后续修改了B为A,那么其他线程修改时判断A仍然是A,认为其没有修改过,因此会CAS成功(即头尾一致,中间存在过变化)
ABA问题产生的影响取决于你的业务是否会因此受到影响.如果有影响那么解决思路一般是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。 在JDK1.5之后提供了AtomicStampedReference
类来解决ABA问题,解决思路是保存元素的引用,引用相当于版本号,是每一个变量的标识,因此在CAS前判断下是否是同一个引用即可。
首先提出一个概念叫原子引用,之前的AtomicInteger是操作Integer数据,其他类型需要保证原子性时,可用AtomicReference、AtomicStampedReference(可解决ABA问题)。
//========================== 用户类 ==========================
package com.demo.jvm1_cas;
public class User {
String userName;
int 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 +
'}';
}
}
//========================== 分割线 main ==========================
package com.demo.jvm1_cas;
import java.util.concurrent.atomic.AtomicReference;
/**
* AtomicReference 原子引用
*/
public class AtomicReferenceDemo {
public static void main(String[] args) {
User zhangsan = new User();
zhangsan.setUserName("zhangsan");
zhangsan.setAge(22);
User lisi = new User();
lisi.setUserName("lisi");
lisi.setAge(25);
AtomicReference atomicReference = new AtomicReference<>();
atomicReference.set(zhangsan);
System.out.println(atomicReference.compareAndSet(zhangsan,lisi) + "\t"+atomicReference.get().toString());
System.out.println(atomicReference.compareAndSet(zhangsan,lisi) + "\t"+atomicReference.get().toString());
}
}
输出
true User{userName='lisi', age=25}
false User{userName='lisi', age=25}
package com.demo.jvm1_cas;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
/**
* ABA问题
*/
public class ABADemo {
static AtomicReference atomicReference = new AtomicReference<>(100);
public static void main(String[] args) {
//原始值
System.out.println(Thread.currentThread().getName() + "\t value = " + atomicReference.get().toString());
new Thread(()-> {
//第一次CAS
atomicReference.compareAndSet(100,101);
System.out.println(Thread.currentThread().getName() + "\t T1 CAS1 value = " + atomicReference.get().toString());
//第二次CAS,ABA问题产生
atomicReference.compareAndSet(101,100);
System.out.println(Thread.currentThread().getName() + "\t T1 CAS2 value = " + atomicReference.get().toString());
},"T1").start();
new Thread(()-> {
//为了保证T1线程完成了ABA操作,暂停T2线程1秒
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicReference.compareAndSet(100,1024);
System.out.println(Thread.currentThread().getName() + "\t T2 CAS3 value = " + atomicReference.get().toString());
},"T2").start();
}
}
输出
main value = 100
T1 T1 CAS1 value = 101
T1 T1 CAS2 value = 100
T2 T2 CAS3 value = 1024
由此可以看出,T1线程中途修改value为101,后续又修改回100,引发了ABA问题,T2线程在T1线程结束后,T2线程操作CAS成功,将值改为1024,并没有发现T1线程曾经修改过原值。
每操作一次,记录一次版本号
package com.demo.jvm1_cas;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicStampedReference;
/**
* 解决ABA问题
*/
public class AtomicStampReferenceDemo {
static AtomicStampedReference atomicStampedReference = new AtomicStampedReference<>(100, 1);
public static void main(String[] args) {
new Thread(()-> {
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "\t 第1次版本号:" + stamp);
//T1暂停一秒,保证T2线程也获取到第一次的版本号
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
//第一次CAS
atomicStampedReference.compareAndSet(100,101, atomicStampedReference.getStamp(), atomicStampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName() + "\t 第2次版本号:" + atomicStampedReference.getStamp());
//ABA问题产生
atomicStampedReference.compareAndSet(101,100, atomicStampedReference.getStamp(), atomicStampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName() + "\t 第3次版本号:" + atomicStampedReference.getStamp());
},"T1").start();
new Thread(()-> {
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "\t 第1次版本号:" + stamp);
//为了保证T1线程完成了ABA操作,暂停T2线程1秒
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
boolean result = atomicStampedReference.compareAndSet(100,2019, atomicStampedReference.getStamp(), stamp+1);
System.out.println(Thread.currentThread().getName() + "\t 修改结果:" + result + "\t 当前最新版本号" + atomicStampedReference.getStamp());
System.out.println(Thread.currentThread().getName() + "\t 当前最新值:" + atomicStampedReference.getReference().toString());
},"T2").start();
}
}
输出
T1 第1次版本号:1
T2 第1次版本号:1
T1 第2次版本号:2
T1 第3次版本号:3
T2 修改结果:false 当前版本号3
T2 当前最新值:100
由此可以看出,当T1发生了ABA问题后,T2依据最初获取的版本号进行CAS操作失败,解决多线程无感知发生ABA的问题
CAS操作在Java中的应用很广泛,比如ConcurrentHashMap
,ReentrantLock
等,其常被用来解决独占锁对线程阻塞而导致的性能低下问题,是高效并发必备的一种优化方法.
多线程问题归根结底要解决的是可见性,有序性,原子性三大问题,大家都知道JVM提供的volatile可以保证可见性与有序性,但是无法保证原子性,换句话说 volatile + CAS实现原子性操作 = 线程安全 = 高效并发,那么CAS就是用来实现这个操作的原子性.
【1】Java面试_高频重点面试题 (第一、二、三季)_ 面试 第1、2、3季_柴林燕_周阳_哔哩哔哩_bilibili
【2】Java--CAS操作分析 - 云+社区 - 腾讯云 (tencent.com)