CAS无锁机制原理解析

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无锁机制
列如AtomicBooleanAtomicInteger等它们分别用于BooleanInteger类型的原子性操作。
下面的一段代码,更能体现出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随便使用会导致指针异常的问题。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值