多线程(进阶二:CAS)

简介: 多线程(进阶二:CAS)



一、CAS的简单介绍

CAS的全称:“Compare And Swap”,字面意思是 “比较和交换”;一个CAS涉及到以下操作,有两个寄存器:A,SwapB,还有内存的值:AddressC。

先判断寄存器A是否和AddressC的值相同,相同,SwapB的值和AddressC的值进行交换,返回成功操作,否则返回失败操作。这里的交换值我们也可以理解成赋值操作,因为寄存器中的值我们不关心,用完就丢掉了,只关心内存中的值。

CAS逻辑(用伪代码来描述)

伪代码:

这里有两个寄存器:expectValue,swapValue,还有内存的值:address,初始化如图:

进入if语句,判断内存的值和寄存器e的值是否相等,如果相等,就交换寄存器swap和内存address的值,如图:

如图:

然后返回true,如果if条件不成立则返回false。

在计算机中,上述操作在计算机只是一条指令,因为单个指令的原因,所以CAS指令是原子的

CAS指令不涉及到加锁,阻塞。基于CAS指令,合理使用的话,在多线程中,我们可以实现无锁编程;因为之前我们讨论并发编程的线程安全问题时,是通过加锁,阻塞这样方式解决线程安全问题,因为会有阻塞,所以性能也就会降低,用CAS指令实现无锁编程,也能保证线程安全,不涉及到阻塞,这样性能就能得到很大的提升,在多线程编程中打开了新世界的大门。


二、CAS在多线程中简单的使用

因为CAS是CPU的指令,有的cpu可能不支持CAS,但主流的CPU(x86,arm...)都是支持的。

CAS本身是CPU的指令,操作系统对其做了封装,jvm又对操作系统提供的api又做了一层封装。java中CAS的api是放在unsafe中的,这个包名的意思,顾名思义也是“不安全”的意思,一般在java中不建议使用,java的标准库中,对于CAS进行了进一步的封装,把CAS的一些操作封装成工具类,供程序猿使用。而主要的一个工具,叫做 “原子类”。

java.util.concurrent.atomic 包下,里面的类就是基于上述方式实现的,典型的类就是AtomicInteger类,里面有很多方法可以实现数值的自增、自减,以及基本的加减操作。下面的代码案例也是使用AtomicInteger展示。

我们以前写过一个代码,让两个线程实现一个变量自增10_0000次,如下代码是线程不安全。

public class AtomicIntegerTest1 {
    public static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                count++;
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                count++;
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }
}

执行结果:

输出结果也和我们预期结果不同,肯定是线程不安全的,而原因就是因为count++操作不是原子的,在计算机中有三个指令

这里,我们使用CAS的方式,来实现让两个线程实现一个变量自增10_0000次,代码如下

public class AtomicIntegerTest1 {
    public static AtomicInteger count = new AtomicInteger(0);
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                count.getAndIncrement();//和count++意思一样
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                count.getAndIncrement();//和count++意思一样
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }
}

执行结果:

因为这里用CAS的方式,原本count++操作在计算机中有3条指令,不是原子的,肯定线程不安全;但是用CAS的方式就可以把像count++这样的操作,用一条指令完成,是原子的,在这里就不涉及到线程安全问题了。

AtomicInteger中有很多方法:自增,自减,+=,-=等待,这里就不展开讨论了。


三、原子类自增的代码分析

代码还是上述的代码,如下:

public class AtomicIntegerTest1 {
    public static AtomicInteger count = new AtomicInteger(0);
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                count.getAndIncrement();//和count++意思一样
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                count.getAndIncrement();//和count++意思一样
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }
}

在标准库中,getAndIncrement方法是这样的:

这里比较难理解,我们用伪代码的形式介绍,伪代码如下:

当有两个线程对同一个变量进行自增操作时,执行流程是这样的:

这里的oldValue是寄存器中的值,它的值就是AtomicInteger括号里面初始化的值,value是内存中的值。从上到下,把内存value的值都赋值给oldValue,两个线程拿到的都是同一个内存value的值。

这时,右边的线程while循环里的判断,先执行CAS这里的操作,如图:

       

执行完CAS操作后返回true,然后while循环里true != ture,条件不成立,不执行while循环内的代码。

当左边线程进入while循环里面的判断语句时,如图:

也会进入CAS操作,这里因为内存value的值修改了,当前线程寄存器oldValue值还是0,给oldValue+1,会返回false,这时循环条件false != true成立,就会执行oldValue = value操作,如图:

