如何实现无锁并发:深入理解CAS原理

简介: 如何实现无锁并发:深入理解CAS原理

深入理解CAS原理

多线程编程中,保证数据的安全性和一致性是至关重要的。CAS(Compare and Swap)作为一种无锁并发技术,为我们提供了一个高效的解决方案。本文将深入探讨CAS的实现原理及其在并发编程中的应用。

为什么会有CAS?

synchronized 时代:在多线程中为了保持数据的准确性,避免多个线程同时操作某个变量,很多情况下利用关键字 synchronized 实现同步锁。


使用 synchronized 关键字可以使操作的线程排队等待运行,可以说是一种悲观策略,认为线程会修改数据,所以开始就把持有锁的线程锁住,其他线程只能是挂起状态,等待锁的释放,所以同步锁带来了效率问题。


synchronized 时代效率问题:在线程执行的时候,获得锁的线程在运行,其他被挂起的线程只能等待着持有锁的线程释放锁才有机会运行,在效率上都浪费在等待上。

更多请移步深入解析 Java 中的 Synchronized


在很多的线程切换的时候,由于有同步锁,就要涉及到锁的释放,加锁,这又是一个很大的时间开销。


volatile 时代:与锁(阻塞机制)的方式相比有一种更有效地方法,非阻塞机制,同步锁带来了线程执行时候之间的阻塞,而这种非阻塞机制在多个线程竞争同一个数据的时候不会发生阻塞的情况,这样在时间上就可以节省出很多的时间。


我们会想到用 volatile,使用 volatile 不会造成阻塞,volatile 保证了线程之间的内存可见性和程序执行的有序性可以说已经很好的解决了上面的问题。


volatile 时代原子操作问题:一个很重要的问题就是,volatile 不能保证原子性,对于复合操作,例如 i++ 这样的程序包含三个原子操作:取值,增加,赋值。

CAS 与系统内核

CAS 操作依赖于底层硬件指令和系统内核的支持。以下是一些关键点:

  • 硬件支持
  • 现代 CPU 提供了专门的指令(如 CMPXCHG、LL/SC)来支持 CAS 操作。
  • 这些指令在硬件层面上保证了操作的原子性。
  • 内核原子操作
  • 操作系统内核提供了对这些硬件指令的封装,以便应用程序能够方便地使用。
  • 在 Linux 内核中,CAS 操作通过 atomic_compare_and_exchange 等内核函数实现。
  • 内存模型
  • CAS 操作需要与内存模型相结合,以确保操作的可见性和有序性。
  • Java 内存模型(JMM)确保了使用 CAS 操作的变量在多线程环境下的正确性。

CAS的基本原理

CAS 操作包含三个操作数:

  • V:需要读写的内存位置。
  • E:预期值(Expected value)。
  • N:新值(New value)。

CAS 操作的步骤如下:

  • 比较:将内存位置 V 当前的值与预期值 E 进行比较。
  • 交换:如果 V 的当前值等于 E,则将新值 N 写入 V 中。
  • 返回结果:返回 V 的旧值,如果 V 的旧值等于 E,则说明交换成功。

这种操作通过硬件指令(如 x86 架构的 CMPXCHG 指令)实现,保证了比较和交换操作的原子性。

Java中的CAS实现

在 Java 中,CAS 操作主要通过 sun.misc.Unsafe 类提供的方法来实现,如 compareAndSwapInt、compareAndSwapLong 和 compareAndSwapObject。这些方法直接调用底层硬件的 CAS 指令。

import sun.misc.Unsafe;
import java.lang.reflect.Field;

public class CASCounter {
    private volatile int value;
    private static final Unsafe unsafe;
    private static final long valueOffset;

    static {
        try {
            // 通过反射获取Unsafe实例
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            unsafe = (Unsafe) field.get(null);

            // 获取value字段在CASCounter中的偏移量
            valueOffset = unsafe.objectFieldOffset(CASCounter.class.getDeclaredField("value"));
        } catch (Exception e) {
            throw new Error(e);
        }
    }

    public int incrementAndGet() {
        int current;
        do {
            current = value;
            // 尝试将value从current更新为current + 1
        } while (!unsafe.compareAndSwapInt(this, valueOffset, current, current + 1));
        return current + 1;
    }

    public int get() {
        return value;
    }

    public static void main(String[] args) {
        CASCounter counter = new CASCounter();
        int threadCount = 10;
        int incrementsPerThread = 1000;

        Thread[] threads = new Thread[threadCount];
        for (int i = 0; i < threadCount; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < incrementsPerThread; j++) {
                    counter.incrementAndGet();
                }
            });
        }

        for (Thread thread : threads) {
            thread.start();
        }

        for (Thread thread : threads) {
            try {
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        System.out.println("Final value: " + counter.get());
    }
}

CAS的优缺点

