CAS基本概念
CAS是英文单词Compare And Swap的缩写是一种无锁算法,像一种无阻塞多线程争抢资源的模型
CAS机制当中的3个操作数:
V 内存地址
A 旧的预期值
B 要将内存地址值修改成的新值
一个线程更新内存地址的一个变量的时候,当变量的预期值A和内存地址V当中的实际值相同时,就会认为没有其他线程修改过,就将内存地址V对应的值修改为B(可能会出现ABA问题,后面会有解析原因),反之则认为有其他线程修改过,放弃此更新操作,重复尝试获取内存地址值,直至修改成功
为什么使用CAS
在多线程高并发编程的时候,最关键的问题就是保证临界区的对象的安全访问。通常是用加锁来处理,其实加锁本质上是将并发转变为串行来实现的,势必会影响吞吐量。对于并发控制而言,锁是一种悲观策略,会阻塞线程执行。而无锁是一种乐观策略,它会假设对资源的访问时没有冲突的,既然没有冲突就不需要等待,线程不需要阻塞。
请参考下面的例子进行深入理解:
假设一个内存地址存着小明的存款变量为10000元
此时财务给他发工资10000元(线程1)
线程1从内存地址中获取得到预期值A为10000,其修改值B则为20000
正当财务打款的时候,
小明女朋友在外面刷他卡买了几只口红先花掉了1000元(线程2)
此时内存地址V中的值就是9000
而线程1中的预期值A依然是10000 修改值B依然是20000
此时线程1进行修改时,就会将V和A进行对比,如果发现不相等,就重新获取存款内存地址的值。
这时如果还有其他线程又修改了内存地址的值,就一直循环上一步操作,直到线程1进行修改操作就发现 V==A,符合条件,就swap 内存地址V的值替换为B,(默认没有其他线程修改)也就是19000。
由上述例子,大致可以看出CAS的工作流程是如何运行的。
缺点一:
但是发现CAS存在的一个问题就是,由于上面例子只是显示双线程下的情况,但是在并发量比较高的情况下,如果很多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力。
仔细观察CAS的工作流程,我们会发现CAS的工作流程,很像一种无阻塞多线程争抢资源的模型(无锁机制)
线程1和线程2,谁先抢到修改内存地址的权限,谁就能先修改,另外一个线程并不会阻塞,而是一直循环,直到成功修改。
在JAVA中java.util.concurrent.atomic
包下的原子类,都使用了CAS无锁机制
列如AtomicBoolean
、AtomicInteger
等它们分别用于Boolean
,Integer
类型的原子性操作。
下面的一段代码,更能体现出CAS的无阻塞多线程争抢资源的模型(无锁机制)
/**
* Author: ww
* Datetime: 2020\6\12 0012
* Description: 模拟两个线程CAS无阻塞抢资源
*/
public class CASDemo implements Runnable{
private static AtomicBoolean ab = new AtomicBoolean(true);
public static void main(String[] args) {
CASDemo cas = new CASDemo();
Thread t1 = new Thread(cas,"cas-thread1");
Thread t2 = new Thread(cas,"cas-thread2");
t1.start();
t2.start();
}
@Override
public void run() {
//ab.get()获取的是真实内存地址的值(多线程下,只要有一个线程修改了ab的值,其他所有线程获取的ab的值都是修改后的值)
System.out.println(Thread.currentThread().getName()+":第1步 进来时ab的值:"+ab.get());
if(ab.compareAndSet(true,false)){//模拟某个线程修改内存地址的值(原子操作)
System.out.println("线程"+Thread.currentThread().getName()+":抢到资源 === ab的值"+ab.get());
try {
//模拟抢到资源的线程操作时间比较长,让其他线程多次重新获取ab的值
Thread.sleep(8000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//释放资源,让其他线程进来
ab.set(true);
//直到控制台打印完这一句,更好的显示线程已经释放资源,其他线程才能获取到被修改后的内存地址的值
System.out.println("线程"+Thread.currentThread().getName()+"释放资源====第2步 ab的值"+ab.get());
}else{
//如果哪个线程打印了这句话,说明他并未第一个抢到资源
System.out.println("线程"+Thread.currentThread().getName()+":第3步 重新获取的ab值:"+ab.get());
try {
//模拟其他线程重复获取内存的时间
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//再次进行判断操作
run();
}
}
}
控制台打印结果
cas-thread2:第1步 进来时ab的值:true //每次执行结果都有点差异,但主体都差不多
线程cas-thread2:抢到资源 === ab的值false //线程2抢到资源,将ab修改成false,
cas-thread1:第1步 进来时ab的值:false //线程1一直循环第一步和第三步
线程cas-thread1:第3步 重新获取的ab值:false
cas-thread1:第1步 进来时ab的值:false
线程cas-thread1:第3步 重新获取的ab值:false
cas-thread1:第1步 进来时ab的值:false
线程cas-thread1:第3步 重新获取的ab值:false
cas-thread1:第1步 进来时ab的值:false
线程cas-thread1:第3步 重新获取的ab值:false
cas-thread1:第1步 进来时ab的值:false
线程cas-thread1:第3步 重新获取的ab值:false
cas-thread1:第1步 进来时ab的值:false
线程cas-thread1:第3步 重新获取的ab值:false
cas-thread1:第1步 进来时ab的值:false
线程cas-thread1:第3步 重新获取的ab值:false
cas-thread1:第1步 进来时ab的值:false
线程cas-thread1:第3步 重新获取的ab值:false
cas-thread1:第1步 进来时ab的值:false
线程cas-thread2释放资源====第2步 ab的值true //知道线程2释放资源,将ab改成true
线程cas-thread1:抢到资源 === ab的值false //线程1才能进来执行第二步
线程cas-thread1释放资源====第2步 ab的值true
和Synchronized
不一样,Synchronized
是通过加锁保证线程的安全,但是会让没有得到锁资源的线程进入blocked
状态,而在争夺到锁资源后恢复为runnable
状态,线程频繁的切换状态对性能代价比较高。
缺点二:
但是CAS不能保证代码块的原子性
CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。如需要保证多个变量原子性的更新,就不得不使用Synchronized
了。在jdk1.8中的concurrentHashMap
里使用的就是CAS+Synchronized
来保证线程的安全性,和性能的提高。
ABA问题(缺点三)
由于 CAS 设计机制是通过判断预期值A和内存地址V的值相同,就认为V没有被其他线程修改过。
ABA例子:
线程1:获取内存地址V得到预期值为"A"之后
线程2:对内存地址V的值进行修改成"B"
线程3:对内存地址V的值进行修改成"A"
导致线程1修改的时候判断内存地址V的值和预期值"A"相等,认为没有线程修改过内存地址V,就会对内存地址V的值进行修改,
并不能感知到有其他线程修改过的痕迹
ABA危害:
如果项目只在乎数值是否正确, 那么ABA 问题不会影响程序并发的正确性
单向链表实现的堆栈(经典例子):
ABA解决方案:
导致的原因:是CAS过程中只简单进行了“值”的校验,再有些情况下,“值”相同不会引入错误的业务逻辑(例如库存),有些情况下,“值”虽然相同,却已经不是原来的数据了。
解决方法:“版本号”的比对,一个数据一个版本,即使值相同,版本不同,也不应该进行修改成功。(每次修改,版本号都+1,版本号修改后是不会相同的)
并发情况下,判断一条数据是否修改过只需要判断版本号是否一致,来断定其记录是否被修改(避免去判断所有的字段是否修改过)
使用注意
Unsafe
类是CAS的核心(从源码中可以看出这个类是一个单例),由于Java无法直接访问底层系统,需要本地(native)方法进行访问,Unsafe
相当于一个后门,基于该类可以直接操作
内存中的数据.Unsafe
存在于sun.misc包中,其内部方法可以向C指针一样直接操作内存,因为Java的CAS执行依赖于Unsafe类的方法
注:Unsafe类中的所有方法都是native修饰的,也就是说,Unsafe类中的方法都是直接调用操作系统底层资源执行相应的任务,如果不能深刻理解CAS以及unsafe还是要慎用,尽量使用别人封装好的无锁类或者框架,Unsafe随便使用会导致指针异常的问题。