当前位置: 首页 > news >正文

多线程 —— CAS 原理

目录

1.什么是CAS

2.CAS 的应用

2.1 实现原子类

2.2 实现乐观锁

3.ABA 问题

3.1 理解 ABA 问题

3.2 版本号机制

4.小结


1.什么是CAS

CAS,全称是 Compare And Swap,也就是“比较并交换”的意思,用于实现乐观锁。

CAS 涉及到三个操作数:

假设内存中的原数据值是V,预期值(我们认为的值)是E,需要把数据值V修改为新值A

  • 比较 E 与 V 是否相等
  • 如果相等,更新原数据值 V 为新值 B
  • 返回操作是否成功

可以借助一个伪代码来理解:

boolean CAS(V, E, A) {

              if (&V == E) {

                     &V = A;

                     return true;

             }

        return false;

}

伪代码只是帮助理解,实际上这里的操作是原子的,只有一个指令。

比如,两个线程要修改一个变量 m 的值为 5,m 的初始值为 1 (V = 1,E = 1,A = 5),假设不存在 ABA 问题的情况下(ABA问题再后面讲到),那么就有:

(1) m 与 1 进行比较,如果相等,说明 m 变量没有被其它线程修改,可以把 m 的值更新为 5 。

(2)m 与 1 进行比较,如果不相等,说明 m 变量已经被其它线程修改,放弃更新 m 的值。

CAS 其实很简单,但初学可能难以理解,下面借助 CAS 的应用来理解。

2.CAS 的应用

2.1 实现原子类

在前面学习多线程时,有两个线程对同一个变量各自增 5000 次(count++),期望结果是 count = 10000,当时是借助锁来实现,即保证 count++ 操作的原子性。

在这里,CAS可以在不加锁的情况下也能保证操作的原子性,Java标准库中提供了 java.tuil.concurrent.atomic 包,里面的类都基于这种方式来实现,也就是 Atomic + 包装类。

上述 count++ 操作就是典型的 AtomicInteger 类,使用时需要实例化,其中,getAndIncrement() 相当于 ++ 操作。

先来看代码:

public class Test {private static AtomicInteger count = new AtomicInteger(0);//创建count这个对象,并赋初始值为0public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {for (int i = 0; i < 5000; i++) {count.getAndIncrement();}});Thread t2 = new Thread(() -> {for (int i = 0; i < 5000; i++) {count.getAndIncrement();}});//线程启动t1.start();t2.start();//先等等待。先让线程1和线程2执行,当值主线程main已经执行完毕t1.join();t2.join();System.out.println("count = " + count);}
}

运行结果:

在代码中,我们并没有加锁,但是结果是符合预期的,这同样保证了线程安全。原子类的底层应用就是 CAS 机制。

伪代码实现就是:

class MyAtomicInteger {
    private int value;
    
    public int getAndIncrement() {
        int oldValue = value;
        while ( CAS( value, oldValue, oldValue + 1)  !=  true ) {
            oldValue = value;
        }
        return oldValue;
    }

}

