【Java多线程进阶】CAS机制

这篇具有很好参考价值的文章主要介绍了【Java多线程进阶】CAS机制。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

【Java多线程进阶】CAS机制

前言

CAS指的是Compare-And-Swap(比较与交换),它是一种多线程同步的技术,常用于实现无锁算法,从而提高多线程程序的性能和扩展性。本篇文章具体讲解如何使用 CAS 的机制以及 CAS 机制带来的问题。

目录

1. 什么是CAS?

2. CAS的应用

2.1 实现原子类

2.2 实现自旋锁

3. CAS的ABA问题

3.1 ABA问题可能引起的BUG

3.2 解决ABA问题


【Java多线程进阶】CAS机制

1. 什么是CAS?

CAS 全名 compare and swap (比较并交换)是一种基于 Java 实现的 计算机代数系统,用于多线程并发编程时数据在无锁的情况下保证线程安全安全运行。

CAS机制 主要用于对一个变量(操作)进行原子性的操作,它包含三个参数值:需要进行操作的变量A、变量的旧值B、即将要更改的新值C。

CAS机制 会对当前内存中的 A 进行判断看是否等同于 B ,如果相等则把 A 值更改为 C ,否则不进行操作。以下为 CAS 操作的一段伪代码:

        boolean CAS(A,B,C) {
            if (&A == B) {
                &A = C;
                return true;
            }
            return false;
        }

当然,以上代码不具有原子性只是简单理解 CAS 的判定以及返回机制。真正的 CAS 只是一条 CPU 指令,相比于上述代码具有原子性 。

在了解 CAS 的基本判定后下面我们来看如何通过 Java 标准库来运用 CAS 。


2. CAS的应用


2.1 实现原子类

CAS 可以不加锁保证操作的原子性,Java 标准库提供了 Atomic + 包装类,相关的组合类来实现原子操作,这些类都是在 java.util.concurrent.atomic 包底下的。

【Java多线程进阶】CAS机制

以常用的 AtomicInteger 类来举例,AtomicInteger 类底下的 getAndIncrement 方法达到的效果就是自增类似于 i++ 操作,getAndDecrement 方法就是自减类似于 i-- 操作。

因此 AtomicInteger 类常见的方法有:

  • getAndIncrement 方法,自增操作,类似于 i++。
  • getAndDecrement 方法,自减操作,类似于 i--。
  • get 方法,获取当前 AtomicInteger 类引用的值。

当然,Atomic + 其他“数值”包装类也能使用以上方法!


代码案例,不使用 synchronized 的情况下保证一个线程自增5000,另一个线程也自增5000,最后返回两线程之和10000:

public static void main(String[] args) throws InterruptedException {
        //初始化number为0
        AtomicInteger number = new AtomicInteger(0);
        //线程1使number自增5000次
        Thread thread1 = new Thread(()->{
            for (int i = 0; i < 5000; i++) {
                number.getAndIncrement();
            }
        });
        //线程2也使number自增5000次(在线程1执行后)
        Thread thread2 = new Thread(()->{
            for (int i = 0; i < 5000; i++) {
                number.getAndIncrement();
            }
        });
        thread1.start();//启动线程1
        thread2.start();//启动线程2
        thread1.join();//等待线程1执行完毕
        thread2.join();//等下线程2执行完毕
        System.out.println(number.get());//输出number的值
    }

 运行后打印:

【Java多线程进阶】CAS机制

以上代码,在不使用锁(synchronized)的情况下保证了线程的安全性。其底层运用的就是 CAS 机制,getAndIncrement 方法的具体实现,我们可以参考以下 伪代码 来理解:

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

假设 getAndIncrement 方法被两个线程同时调用,线程1 和 线程2 的 oldValue 值都为 0,内存中的 value 值为0。

【Java多线程进阶】CAS机制

1)线程1 进入了 getAndIncrement 方法,此时线程1进行 CAS 判定,发现线程1的 oldValue = value,就把 value 进行自增。

【Java多线程进阶】CAS机制

