java EE 初阶 — CAS 的介绍

文章目录

  • CAS
    • 1. 什么是 CAS
    • 2. CAS 是怎么实现的
    • 3. CAS 有哪些应用
      • 3.1 实现原子类
      • 3.2 实现自旋锁
    • 4. CAS 的 ABA 问题
      • 4.1 什么是 ABA 问题
      • 4.2 ABA 问题引来的 BUG
      • 4.3 解决方案
    • 5. 相关面试题

CAS

1. 什么是 CAS


CAS:全称 Compare and swap,字面意思:”比较并交换“

一个 CAS 涉及到以下操作:

我们假设内存中的 原数据V旧的预期值A,需要修改的新值B

  1. 比较 A 与 V 是否相等。(比较)
  2. 如果比较相等,将 B 写入 V。(交换)
  3. 返回操作是否成功。

举个例子。

java EE 初阶 — CAS 的介绍_第1张图片

2. CAS 是怎么实现的


此处最特别的地方,上述这个 CAS 的过程并非是通过一段代码实现的,
而是通过一条 CPU 指令完成的。

因为 CAS 操作是原子的,所以就可以在一定程度上回避线程安全问题。

解决线程安全问题,除了加锁之外,又有了一个新的方向了。

CAS 可以理解为是 CPU 给咱们提供的一个特殊指令,通过这个指令,就可以一定程度上处理线程安全问题。

观察下面一段伪代码(只是辅助理解CAS 的工作流程)

boolean CAS(address, expectValue, swapValue) {
    if (&address == expectedValue) {
        &address = swapValue;
        return true;
    }
    return false;
}


address 就相当于是上面例子的 V,expectValue 相当于是 A
swapValue 相当于是 B。

3. CAS 有哪些应用

3.1 实现原子类


java 标准库中提供了 java.util.concurrent.atomic 包,里面的类都是基于这种方式来实现的。

典型的就是 AtomicInteger 类。
其中:
getAndIncrement 相当于 i++ 操作,
incrementAndGet 相当于是 ++i 操作。
getAndDecrement 相当于是 i-- 操作,
decrementAndGet 相当于是 --i 操作。

package thread;

import java.util.concurrent.atomic.AtomicInteger;

public class ThreadDemo8 {

    public static void main(String[] args) throws InterruptedException{
        // 这些原子类,就是基于 CAS 实现了自增,自减操作,此时进行这类操作不加锁,也是线程安全的
        AtomicInteger count = new AtomicInteger(0);

        // 使用原子类来解决线程安全问题
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                count.getAndIncrement();// 相当于是 count++
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                count.getAndIncrement();// 相当于是 count++
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println(count.get());
    }
}


因为进行这类操作不加锁是,也是线程安全的,所以代码会输出 10000。

java EE 初阶 — CAS 的介绍_第2张图片

针对上面 count++ 操作的伪代码实现:

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


代码分析

箭头从上到下代表执行的顺序。

java EE 初阶 — CAS 的介绍_第3张图片

1、t1 先执行 load ,把 value 的值读到 oldvalue 中。

读取后

java EE 初阶 — CAS 的介绍_第4张图片


2、执行 t2 的 load 操作,把 value 的值 读到 oldvalue 中

执行后

java EE 初阶 — CAS 的介绍_第5张图片


3、t2 执行 CAS 操作比较 oldvalue 和 value 的值是否相等

此时 value 与 oldvalue 的值相等,就将 oldValue+1 的值读给 value。

读取后

java EE 初阶 — CAS 的介绍_第6张图片


4、t1 执行 CAS 操作比较 oldvalue 和 value 的值是否相等

此时 value 与 oldvalue 的值不相等,CAS 返回 false,并且不进行任何交换。

  while ( CAS(value, oldValue, oldValue+1) != true) {
      oldValue = value;
  }

针对于上述的代码,CAS 返回 false 后,进入 while 循环,将 value 的值 读给 oldvalue。

紧接着 t1 就要再接着执行一次 load 和 CAS。

读取后。

java EE 初阶 — CAS 的介绍_第7张图片


5、t1 执行 CAS 操作比较 oldvalue 和 value 的值是否相等

此时 value 与 oldvalue 的值相等,就将 oldValue+1 的值读给 value。

读取后

java EE 初阶 — CAS 的介绍_第8张图片

此时比较相等,CAS 返回 true ,循环就结束了。

上面的伪代码要结合下面的图示来理解。

3.2 实现自旋锁


基于 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;
    }
}


this.owner, null 监测当前的 owner 是否是 null ,如果是 null就进行交换,
也就是把当前线程的引用赋值给 owner 。
如果赋值成功,此时循环结束,加锁完成了。

如果当前锁已经被别的线程占用了,CAS 就会发现 this.owner 不是 null。
CAS 就不会产生赋值,也同时返回 false ,循环继续执行,并且下次判定。

4. CAS 的 ABA 问题

4.1 什么是 ABA 问题


CAS 在运行中的核心,检查 value 和 oldvalue 是否一致。
如果一致就视为 value 中途没有被修改过,所以进行下一步操作是没有问题的。

这里指的一致,可能是没有改过,也可能改过之后又还原回来了。

比方说,把 value 的值设为 A 的话,CAS 判定 value 为 A 此时可能确实 value 始终是 A。
也可能是 value 本来是 A ,被改成了 B 后又改回了 A 。

举个例子。

就像是买手机,我买到的这个手机,可能是翻新机,也可能是新机。
不管是哪一种,我都无法区分。

4.2 ABA 问题引来的 BUG


ABA 这个情况大部分情况下其实是不会对代码逻辑产生太大影响的。

但是不排除一些比较极端情况,也是可能造成影响的。

下面举一个在实际开发环境中发生概率不大的例子

假设当前滑稽老铁的账户余额 为 1000 ,滑稽准备去 500 。

当按下取款这一瞬间的时候,机器卡了一下,滑稽老铁忍不住就多按了几下。
这是可能就会产生 bug ,可能就会触发重复扣款的操作。

考虑使用 CAS 的方式来扣款。

java EE 初阶 — CAS 的介绍_第9张图片

t1 和 t2 都执行 load 操作,都读取到了 1000 这个值。
紧接着 t1 执行 CAS 比较一下,看看此时的余额是不是 1000,如果是 1000 就扣 500。

当 t2 执行 CAS 操作的时候,滑稽的余额为 500 不等于 1000,此时扣款不成功。

如果在执行果第一个 CAS 后,有人给滑稽转了 500 元,此时余额又变成了 1000。
这就相当于,本来是 1000 变成 500 后 又变成了 1000 ,这就是一个ABA 问题。

java EE 初阶 — CAS 的介绍_第10张图片

此时的余额是 1000 ,就又扣款成功了,这里就出现 bug 了。

4.3 解决方案


针对当前的问题,采取的方案就是 加一个版本号

比方说,初始版本号是 1 ,每次修改版本号都加1,然后进行 CAS 的时候,不是以金额为准了,而是以版本号为准。

版本号真是增长的,不能降低。此时只要是版本号没变,就是一定没有发生改变。

5. 相关面试题


1、讲解下你自己理解的 CAS 机制

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


2、ABA问题怎么解决

给要修改的数据引入版本号。在 CAS 比较数据当前值和旧值的同时,也要比较版本号是否符合预期。
如果发现当前版本号和之前读到的版本号一致,就真正执行修改操作,并让版本号自增;如果发现当
前版本号比之前读到的版本号大,就认为操作失败。

你可能感兴趣的:(java,EE,从入门到进阶,java-ee,java,多线程)