CAS比较并交换


CAS(Compare-And-Swap,比较并交换)是一种用于实现多线程同步的无锁编程技术,主要用于解决并发编程中的数据一致性问题。它是一种原子操作,通常用于实现线程安全的算法,而无需使用传统的锁机制(如互斥锁)。

在并发编程中,代码的“原子性”指的是一个操作在执行过程中不会被其他线程中断,即要么全部完成,要么全部不完成

如果一个操作不是原子性的,那么在多线程环境中可能会导致数据不一致或其他并发问题。

CAS 伪代码

这里的代码不是原⼦的,真实的 CAS 是⼀个原⼦的硬件指令完成的,这个伪代码只是辅助理解 CAS 的⼯作流程。

boolean CAS(address, expectValue, newValue) {
    if (&address == expectedValue) {
        &address = newValue;
        return true;
    } else {
        return false;
    }
}
  1. CAS操作涉及三个参数:
    1. 内存位置(address):要更新的变量在内存中的地址。
    2. 预期值(expectedValue):期望内存位置当前的值。
    3. 新值(newValue):如果内存位置的值等于预期值,则将该位置更新为新值。
  2. CAS 的操作流程如下:
    1. 比较内存中的值address和预期值expectedValue
    2. 如果相等,则将内存中的值address更新为新值newValue,并返回成功;
    3. 如果不相等,则不进行任何修改,并返回失败。

注意:

当多个线程同时对某个资源进⾏ CAS 操作,只能有⼀个线程操作成功,但是并不会阻塞其他线程,其他线程只会收到操作失败的信号。

CAS 可以视为是⼀种乐观锁,或者可以理解成 CAS 是乐观锁的⼀种实现⽅式。

两种典型的不是“原⼦性”的代码:

check and set(if 判定然后设定值)

public class NonAtomicCheckAndSet {
    private int value;

    public void setValueIf(int expectedValue, int newValue) {
        if (value == expectedValue) { // 非原子性检查
            value = newValue; // 非原子性更新
        }
    }

    public int getValue() {
        return value;
    }

    public static void main(String[] args) {
        NonAtomicCheckAndSet example = new NonAtomicCheckAndSet();
        example.setValueIf(0, 1); // 假设value初始值为0
    }
}

问题:

在多线程环境下,if判断和赋值操作不是原子性的。可能存在以下问题:

  1. 线程1读取value为0,判断条件成立。
  2. 在线程1执行value = newValue之前,线程2value修改为其他值。
  3. 线程1继续执行value = newValue,此时value的值可能已经不符合预期条件了。

解决方案:

使用AtomicIntegercompareAndSet方法来实现原子性操作:

public final boolean compareAndSet(int expect, int update)
  • expect是当前值的预期值,update是想要更新的新值。
  • 如果当前值等于预期值,则将当前值更新为新值,并返回true
  • 否则不进行更新,返回false
import java.util.concurrent.atomic.AtomicInteger;

public class AtomicCheckAndSet {
    private AtomicInteger value = new AtomicInteger(0);

    public void setValueIf(int expectedValue, int newValue) {
        value.compareAndSet(expectedValue, newValue); // 原子性操作
    }

    public int getValue() {
        return value.get();
    }

    public static void main(String[] args) {
        AtomicCheckAndSet example = new AtomicCheckAndSet();
        example.setValueIf(0, 1); // 假设value初始值为0
    }
}

read and update (i++)

public class NonAtomicReadAndUpdate {
    private int counter;

    public void increment() {
        counter++; // 非原子性操作
    }

    public int getCounter() {
        return counter;
    }

    public static void main(String[] args) {
        NonAtomicReadAndUpdate example = new NonAtomicReadAndUpdate();
        example.increment(); // 假设counter初始值为0
    }
}

问题:

在多线程环境下,counter++操作不是原子性的。counter++实际上是一个复合操作,包括以下步骤:

  1. 读取counter的当前值。
  2. 将当前值加1。
  3. 将新值写回counter

所以,可能会出现以下问题:

  1. **线程1 **读取counter的值为0。
  2. **线程2 **读取counter的值也为0。
  3. 线程1 将counter的值加1,更新为1。
  4. 线程2 也将counter的值加1,更新为1。

最终,counter的值可能只有1,而不是预期的2。

