面经手册 —— 谈谈对 CAS 的理解
题目描述
提问线路: CAS
=> Unsafe
=> CAS 底层原理
=> 原子更新
=> 如何规避 ABA 问题
面试题分析
compareAndSet 怎么用?
import java.util.concurrent.atomic.AtomicInteger;
public class Main {
/**
* boolean compareAndSet(int expect,int update)
* - 如果主内存的值=期待值expect,就将主内存值改为update
* - 该方法可以检测线程a的操作变量X没有被其他线程修改过,保证了线程安全
*/
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(5);
System.out.println(atomicInteger.compareAndSet(5,10) + "\t" + atomicInteger); //true 10
System.out.println(atomicInteger.compareAndSet(5,20) + "\t" + atomicInteger); //false 10
}
}
public static void main(String[] args) {
User z3 = new User("z3", 18);
User l4= new User("l4", 18);
AtomicReference<User> atomicReference = new AtomicReference<>(z3);
System.out.println(atomicReference.compareAndSet(z3,l4) + "\t" + atomicReference.get().toString());
System.out.println(atomicReference.compareAndSet(z3,l4) + "\t" + atomicReference.get().toString());
//true com.example.concurrent.cas.User@4554617c
//false com.example.concurrent.cas.User@4554617c
}
CAS 底层原理简述
- Compare-And-Swap,是一条 CPU 并发原语。(原语:操作系统范畴,依赖硬件,不被中断)
- 功能是判断内存某个位置的值是否为预期值(compare),如果是,就更新(swap),这个过程是原子的(要么成功,要么失败)。
- CAS 有 3 个操作数,内存值 V ,预期值 A,要更新的值 B。仅当预期值 A == 内存值时,才将内存值 V 修改为 B,否则什么都不做。
- 自旋:比较并交换,直到比较成功
- 底层靠
Unsafe
类保证原子性
getAndIncrement 源码解析
java.util.concurrent.atomic.AtomicInteger#getAndIncrement
源码解析(用了CAS保证线程安全)
/**
* 参数说明:
* this:AtomicInteger对象
* valueOffset:对象的内存地址
* unsafe:sun.misc.Unsafe 类
* AtomicInteger中变量value使用volatile修饰,保证内存可见
*/
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
底层调用了 sun.misc.Unsafe 类
/**
* compareAndSwapInt,即CAS
* 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;
}
简述:
- 调用了
sun.misc.Unsafe#getAndAddInt
方法 getAndAddInt
方法使用 CAS 一直循环尝试修改主内存
对 Unsafe 的理解
- 该类所有方法都是 native 修饰,直接调用底层资源。位于 sun.misc 包中
- 可以像 C 的指针一样直接操作内存。Java 的 CAS 操作依赖 Unsafe 类的方法
CAS 有哪些缺点?
-
循环时间长,开销大
如果 CAS 失败,就一直 while 尝试。如果长时间不成功,可能给 CPU 带来很大的开销
- 在内存地址 V 中,存储着值为 10 的变量
- 线程1想把变量的值加一,对线程1来说,预期值为10,要修改的值为11
- 在线程1提交更新之前,另一个线程2抢先一步,把变量值更新成了11
- 线程1开始提交更新,期望值不等于实际值,提交失败
- 线程1重新获取 V 的值,并重新计算想要修改的值。此时期望值为11,要修改的值为12。这个重新尝试的过程被称为自旋。
- CAS属于乐观锁,乐观地认为程序中的并发情况不那么严重,所以让线程不断去重试更新。
-
只能保证一个共享变量的原子操作
如果时多个共享变量,CAS 无法保证原子性,只 能加锁,锁住代码段。
-
存在 ABA 问题
ABA 问题描述?
- 线程1从内存位置 V 取出值 A
- 此时线程2也取出 A ,且线程2做了一次 CAS 将值改为 B,然后又做了一次 CAS 将值改回了 A
- 此时线程1开始 CAS,发现内存值还是 A,则线程1 操作成功
- 这个时候实际上 A 值其实已经被其他线程改变过,这与设计思想是不符合的。
如果只在乎结果,ABA 不介意 B 的存在,没什么问题
如果 B 的存在会造成影响,需要通过 AtomicStampReference
,加时间戳解决。
解决思路:每次变量更新的时候,把变量的版本号加1,这样只要变量被某一个线程修改过,版本号就会递增,从而解决了ABA问题
public class ABADemo {
AtomicStampedReference atomicStampedReference = new AtomicStampedReference<>(100, 1);
}
public class Main2 {
public static void main(String[] args) {
// ABAProblem();
ABADemo abaDemo = new ABADemo();
new Thread(() -> {
// 等线程2读到初始版本号的值
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程1在ABA前的版本号:" + abaDemo.atomicStampedReference.getStamp());
abaDemo.atomicStampedReference.compareAndSet(100, 101, abaDemo.atomicStampedReference.getStamp(),
abaDemo.atomicStampedReference.getStamp() + 1);
abaDemo.atomicStampedReference.compareAndSet(101, 100, abaDemo.atomicStampedReference.getStamp(),
abaDemo.atomicStampedReference.getStamp() + 1);
System.out.println("线程1在ABA后的版本号:" + abaDemo.atomicStampedReference.getStamp());
}, "1").start();
new Thread(() -> {
// 存一下修改前的版本号
int stamp = abaDemo.atomicStampedReference.getStamp();
System.out.println("线程2在修改操作前的版本号:" + stamp);
// 睡1s等线程1执行完ABA
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(abaDemo.atomicStampedReference.compareAndSet
(100, 2020, stamp, abaDemo.atomicStampedReference.getStamp() + 1) + "\t" +
abaDemo.atomicStampedReference.getReference());
}, "2").start();
//线程2在修改操作前的版本号:1
//线程1在ABA前的版本号:1
//线程1在ABA后的版本号:3
//false 100
}
}