2) 线程2 进入了 getAndIncrement 方法,此时 线程2 进行 CAS 判定,发现 oldValue != value,进入 while 循环,把 value 赋值给 old Value。

【Java多线程进阶】CAS机制

3)经过以上判断后,线程2 再次进行 CAS 判断时,发现 oldValue = value 了,此时的 value 值又会自增。

以上的 伪代码 就能实现一个原子类,里面的 getAndIncrement 方法也是具备原子性的。通过上述图例就能很好的理解。


2.2 实现自旋锁

CAS的自旋锁指的是在使用CAS操作时,当CAS操作失败后,线程不直接阻塞等待,而是继续尝试执行CAS操作,即对前一次CAS操作的失败进行重试,直到CAS操作成功为止。

自旋锁的意思是程序使用循环来等待特定条件的实现方式,相较于传统的阻塞锁,自旋锁不会使线程进入阻塞状态,因此避免了线程上下文切换带来的开销。通常,当线程竞争的资源空闲等待的时间不长,自旋锁是一种比较高效的同步机制。

CAS 自旋锁体现:一段 伪代码

public class SpinLock {
  private Thread owner = null;
  public void lock(){
    // 通过 CAS 看当前锁是否被某个线程持有.
    // 如果这个锁已经被别的线程持有, 那么就自旋等待.
    // 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程.
    while(!CAS(this.owner, null, Thread.currentThread())){
   }
 }
  public void unlock (){
    this.owner = null;
 }
}

Thread.currentThread() 为当前对象的引用,以上代码进行 CAS 判定时:

  • 如果判断 this.owner 为空,则把当前对象的引用赋值给 this.owner。此时 CAS 方法返回 true,并取反,while 循环退出。
  • 判断 this.owner 不为空,则不做任何操作,CAS 方法返回 false,并取反,while 循环继续执行。由于 while 循环体内没有任何内容,while 条件判断会执行很快,直到 this.owner 加锁成功为止。

这就是自旋锁的体现,关于锁的策略在本专栏中有详细讲解。大家可以前去查找。


3. CAS的ABA问题

ABA 问题是:当线程1首先读取到共享变量值A。然后线程2先把这个共享变量值修改为B,再修改回A。

此时其他线程再进行 CAS 操作时误以为共享变量值没有被修改过,从而成功的将共享变量更改为新值。

但实际过程中共享变量经历了 由 A 变为 B,再由 B 变为 A,这样就可能会导致一些问题。

类似于,网上购买一部二手机。买的时候,卖家说是零件完好,到手后才发现是一部翻新机。这样就会导致手机用不了几天就出问题。至于到手之前,卖家不说是识别不出这部手机的好坏的。


3.1 ABA问题可能引起的BUG

ABA 问题,就是 CAS 机制导致的数据反复横跳。

假设,张三要去 ATM 取钱,张三余额有 1000 元,他要取 500 元。他安排两个线程,线程1 和 线程2 来并发执行取钱操作。

预期效果:线程1 执行取钱操作判断余额为 1000,执行余额 -500 操作,此时余额 500,线程2 处于阻塞等待状态。当 线程2 执行取钱操作判断余额不是 1000 不执行 -500 操作。

ABA问题出现:线程 1 执行取钱操作判断余额为 1000,执行余额 -500 操作,此时余额 500,线程2 阻塞等待状态。突然,张三的朋友给他转账了 500 ,此时 余额又变回了 1000。

线程2 进入取钱操作时,判断余额为 1000 元,执行余额 -500 操作,此时余额剩余 500。这就是 ABA 问题造成的后果,张三回家后打开手机查看余额剩余 500,实际张三被 ABA 问题坑了 500元。

【Java多线程进阶】CAS机制


3.2 解决ABA问题

CAS 操作,是将需要改变的值 A 与旧值 B 进行比较,相等则把新值 C 赋值给 A ,否则不做改变。解决 CAS 出现 ABA 问题,我们可以引入一个版本号,比较版本号是否符合预期。