再次进入循环条件里面执行CAS操作,这时候value == oldValue,所以会让oldvalue+1赋值给value,这时候value的值就是2了,如图:

所以,两个线程不管顺序是啥样的,使用getAndIncrement方法都不会出现线程安全问题,因为CAS操作本身就是原子的,原因的逻辑理解也大概是下面这样的:

如果cas不成功,会重复上面的操作,再次读取数据,这次读取到的数据就是正确的了,cas也就能成功。意思就是这个方法里面会判断拿到的值是不是最新值,如果不是就去拿最新的值,再去CAS,这时候因为拿到的是最新值,所以这时能CAS成功。


四、CAS的ABA问题

举个栗子:我们现实生活中有无良商家会贩卖翻新机,而翻新机是较低价格回收的二手机,进行一系列翻新,变成和新机一样的外表,肉眼看不出来,但本质还是翻新机。

(1)CAS的ABA问题

而CAS中的ABA问题类似翻新机,CAS在使用的时候,主要关心的是内存上的值是否和CPU上寄存器的值相同,如果相同才进行对内存的赋值操作(交换另一个寄存器的值),不相同就不交换。如果一个线程在CAS操作时,穿插了其他线程的操作,内存上的值变换了两次:0 -> 100, 100 -> 0,内存中的值本质上是没改变的,但是它有改变的操作,进行CAS操作是感知不到内存的值是否有改变的;一般情况下,出现了上述情况,也不会出啥问题,不会产生BUG,但也有非常极端的情况,如下:

在ATM取钱的场景,你的银行卡里有1000块,要取500块,在点击取500块的时候卡了,没有反应,你又点了一下,总共点了两下,这时候就会产生两个线程,这里如果没有其他的一些极端情况,不会出问题,如下是伪代码进行分析:

但如果在右边线程的CAS操作完后,这里又有其他线程给balance变量加500,又变成了1000,就会出现问题,如图:

这样,就会导致,你存了500进去,但钱还是500,而能取出的钱也只有500,这个BUG就非常的严重,造成了吞钱的现象。这也就是ABA问题。

(2)解决ABA问题的方案

       1、约定数据变化只能是单向的(只能增加或只能减少),不能是双向的(既能增加又能减少)。

       2、对于本身就必须双向变化的数据,引入版本号,这个版本号的数字只能增加,不能减少

注意:上述思路不局限于CAS本身,有一些场景没有使用CAS,也可能会产生上述问题,但也可以使用上面的思路。


都看到这了,点个赞再走吧,谢谢谢谢谢

相关文章
|
Java 索引
多线程和并发编程(2)—CAS和Atomic实现的非阻塞同步
多线程和并发编程(2)—CAS和Atomic实现的非阻塞同步
86 1
多线程和并发编程(2)—CAS和Atomic实现的非阻塞同步
|
Java 编译器
解密Java多线程中的锁机制:CAS与Synchronized的工作原理及优化策略
解密Java多线程中的锁机制:CAS与Synchronized的工作原理及优化策略
|
6月前
|
算法 安全 Java
Java多线程基础-12:详解CAS算法
CAS(Compare and Swap)算法是一种无锁同步原语,用于在多线程环境中更新内存位置的值。
68 0
|
1月前
|
安全
【多线程】CAS、ABA问题详解
【多线程】CAS、ABA问题详解
20 0
|
6月前
|
存储 安全 Java
并发编程知识点(volatile、JMM、锁、CAS、阻塞队列、线程池、死锁)
并发编程知识点(volatile、JMM、锁、CAS、阻塞队列、线程池、死锁)
105 3
|
6月前
|
安全 Java 编译器
Java 多线程系列Ⅴ(常见锁策略+CAS+synchronized原理)
Java 多线程系列Ⅴ(常见锁策略+CAS+synchronized原理)
|
6月前
|
安全 Java
多线程(CAS, ABA问题, Runnable & Callable & 僵尸线程 & 孤儿进程)
多线程(CAS, ABA问题, Runnable & Callable & 僵尸线程 & 孤儿进程)
60 1
|
6月前
|
安全 Java 程序员
【Java多线程】面试常考——锁策略、synchronized的锁升级优化过程以及CAS(Compare and swap)
【Java多线程】面试常考——锁策略、synchronized的锁升级优化过程以及CAS(Compare and swap)
62 0
|
6月前
|
安全 Java API
JavaEE多线程】深入理解CAS操作:无锁编程的核心
JavaEE多线程】深入理解CAS操作:无锁编程的核心
48 0