优点:

  • 无锁并发:避免了传统锁带来的开销和线程上下文切换问题,提高了并发性能。
  • 高效:在低竞争情况下,CAS操作比锁机制更高效。

缺点:

  • ABA问题:如果变量在检查和更新之间发生了变化,且最终值和初始值相同,则CAS无法检测到。可以通过增加版本号解决。
  • 自旋等待:在高竞争情况下,CAS操作可能会导致自旋等待(不断重试),造成CPU资源浪费。

ABA 问题及其解决

版本号机制:一般是在数据中加上一个数据版本号 version 字段,表示数据被修改的次数,当数据被修改时,version 值会加 1。当线程 A 要更新数据值时,在读取数据的同时也会读取 version 值,在提交更新时,若刚才读取到的 version 值为当前数据中的 version 值相等时才更新,否则重试更新操作,直到更新成功。


场景示例:假设商店类 Shop 中有一个 version 字段,当前值为 1 ;而当前商品数量为 50。

  • 店员 A 此时将其读出( version=1 ),并将商品数量扣除 10,更新为 50 - 10 = 40;
  • 在店员 A 操作的过程中,店员 B 也读入此信息( version=1 ),并将商品数量扣除 20,更新为 50 - 20 = 30;
  • 店员 A 完成了修改工作,将数据版本号加 1( version=2 ),商品数量为 40,提交更新,此时由于提交数据版本大于记录当前版本,数据被更新,数据记录 version 更新为 2 ;
  • 店员 B 完成了操作,也将版本号加 1( version=2 ),试图更新商品数量为 30。但此时比对数据记录版本时发现,店员 B 提交的数据版本号为 2 ,数据记录当前版本也为 2 ,不满足 “ 提交版本必须大于记录当前版本才能执行更新 “ 的乐观锁策略,因此,店员 B 的提交被驳回;
  • 店员 B 再次重新获取数据,version = 2,商品数量 40。在这个基础上继续执行自己扣除 20 的操作,商品数量更新为 40 - 20 = 20;
  • 店员 B 将版本号加 1 ,version = 3,将之前的记录 version 2 更新为 3 ,将之前的数量 40 更新 为 20。

从如上描述来看,所有的操作都不会出现脏数据,关键在于版本号的控制。

Tips:Java 对于乐观锁的使用进行了良好的封装,我们可以直接使用并发编程包来进行乐观锁的使用。本节接下来所使用的 Atomic 操作即为封装好的操作。

之所以还要对 CAS 原理以及 ABA 问题进行深入的分析,主要是为了让学习者了解底层的原理,以便更好地在不同的场景下选择使用锁的类型。


总结

CAS 是一种高效的原子操作,通过比较和交换来实现无锁并发。它利用底层硬件指令实现了高性能的同步机制,广泛应用于 Java 并发包中。尽管 CAS 操作存在一些问题,如 ABA 问题和自旋等待,但通过版本号机制等方法,可以有效地解决这些问题。CAS 的应用极大地提高了多线程编程的性能,是实现高效并发的关键技术之一。

目录
相关文章
|
6月前
多线程并发锁的方案—原子操作
多线程并发锁的方案—原子操作
|
Java 编译器
解密Java多线程中的锁机制:CAS与Synchronized的工作原理及优化策略
解密Java多线程中的锁机制:CAS与Synchronized的工作原理及优化策略
|
22天前
|
算法 Java
介绍一下CAS算法的实现原理
【10月更文挑战第20天】介绍一下CAS算法的实现原理
10 0
|
5月前
|
缓存 安全 Java
原子操作的实现原理
原子操作的实现原理
77 0
|
3月前
|
Java
什么是 CAS(自旋锁)? 它的优缺点? 如何使用CAS实现一把锁?
该博客文章解释了什么是CAS(自旋锁),包括CAS的基本概念、实现原理、优缺点,以及如何使用CAS实现锁的逻辑,并提供了使用CAS实现锁的Java完整代码示例和测试结果。
什么是 CAS(自旋锁)? 它的优缺点? 如何使用CAS实现一把锁?
|
5月前
|
存储 缓存 安全
架构面试题汇总:并发和锁(2024版)
架构面试题汇总:并发和锁(2024版)
|
安全 算法 架构师
带你了解什么是无锁并发 CAS
带你了解什么是无锁并发 CAS
261 0
带你了解什么是无锁并发 CAS
|
安全 Java 编译器
【JavaEE】多线程进阶问题-锁策略and死锁,CAS操作,Synchronized原理
JavaEE & 多线程进阶问题 & 锁策略and 死锁,CAS操作,Synchronized原理
59 0
|
存储 Java 对象存储
|
Java 编译器 C++
java多线程常见锁策略CAS机制(2)
java多线程常见锁策略CAS机制(2)
167 1
java多线程常见锁策略CAS机制(2)