比如在网上购买一部二手机,卖家会将手机的翻新程度进行一个版本号标记,翻新1次记版本号1,翻新2次的记版本号2,以此类推。这时候,客户会根据版本号来选择翻新程度相应的手机。

  • 当版本号和读到的版本号相等,则修改数据,并把版本号 + 1。
  • 当版本号高于读到的版本号,就操作失败(认为数据已经被修改过了)

根据以下 伪代码 来理解:

num = 0;
version = 1;
old = version;
CAS(version,old,old+1,num);

public void CAS(version,oldVersion,oldVersion+1,num){
    if(version == oldVersion) {
        version = oldVersion + 1;
        num++;
    }
}

对以上代码进行一个讲解, version 作为版本号,当 version 版本号等于读到的 oldVersion 版本号,则把 oldVersion +1 赋值给 version,并且 num ++ 。这样就能避免 ABA 问题的出现。


当然,Java 中 提供了一个 AtomicStampedReference<>类,这个类可以对某个类进行保证,这样就能提供上述的版本号管理功能。

public class TestDemo {
    private static final AtomicStampedReference<Integer> sharedValue = new AtomicStampedReference<>(10, 0);
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> {
            int expectedStamp = sharedValue.getStamp();
            int newValue = 20;
            sharedValue.compareAndSet(10, newValue, expectedStamp, expectedStamp + 1);
            System.out.println(Thread.currentThread().getName() + " updated sharedValue to " + newValue);
        }, "Thread-1");

        Thread thread2 = new Thread(() -> {
            int expectedStamp = sharedValue.getStamp();
            int oldValue = sharedValue.getReference();
            int newValue = 30;
            sharedValue.compareAndSet(oldValue, newValue, expectedStamp, expectedStamp + 1);
            System.out.println(Thread.currentThread().getName() + " updated sharedValue to " + newValue);
        }, "Thread-2");

        thread1.start();
        thread1.join();
        thread2.start();
        thread2.join();
        
        System.out.println("final value: " + sharedValue.getReference());
    }
}

运行后打印:

【Java多线程进阶】CAS机制

以上代码,共享变量的初始值为10,然后线程1将共享变量的值修改为20,线程2将共享变量的值修改为30。由于AtomicStampedReference类包含版本号信息,因此即使共享变量的值在这个过程中发生了ABA的变化,CAS操作也可以正常进行,不会出现误判现象。


谈谈你对 CAS 机制的理解?

CAS 全称 compare and swap 即比较并交换,它通过一个原子的操作完成“读取内存,比较是否相等,修改内存”这三个步骤,本质上需要 CPU 指令的支持。


ABA 问题如何解决?

我们可以给修改的数据加上一个版本号,初始化当前版本号与旧的版本号相等。判断当前版本号如果等于旧版本号则对数据进行修改,并使版本号自增。判断当前版本号大于旧版本号,则不进行任何操作。


🧑‍💻作者:一只爱打拳的程序猿,Java领域新星创作者,阿里云社区优质创作者、专家博主。

📒博客主页:这是博主的主页 

🗃️文章收录于:Java多线程编程 

🗂️JavaSE的学习:JavaSE 

🗂️Java数据结构:数据结构与算法 

【Java多线程进阶】CAS机制

 本篇博文到这里就结束了,感谢点赞、收藏、评论、关注~文章来源地址https://www.toymoban.com/news/detail-495437.html

