本节介绍:
什么是CAS机制
CAS机制的作用(无锁编程)
CAS实现自旋锁
有关CAS机制的面试题等知识
目录
编辑
一:CAS
1.1 什么是CAS
1.2 CAS是怎么实现原子性的
1.3 JAVA中基于CAS实现的原子类介绍
1.4 用CAS实现自旋锁
1.5 CAS的ABA问题+解决方式
什么是ABA问题:
解决ABA问题的方式:
二:本文的经典面试题挑战
2.1 讲解下你自己理解的CAS机制
2.2 ABA问题怎么解决
CAS全称为 compare and swap(比较和交换)
CAS实际上是一种不加锁但是又保证了线程安全的一种机制
CAS的本质其实是一个原子操作,既然是原子操作那么线程安全就得到了有效的保障
CAS的操作可理解为:
假设:内存中的值为V, 拿到的值为A , 需要更新的值为 B
1. 比较V和A里的值
2. 如果V和A相等,那么把V的值更新为B
(如果目前内存中的实际值V与我们拿到的旧值A不相等,那么说明在本次操作的过程中内存中的值V已经被其他线程操作修改过了,此时我们拿到的旧值A是无效的,则不执行2操作,且返回交换失败的信息)(当多个线程同时对CAS指令进行操作时,只有一个线程会执行成功返回正确信息,其他线程执行失败返回的为错误信息)
3. 返回是否交换成功的信息
伪代码:
//此处伪代码用于理解:并非实际实现过程
public boolean CAS(内存值V,拿到的值A,需要更新的值B)){
if(V == A){ //再次判断V和A是否相等(对应前面第2步)
V = B;
return true; //交换成功 返回成功信息
}
return false; //交换失败,返回失败信息
}
我们可以把CAS指令理解为乐观锁,CAS代码在多线程并发执行下,当其中一个线程成功的执行了更新操作,那么其他线程在V == A阶段就会不相等,因为执行成功的线程已经把内存值给改变了,那么其他线程就会返回错误信息
(并不会堵塞线程和乐观锁一样)
不会堵塞线程,那么业务的并发程度就高了,这么一个纯用户态的操作就会使得效率提高
例子:基于CAS操作的改变余额操作:
小明的账户有100元钱
此时有两个线程操作正在同时对小明的账户余额进行更改
A:由于自己消费让账户减去50元
B: 由于别人转账让账户增加20元
此时t1,t2线程同时拿到了内存中的值100
1. t1,t2同时线程先拿到内存中的100
2. 在判断一下自己拿到的100,是否与内存中的值相等
如果相等,那么执行更改操作,让100-50,把内存中的余额值改为50,且返回操作成功的信息
如果不相等的,那么不进行更改操作,返回错误信息
假设:t1线程先对余额进行了更改
那么此时t1线程成功更改了余额
t2线程则更改失败
(如果需要成功让t2的更改成功,可以在更改失败后用错误信息进行条件判定去循环去执行CAS机制的原子更改操作)
上述代码并非实际实现过程,只是用于理解CAS指令
CAS的判断值相等和赋值操作等一系列代码整体并不是一个原子操作,那么CAS是怎么保证原子性的呢?
CAS的实现主要是由于CPU硬件的支持,CPU针对CAS的代码就会把其整体看做成一个原子指令进行执行。
正是因为CPU这种硬件的支持,然后JVM再对各个操作系统的汇编CAS操作进行封装,最后得到了java的Unsafe类,Unsafe类就实现了各自基于CAS实现的线程安全的原子类
JAVA依据JVM对CAS操作的封装提供了Unsafe类来实现CAS指令
JAVA标准库中提供了java.util.concurrent.atomic包,里面的类都是基于CAS实现的各个类型的原子类。(针对这写类执行的修改方法都是基于CAS实现的保证了线程安全)
例如:AtmoicInteger类就是对应的Integer类来实现的原子类
而其中的 getAndIncrement就相当于是i++操作
代码:
public class Test {
public static AtomicInteger count = new AtomicInteger(0); //给定一个AtomInteger的整形的原子类对象
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
count.getAndIncrement();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
count.getAndIncrement();
}
});
t1.start();
t2.start();
Thread.sleep(1000); //确保t1,t2线程执行完毕
System.out.println(count);
}
}
我们都知道普通情况下多个线程同时对一个成员变量进行修改,对导致线程安全,其结果不会满足预期值
这里我们创建了一个AtomicInteger原子类对象count,初始化为0
此时创建了t1和t2两个线程同时对count进行getAndIncrement方法(i++自增操作)各50000次
结果为100000与预期值相符合
证明了基于CAS实现的原子类操作保证了线程安全
getAndIncrement原码:
private static final Unsafe unsafe = Unsafe.getUnsafe();//Unsafe类实现了CAS
private static final long valueOffset;
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
其他常用原子类:
实现逻辑:
1. 定义一个代表加锁线程的成员变量,去表示该锁是否被占用
2. 写一个加锁方法(lock),和解锁方法(unlock)
3.基于CAS机制:如果加锁线程==null,那么加锁线程 = 当前线程
如果加锁!=null,那么加锁线程重复进行判断是否为null,不为空时进行加锁
伪代码:while(CAS(thread, null, Thread.currentThread()) != true)
伪代码:
class SpitLock{
//定义一个代表线程的成员变量,去表示该锁是否被占用
public Thread thread;
//加锁
public void lock(){
//基于CAS实现加锁
while(CAS(thread,null,Thread.currentThread())!=true){
//自旋加锁
//如果thread == null,那么 thread = Thread.currentThread(),返回true
// 此时锁并没有被占用,那么让当前线程占用锁,返回成功信息
//如果thread != null
// 那么此时锁已经被占用了,返回false信息,再次进行循环再次进行加锁操作(自旋加锁)
}
}
//解锁
public void unlock(){
//把占锁线程置为null,说明锁已经不被占用
thread = null;
}
}
前面已经介绍,CAS机制是通过判断拿到的值和内存的值是否相等再来进行后续更改操作
如果A线程针对某个值进行该更,又有其他线程对该值更改了,但是经过多次更改导致内存中进行判定的值又恢复到了A线程的判定值,那么此时A线程依旧会执行已经生效的
这个就是CAS的ABA问题
例子:
内存余额为100元
t1线程对余额进行减50操作
t2线程对余额进行加20操作
t3线程对余额进行减20操作
如果t2,t3线程在t1前面执行,那么余额又回到了100元
那么线程t1在进行判定的时候依旧是100 == 100,正常执行
此时t1线程在拿到内存值100进行判定时,t2,t3线程已经对余额进行操作了,但是操作完余额还是100,那么此时t1的线程判定仍然生效,这就是典型的ABA问题
虽然这里例子不会出现问题,并且在日常开发中这种ABA问题也一般不会出现问题
但是业务的逻辑会更复杂,执行过程中不一定只执行了这么一个余额的更改操作,此时中间已经执行的其他逻辑,再让前面的判定再成功执行线程,保不齐会出现什么问题
所以开发人员们队友这种不确定的安全隐患很是忌讳
引入一个版本号version,我们让版本号来进行判定,版本号只能进行自增表示版本的调整
伪代码
CAS(内存中的版本号,拿到的版本号,版本号++)
通过单项递增的版本号来进行条件判定,判定成功再进行我们自己的逻辑。
每次执行一次逻辑成功,版本号进行自增,其他的版本号也就失效的
从而解决ABA这种反复横条判定异常的问题