关键步骤:

  1. 读取当前 value作为 oldValue
  2. 通过循环尝试用 CAS 操作将 value从 oldValue更新为 oldValue + 1
  3. 如果 CAS 失败(说明其他线程修改了value),则重新读取value并重试,直到成功。
  4. 返回修改前的旧值(oldValue

下面通过画图来理解:

2.2 实现乐观锁

伪代码:

public class OptimismLock {
     private Thread owner = null;
     public void lock(){
          while(!CAS(this.owner, null, Thread.currentThread())){
     }
 }
  public void unlock (){
      this.owner = null;
      }

}

在while循环中,先通过 CAS 查看当前锁是否被线程持有,如果已经被线程持有,就自旋等待,如果没有被线程持有,就把 owner 设为当前正在尝试获取锁的线程

3.ABA 问题

3.1 理解 ABA 问题

假设有两个线程要修改同一个变量A,线程1先获取到变量A,并记录到自己的内存中,然后使用 CAS 判定当前主内存值是否为 A,如果是A,就修改为 B。这看似没有我们问题,但是,再线程1执行这两个操作的中间,线程2先把主内存的变量A修改为B,再把B修改为A,也就是线程2对主内存的共享变量进行了两次修改,而线程1并不知道这个共享变量是否已经修改过,所有会继续修改这个共享变量,把它变为B。这就是典型的 ABA 问题。

可能你觉得这并没有什么影响,可以想象一下你的银行卡里有 1000 块钱。有一天你要去取 500 块钱,取款机创建了两个线程,线程1你正常取出 500 块钱,线程2阻塞等待,线程1的操作结束后,也就是你取出了 500 块钱(银行卡扣款500)。轮到线程2,但在线程2判断银行卡余额和之前读到的余额之前,你的朋友又给你转了 500 块钱,也就是钱又变成了 1000 块钱,此时线程2会以为银行卡余额没有被修改,于是又给你扣款500块钱,相当于这两波操作下来,取款机扣款了两次,卡里只剩500块钱了,但你朋友又给你转了500款前,卡里的余额应该是1000才对!这个钱就是被取款机吞了,这就是ABA问题。

怎么解决ABA问题呢?

3.2 版本号机制

想要解决ABA问题,就要引入版本号机制

版本号机制的原理就是:引入一个变量 stamp,表示数据被修改的次数。在使用 CAS 比较数据的当前值的初始值时,也比较版本号是否符合预期。也就是说,真正记录修改时,

  • 如果当前的版本号和读到的版本号相同,就更新数据,并让 stamp + 1
  • 如果当前的版本号和读到的版本号不相同,放弃更新数据

假设有这么一个场景,你家里用一个冰箱,冰箱里有一瓶可乐,你和你的弟弟约定,谁喝了可乐,谁就要马上补一瓶可乐放进冰箱里。这样,不论你什么时候去看,冰箱里总会有一瓶可乐,你很难判断这瓶可乐是新放进去的还是原理的可乐,但是你有很多种方法可以判断,比如生产日期、编号等,因为生产日期肯定是不一样的,即使是同一天生产,也有时间上的差异,编号就更不用说了。只不过版本号在这里相当于一个计数器,也就是每拿一瓶可乐,你和你的弟弟就约定在旁边的计数纸上+1,这样你就可以通过这个号码判断可乐是否已经被替换过。

在Java标准库中,提供了 AtomicStampedReference<E> 类,主要方法是 compareAndSet()

具体用法可以看以下代码:

public class Test {private static AtomicStampedReference<Integer> pair = new AtomicStampedReference<>(10, 66);// 初始值为10,没有被修改过,为了理解,这里版本号初始设置为66public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {int getCurrentValue = pair.getReference();// 获取变量值int getCurrentStamp = pair.getStamp();// 获取当前的版本号pair.compareAndSet(getCurrentValue, 20, getCurrentStamp, getCurrentStamp + 1);// 在当前的变量值+10,更新版本号+1});Thread t2 = new Thread(() -> {int getCurrentValue = pair.getReference();// 获取变量值int getCurrentStamp = pair.getStamp();// 获取当前的版本号pair.compareAndSet(getCurrentValue, 20, getCurrentStamp, getCurrentStamp + 1);// 在当前的变量值+10,更新版本号+1});// 启动线程t1.start();t2.start();// 等待线程t1.join();t2.join();System.out.println("值 = " + pair.getReference() + " 版本号 = " + pair.getStamp());// 结果 值 = 20 版本号 = 67// 因为是两个线程都对 pair 进行了修改,而且都是把新值修改为20。// 这种情况下,因为初始值是10,线程1和线程2不论哪个线程先执行,都会把值更新为20,并且版本号+1,版本号更新为67// 但是后面执行的线程,发现主内存要是之已经是20,而要更新的值由恰好是20,将不再更新// 也就是版本号不变,依然是67}
}

版本号机制可以解决 CAS 的典型问题—— ABA 问题,这样就避免是余额被吞的情况!

4.小结

本期主要讲 CAS 原理,通过一个原子的操作完成“读取内存,比较是否相等,修改内存”,并通过引入版本号机制解决 ABA 问题。


下期分享 JUC 的常见类,JUC 就是 java.util.concurrent.* 包的所写,是Java并发编程的核心工具包,在前面多线程的学习中,也有接触到一些,比如线程池,本期的原子类等。在前面学习创建线程时,我们知道有继承 Thread 类、实现 Runnable 类,lambda 表达式和线程池,在下期还会再学习一种创建方式,以及在不加锁也不使用原子类的情况下也可以实现两个线程各自增5000次得到的最终结果也是10000,这些都会在下期讲到。

欲知后事如何,且听下回分解!

http://www.dtcms.com/a/474152.html

相关文章:

  • 兰州做网站的有哪几个网站后台登陆不了
  • css实现表格中最后一列固定
  • 优秀原创设计网站门户系统设计
  • linux环境docker如何让启动的容器在后台运行
  • 软考架构师高分避坑指南:三科实战拆解与破局之道
  • 新手向C语言JavaPython 的选择与未来指南
  • 摄影网站难做吗网站如何集成微信支付
  • Redis5安装与核心命令详解
  • 单个请求中同时使用 multipart/form-data 和 application/json 的可行性及正确实现方式
  • wordpress网站配置甜妹妹福利wordpress
  • 新奇的Word表格单元格合并
  • 网站建设模板系统网站漂浮广告怎么做
  • 【如何解决“支付成功,但订单失败”的分布式系统难题?】
  • MQTT系列(三)
  • app开发流程表北京网站优化快速排名
  • 衡石科技嵌入式BI:重构企业应用的数据智能生态
  • rdd数据存储在spark内存模型中的哪一部分
  • 肥西县重点工程建设管理局网站支付宝 收费 网站开发
  • [webgl]基于THREEJS开发的sdk,使开发三维效果更加的容易
  • [Java、C语言、Python、PHP、C#、C++]——深度剖析主流编程语言的核心特性与应用场景
  • Deployment 和 StatefulSet 的区别
  • 广州自助网站制作网站开发成app
  • LeetCode 396 - 旋转函数 (Rotate Function)
  • 服装公司网站策划书网站无法连接服务器
  • 【C++篇】:LogStorm——基于多设计模式下的同步异步高性能日志库项目
  • php怎么做网站怎么做试玩平台推广网站
  • go语言:在 Win10上,如何编译 ffuf-v2.1.0?
  • 做网站没装数据库建站 网站程序
  • 有哪些做高考模拟卷的网站做第一个网站什么类型
  • Maven 设置项目编码,防止编译打包出现编码错误