解决方案:

使用AtomicIntegergetAndIncrement方法来实现原子性操作:

**incrementAndGet**:相当于 ++i

**getAndIncrement**:相当于 i++

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicReadAndUpdate {
    private AtomicInteger counter = new AtomicInteger(0);

    public void increment() {
        counter.getAndIncrement(); // 原子性操作
    }

    public int getCounter() {
        return counter.get();
    }

    public static void main(String[] args) {
        AtomicReadAndUpdate example = new AtomicReadAndUpdate();
        example.increment(); // 假设counter初始值为0
    }
}

CAS 是怎么实现的?

针对不同的操作系统,JVM ⽤到了不同的 CAS 实现原理:

  1. Java 层面:**Unsafe**
    1. 在 Java 中,CAS 操作通常通过 sun.misc.Unsafe 类提供的本地方法来实现。这个类提供了低级别的内存操作和原子操作支持。
    2. Unsafe 类中的 compareAndSwapInt, compareAndSwapLong, 和 compareAndSwapObject 方法分别用于对 intlong、对象类型的变量进行 CAS 操作。
public final boolean compareAndSet(int expectedValue, int newValue) {
    return unsafe.compareAndSwapInt(this, valueOffset, expectedValue, newValue);
}
  1. JVM 层面:**Atomic::cmpxchg**
    1. Unsafe 类中的 CAS 方法最终调用了 JVM 内部的 Atomic::cmpxchg 函数。这个函数是 JVM 针对不同操作系统和硬件平台的具体实现。
    2. JVM 通过 JNI(Java Native Interface)与底层操作系统和硬件进行交互,并根据不同的操作系统和处理器架构实现相应的 CAS 操作。
  2. 操作系统和硬件层面:汇编指令和 CPU 硬件支持
    1. 在最底层,Atomic::cmpxchg 函数依赖于特定 CPU 架构提供的原子指令。例如:
      1. **x86/x64 架构:**使用 CMPXCHG 指令。
LOCK CMPXCHG [destination], source
  • CMPXCHG 指令比较目标地址的值和累加器(通常是 EAXRAX 寄存器)中的值。如果相等,则将源操作数写入目标地址;否则,将目标地址的值加载到累加器中。
  • LOCK 前缀确保该指令在整个执行过程中不会被其他处理器中断,从而保证了原子性。
    2. **ARM 架构:**使用 `LDREX` 和 `STREX` 指令组合。
LDREX R1, [R0]      ; 加载目标地址的值到 R1 并标记该地址为独占访问
...                  ; 执行其他操作
STREX R2, R3, [R0]  ; 如果目标地址仍处于独占状态,则将 R3 的值存储到目标地址并设置 R20;否则设置 R2 为非零值
  • LDREX 指令加载一个值并标记该地址为独占访问。
  • STREX 指令尝试将一个新值存储到该地址,但如果该地址不再处于独占状态,则操作失败。

这些指令确保了在多核或多线程环境下,对共享内存的操作是原子性的。

总结:

  1. Java层面:通过Unsafe类提供的 CAS 方法,Java代码可以实现高效的并发控制。
  2. JVM层面Unsafe类的 CAS 方法调用 JVM 的Atomic::cmpxchg方法,这是一个平台相关的实现。
  3. 硬件层面Atomic::cmpxchg方法最终依赖于 CPU 提供的原子操作指令(如CMPXCHG),并使用lock前缀确保操作的原子性。

正是因为有了硬件和操作系统的支持,软件层面才能实现这些高效的并发控制机制。

CAS 有哪些应用?

实现原子类

标准库中提供了java.util.concurrent.atomic包,⾥⾯的类都是基于这种⽅式来实现的。典型的就是AtomicInteger类,其中的

  • getAndIncrement:相当于i++操作
  • incrementAndGet:相当于++i操作
  • getAndDecrement:相当于i--操作
  • decrementAndGet:相当于--i操作
  • getAndAdd:相当于累加操作

伪代码解释:

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