到了这里,关于【Java多线程进阶】CAS机制的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 深入浅出Java多线程(十):CAS

    大家好,我是你们的老伙计秀才!今天带来的是[深入浅出Java多线程]系列的第十篇内容:CAS。大家觉得有用请点赞,喜欢请关注!秀才在此谢过大家了!!! 在多线程编程中,对共享资源的安全访问和同步控制是至关重要的。传统的锁机制,如synchronized和ReentrantLock等

    2024年03月11日
    浏览(38)
  • Java多线程(3)---锁策略、CAS和JUC

    目录 前言 一.锁策略 1.1乐观锁和悲观锁 ⭐ 两者的概念 ⭐实现方法 1.2读写锁  ⭐概念 ⭐实现方法 1.3重量级锁和轻量级锁 1.4自旋锁和挂起等待锁 ⭐概念 ⭐代码实现 1.5公平锁和非公平锁 1.6可重入锁和不可重入锁 二.CAS 2.1为什么需要CAS 2.2CAS是什么 ⭐CAS的介绍 ⭐CAS工作原理

    2024年02月13日
    浏览(36)
  • 通俗解释 JVM CAS 机制

    JVM 的 CAS,即 Compare And Swap,是一种并发编程中常用的保证原子性的技术。 通俗地讲,CAS 用于在多线程环境下对共享变量的值进行原子操作,即多个线程同时访问同一个共享变量,但只有一个线程能修改这个变量的值,并且这个操作是原子的,不会被其他线程打断。 具体来说

    2023年04月10日
    浏览(25)
  • 【多线程】CAS

    ✨个人主页:bit me👇 ✨当前专栏:Java EE初阶👇 CAS 是操作系统/硬件,给 JVM 提供的另外一种更轻量的原子操作的机制 CAS: 全称Compare and swap,字面意思: \\\" 比较并交换 \\\",一个 CAS 涉及到以下操作: 比较内存和寄存器的值 ,如果 相等 ,则 把寄存器和另一个内存中的值进行交

    2023年04月26日
    浏览(20)
  • Java之多线程进阶

    目录 一.上节内容复习 1.线程池的实现 2.自定义一个线程池,构造方法的参数及含义 3.线程池的工作原理 4.拒绝策略 5.为什么不推荐系统提供的线程池 二.常见的锁策略 1.乐观锁和悲观锁 2.轻量级锁和重量级锁 3.读写锁和普通互斥锁 4.自旋锁和挂起等待锁 5.可重入锁和不可重入

    2024年02月05日
    浏览(34)
  • Java 进阶(12) 线程通信

    多个线程在处理同⼀个资源,但是处理的动作(线程的任务)却不相同。 为什么要处理线程间通信 多个线程并发执⾏时, 在默认情况下CPU是随机切换线程的,当我们需要多个线程来共同完成⼀件任务,并且我们希望他们有规律的执⾏, 那么多线程之间需要⼀些协调通信,以此

    2023年04月16日
    浏览(25)
  • 【Java】多线程(进阶)

    乐观锁 乐观锁的基本思想是假设在数据的读取和修改过程中不会有其他的线程对其进行修改, ,因此乐观锁不会立即对数据进行加锁,而是在更新数据时检查是否发生了冲突,如果发现冲突(即数据被其他线程修改),则会进行回滚操作,乐观锁通常使用版本号,时间戳等机制来实现 优

    2024年02月10日
    浏览(25)
  • Java 进阶(7) 创建线程

    1. 定义Thread类的⼦类,并重写该类的run()⽅法,该run()⽅法的⽅法体就代表了线程需要完成的任务,因此把run()⽅法称为线程执⾏体。 2. 创建Thread⼦类的实例,即创建了线程对象 3. 调⽤线程对象的start()⽅法来启动该线程 示例: 测试: 1. 定义Runnable接⼝的实现类,并重写该接⼝

    2023年04月16日
    浏览(29)
  • 一篇搞懂Java多线程运行机制

    Java是一种支持多线程编程的语言。多线程可以让程序同时执行多个任务,从而提高程序的效率和响应速度。在本篇博客中,我将介绍Java多线程的基础知识,包括线程的创建、启动、中断以及线程同步等方面。 什么是程序? 程序是为完成特定任务,用某种语言编程写的一组指

    2023年04月15日
    浏览(27)
  • Java 进阶(8) 线程常用方法

    方法名 说明 public static void sleep(long millis) 当前线程主动休眠 millis 毫秒。 public static void yield() 当前线程主动放弃时间⽚,回到就绪状态,竞争下⼀次时间⽚。 public final void join() 允许其他线程加⼊到当前线程中。 public void setPriority(int) 线程优先级为1-10,默认为5,优先级越⾼,

    2023年04月16日
    浏览(29)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包