假设两个线程同时调⽤getAndIncrement

  1. 两个线程都读取 value 的值到 oldValue 中(oldValue 是⼀个局部变量,在栈上,每个线程有⾃⼰的栈)

  1. **线程1 **先执⾏ CAS 操作。由于 oldValue 和 value 的值相同,直接进⾏对 value 赋值
  • CAS 是直接读写内存的,⽽不是操作寄存器;
  • CAS 的读内存、⽐较、写内存操作是⼀条硬件指令,是原⼦的。

  1. **线程2 **再执⾏ CAS 操作,第⼀次 CAS 的时候发现 oldValue 和 value 不相等,不能进⾏赋值,因此需要进⼊循环,在循环⾥重新读取 value 的值赋给 oldValue。

  1. **线程2 **接下来第⼆次执⾏ CAS,此时 oldValue 和 value 相同,于是直接执⾏赋值操作。

  1. 线程1 和 线程2 返回各⾃的 oldValue 的值即可。

通过形如上述代码就可以实现⼀个原⼦类,不需要使⽤重量级锁,就可以⾼效的完成多线程的⾃增操作。

本来check and set这样的操作在代码⻆度不是原⼦的,但是在硬件层⾯上可以让⼀条指令完成这个操作,也就变成原⼦的了 。

实现自旋锁

自旋锁就是通过 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;
    }
}

CAS 的 ABA 问题

ABA问题是指在多线程环境下,一个变量的值可能在CAS操作期间被其他线程修改多次,导致CAS操作误认为变量的值没有发生变化,从而引发错误。

ABA 场景描述

假设有一个共享变量 A,初始值为 A0

  1. 线程 T1 读取到 A = A0
  2. 在 T1 进行 CAS 操作之前,另一个线程 T2 修改了 A 的值:
    1. T2 先将 AA0 改为 B
    2. 然后 T2 又将 AB 改回 A0
  3. 当 T1 执行 CAS 操作时,它会发现 A 的值仍然是 A0,因此 CAS 操作成功,但实际上 A 已经经历了 A0 -> B -> A0 的变化。

这种情况下,T1 并不知道 A 曾经被修改过,这可能导致程序逻辑错误。

ABA 问题引来的 BUG

⼤部分的情况下,T2 线程这样的⼀个反复横跳改动,对于 T1 修改 A 的值是没有影响的,但是不排除⼀些特殊情况,场景如下:

假设:

滑稽老铁 A 有 100 元存款,想在早上 11 点钟给老铁 B 转 50 元,但是他可能会没有时间,所以和老铁 C 约定好:在 11 点时,查一下我的存款余额,

  • 如果是 50 元,则说明已经转过账了,就不用麻烦老铁 C 了;
  • 如果是 100 元,则说明还没有转账,需要老铁 C 拿 A 的账号给老铁 B 转账。

如果使用 CAS 的方式,则可能会出现异常情况:

  1. 在 11 点之前,A 已经给 B 转过账了,此刻余额为 50 元;
  2. 但是,同样在 11 点之前,A 的公司给 A 转了一笔账,刚好是 50 元,此刻,A 的余额为 100 元;
  3. 11 点时,C 查看 A 的余额,发现是 100 元,所以向 B 转账 50**(重复转账)**。

从上面场景可以看出,多给 B 转了 50 元,这就出现了 BUG。

解决方案

通过引入一个版本号字段来跟踪变量的变化情况。每次对变量进行修改时,版本号也会相应增加。这样,在进行 CAS 操作时不仅比较变量的值,还要比较版本号

版本号可以使用自增的数值,也可以使用时间戳,只要可以区分变量是否改变即可。

Java 提供了一个内置的类 **AtomicStampedReference**,它可以同时存储一个引用和一个整数戳(stamp)。通过比较引用和戳的值,可以有效避免 ABA 问题。

CAS 的优缺点

  1. 优点:
    1. 无锁机制:CAS操作不需要锁,因此不会导致线程阻塞,提高了并发性能。
    2. 避免死锁:由于没有锁,因此不会出现死锁问题。
    3. 减少上下文切换:无锁机制减少了线程之间的上下文切换,提高了程序的效率。
  2. 缺点:
    1. ABA问题
    2. 循环时间长:如果多个线程竞争同一个变量,CAS操作可能会进入长时间的自旋状态,导致CPU资源浪费。
    3. 仅适用于单个变量操作:CAS操作通常只能用于单个变量的原子操作,对于多个变量的复合操作,需要额外的机制来保证原子性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值