CAS&Atomic 原子操作详解

发布于:2025-08-04 ⋅ 阅读:(16) ⋅ 点赞:(0)


1. 什么是原子操作?如何实现原子操作?

事务的一大特性就是 原子性, 一个事务包含多个操作,这些操作要么全 部执行,要么全都不执行。
并发里的原子性和原子操作是一样的内涵和概念,假定有两个操作 A 和 B 都包含多个步骤,如果从执行 A 的线程来看, 当另一个线程执行 B 时, 要么将 B 全部执行完, 要么完全不执行 B ,执行B 的线程看 A 的操作也是一样的, 那么 A 和 B 对彼此来说是原子的。

实现原子操作可以使用锁, 锁机制, 满足基本的需求是没有问题的了, 但是 有的时候我们的需求并非这么简单,我们需要更有效,更加灵活的机制,java中synchronized 关键字是基于阻塞的锁机制,也就是说当一个线程拥有锁的时候, 访问同一资源的其它线程需要等待,直到该线程释放锁

这里会有些问题: 首先,如果被阻塞的线程优先级很高很重要怎么办?其次, 如果获得锁的线程一直不释放锁怎么办? 同时,还有可能出现一些例如死锁之类 的情况, 最后, 其实锁机制是一种比较粗糙, 粒度比较大的机制, 相对于像计数 器这样的需求有点儿过于笨重。为了解决这个问题,Java 提供了 Atomic 系列的 原子操作类。

**这些原子操作其实是使用当前的处理器基本都支持的CAS指令 。**一个 CAS 操作过程都包含三

个运算符: 一个内存地址 V,一个期望的值 A 和一 个新值 B,操作的时候如果这个地址上存放的值

等于这个期望的值 A,则将地址 上的值赋为新值 B,否则不做任何操作。

类似的代码就是:

public boolean compareAndSwap(int expectedValue, int newValue) {
        // === CAS 的关键步骤 ===
        // 1. 读取当前值
        int currentValue = value;
 
        // 2. 检查当前值是否等于期望值
        if (currentValue == expectedValue) {
            // 3. 如果相等,则尝试更新为新值(原子操作)
            value = newValue;  // 实际硬件级原子操作
            return true;       // CAS 成功
        }
 
        // 4. 如果不相等,说明其他线程已修改,CAS 失败
        return false;
    }

CAS 的基本思路就是, 如果这个地址上的值和期望的值相等, 则给其赋予新值, 否则不做任何事儿,但是要返回原值是多少。 自然 CAS 操作执行完成时, 在业务上不一定完成了, 这个时候我们就会对 CAS 操作进行反复重试, 于是就有了 循环 CAS。很明显, 循环 CAS 就是在一个循环里不断的做 CAS 操作, 直到成功为止。 Java 中的 Atomic 系列的原子操作类的实现则是利用了循环 CAS来实现。

2. CAS 实现原子操作的三大问题

2.1. ABA问题

因为 CAS 需要在操作值的时候, 检查值有没有发生变化, 如果没有发生变化 则更新,但是如果一个值原来是 A,变成了 B,又变成了 A,那么使用 CAS 进行 检查时会发现它的值没有发生变化,但是实际上却变化了。

ABA 问题的解决思路就是使用版本号。在变量前面追加上版本号, 每次变量 更新的时候把版本号加 1,那么 A →B →A 就会变成 1A →2B →3A。

2.2. 循环时间长开销大

自旋 CAS 如果长时间不成功,会给 CPU 带来非常大的执行开销。

2.3. 只能保证一个共享变量的原子操作。

当对一个共享变量执行操作时, 我们可以使用循环 CAS 的方式来保证原子操 作, 但是对多个共享变量操作时, 循环 CAS 就无法保证操作的原子性, 这个时候 就可以用锁。

还有一个取巧的办法, 就是把多个共享变量合并成一个共享变量来操作。比 如,有两个共享变量i =2,j=a,合并一下 ij=2a,然后用 CAS 来操作 ij。从 Java 1.5 开始, JDK 提供了 AtomicReference 类来保证引用对象之间的原子性,就可以把 多个变量放在一个对象里来进行 CAS 操作。

3. Jdk 中相关原子操作类的使用

3.1. AtomicInteger

  • intaddAndGet(int delta):以原子方式将输入的数值与实例中的值 (AtomicInteger 里的 value)相加,并返回结果。
  • boolean compareAndSet(int expect ,int update):如果输入的数值等于预 期值,则以原子方式将该值设置为输入的值。
  • intgetAndIncrement():以原子方式将当前值加 1,注意,这里返回的是自 增前的值。
  • intgetAndSet(int newValue):以原子方式设置为 newValue 的值, 并返回 旧值。

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicIntegerMultiThreadExample {
    // 创建原子整数,初始值为0
    private static AtomicInteger counter = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        // 创建两个线程,每个线程都对计数器自增1000次
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                // 原子方式自增,确保线程安全
                counter.incrementAndGet();
            }
        });

        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.incrementAndGet();
            }
        });

        // 启动线程
        thread1.start();
        thread2.start();

        // 等待两个线程执行完毕
        thread1.join();
        thread2.join();

        // 输出最终结果
        System.out.println("预期结果: 2000");
        System.out.println("实际结果: " + counter.get());
    }
}

3.2. AtomicIntegerArray

主要是提供原子的方式更新数组里的整型,其常用方法如下。

  • int addAndGet(int i,int delta):以原子方式将输入值与数组中索引 i 的元 素相加。
  • boolean compareAndSet(int i ,int expect ,int update):如果当前值等于 预期值,则以原子方式

将数组位置 i 的元素设置成 update 值。

需要注意的是, 数组 value 通过构造方法传递进去, 然后 AtomicIntegerArray 会将当前数组复制

一份,所以当 AtomicIntegerArray 对内部的数组元素进行修改 时,不会影响传入的数组。

import java.util.concurrent.atomic.AtomicIntegerArray;

public class AtomicIntegerArrayDemo {
    public static void main(String[] args) throws InterruptedException {
        // 初始数组
        int[] array = {10, 20, 30, 40};
        
        // 创建 AtomicIntegerArray(会复制原始数组)
        AtomicIntegerArray atomicArray = new AtomicIntegerArray(array);
        
        // 测试 addAndGet 方法
        int newValue = atomicArray.addAndGet(0, 5); // 索引0的值加5
        System.out.println("索引0添加后的值: " + newValue); // 输出15
        System.out.println("原始数组索引0的值: " + array[0]); // 仍为10(原始数组未变)
        
        // 测试 compareAndSet 方法
        boolean success = atomicArray.compareAndSet(1, 20, 25);
        System.out.println("修改索引1是否成功: " + success); // true
        System.out.println("索引1修改后的值: " + atomicArray.get(1)); // 25
        
        // 多线程操作示例
        Runnable task = () -> {
            for (int i = 0; i < 1000; i++) {
                // 对索引2的值进行原子递增
                atomicArray.incrementAndGet(2);
            }
        };
        
        // 启动10个线程
        Thread[] threads = new Thread[10];
        for (int i = 0; i < 10; i++) {
            threads[i] = new Thread(task);
            threads[i].start();
        }
        
        // 等待所有线程完成
        for (Thread t : threads) {
            t.join();
        }
        
        System.out.println("索引2最终值: " + atomicArray.get(2)); // 30 + 10*1000 = 10030
    }
}
    

3.3. 更新引用类型

原子更新基本类型的 AtomicInteger,只能更新一个变量, 如果要原子更新多 个变量,就需要使用这个原子更新引用类型提供的类。 Atomic 包提供了以下 3 个类。

3.3.1. AtomicReference

原子更新引用类型

3.3.2. AtomicStampedReference

利用版本戳的形式记录了每次改变以后的版本号, 这样的话就不会存在 ABA 问题了。这就是AtomicStampedReference 的解决方案。AtomicMarkableReferenceAtomicStampedReference 差不多,

AtomicStampedReference 是使用 pair 的 int stamp 作为计数器使用,AtomicMarkableReference 的 pair

使用的是 boolean mark。

AtomicStampedReference 可能关心的是动过几次,AtomicMarkableReference 关心的是有没有被人动过

3.3.3. AtomicMarkableReference

原子更新带有标记位的引用类型。可以原子更新一个布尔类型的标记位和引用类型。

package jwcb.concurrent_programming.threadLocal;

import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;
import java.util.concurrent.atomic.AtomicMarkableReference;

// 定义一个简单的用户类作为引用类型
class User {
    private String name;
    private int age;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    @Override
    public String toString() {
        return "User{name='" + name + "', age=" + age + "}";
    }
}

public class AtomicReferenceDemo {
    public static void main(String[] args) {
        // 1. AtomicReference示例:原子更新引用类型
        User user1 = new User("张三", 20);
        User user2 = new User("李四", 25);
        AtomicReference<User> atomicRef = new AtomicReference<>(user1);
        
        // 比较并设置引用
        boolean swapped = atomicRef.compareAndSet(user1, user2);
        System.out.println("AtomicReference 交换是否成功: " + swapped); // true
        System.out.println("当前引用对象: " + atomicRef.get()); // 李四,25

        // 2. AtomicStampedReference示例:解决ABA问题,记录版本戳
        User initialUser = new User("初始用户", 30);
        // 初始版本号为0
        AtomicStampedReference<User> stampedRef = new AtomicStampedReference<>(initialUser, 0);
        
        // 获取当前值和版本号
        User currentUser = stampedRef.getReference();
        int currentStamp = stampedRef.getStamp();
        System.out.println("\nAtomicStampedReference 初始值: " + currentUser + ", 版本: " + currentStamp);
        
        // 更新引用并增加版本号
        boolean updated = stampedRef.compareAndSet(currentUser, user1, currentStamp, currentStamp + 1);
        System.out.println("第一次更新是否成功: " + updated); // true
        System.out.println("更新后版本: " + stampedRef.getStamp()); // 1
        
        // 3. AtomicMarkableReference示例:标记引用是否被修改过
        User markableUser = new User("标记用户", 35);
        // 初始标记为false(未修改)
        AtomicMarkableReference<User> markableRef = new AtomicMarkableReference<>(markableUser, false);
        
        // 获取当前标记
        boolean[] markHolder = new boolean[1];
        User retrievedUser = markableRef.get(markHolder);
        System.out.println("\nAtomicMarkableReference 初始值: " + retrievedUser + ", 标记: " + markHolder[0]);
        
        // 更新引用并改变标记
        boolean markUpdated = markableRef.compareAndSet(retrievedUser, user2, false, true);
        System.out.println("标记更新是否成功: " + markUpdated); // true
        
        // 获取更新后的标记
        User newUser = markableRef.get(markHolder);
        System.out.println("更新后的值: " + newUser + ", 标记: " + markHolder[0]); // true
    }
}
    

3.4. 原子更新字段类

如果需原子地更新某个类里的某个字段时,就需要使用原子更新字段类, Atomic 包提供了以下 3个类进行原子字段更新。

要想原子地更新字段类需要两步。第一步,因为原子更新字段类都是抽象类, 每次使用的时候必须使用静态方法 newUpdater()创建一个更新器, 并且需要设置 想要更新的类和属性。第二步,更新类的字段(属性)必须使用 public volatile 修饰符。

3.4.1. AtomicIntegerFieldUpdater

原子更新整型的字段的更新器。

3.4.2. AtomicLongFieldUpdater

原子更新长整型字段的更新器。

3.4.3. AtomicReferenceFieldUpdater

原子更新引用类型字段的更新器。

import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;

// 示例实体类
class User {
    private String name;
    // 必须用public volatile修饰需要原子更新的字段
    public volatile int age;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }
}

public class AtomicFieldUpdaterDemo {
    // 1. 创建原子更新器,指定要更新的类和字段
    private static final AtomicIntegerFieldUpdater<User> ageUpdater = 
        AtomicIntegerFieldUpdater.newUpdater(User.class, "age");

    public static void main(String[] args) throws InterruptedException {
        User user = new User("张三", 20);
        
        // 2. 使用更新器进行原子操作
        // 增加年龄并获取新值
        int newAge = ageUpdater.addAndGet(user, 5);
        System.out.println("更新后的年龄: " + newAge); // 25
        System.out.println("用户对象的年龄: " + user.getAge()); // 25
        
        // 比较并设置年龄
        boolean success = ageUpdater.compareAndSet(user, 25, 30);
        System.out.println("年龄更新是否成功: " + success); // true
        System.out.println("当前年龄: " + user.getAge()); // 30
        
        // 多线程更新示例
        Runnable task = () -> {
            for (int i = 0; i < 1000; i++) {
                ageUpdater.incrementAndGet(user);
            }
        };
        
        // 启动10个线程同时更新年龄
        Thread[] threads = new Thread[10];
        for (int i = 0; i < 10; i++) {
            threads[i] = new Thread(task);
            threads[i].start();
        }
        
        // 等待所有线程完成
        for (Thread t : threads) {
            t.join();
        }
        
        System.out.println("多线程更新后的年龄: " + user.getAge()); // 30 + 10*1000 = 10030
    }
}
    

3.5. LongAdder

JDK1.8 时,java.ul.concurrent.atomic 包中提供了一个新的原子类:LongAdder。 根据 Oracle 官方文档的介绍, LongAdder 在高并发的场景下会比它的前辈——— AtomicLong 具有更好的性能,代价是消耗更多的内存空间。

AtomicLong 是利用了底层的 CAS 操作来提供并发性的, 调用了 Unsafe 类的 getAndAddLong 方法, 该方法是个 nave 方法, 它的逻辑是采用自旋的方式不断 更新目标值,直到更新成功。

在并发量较低的环境下, 线程冲突的概率比较小, 自旋的次数不会很多。但 是, 高并发环境下, N 个线程同时进行自旋操作, 会出现大量失败并不断自旋的 情况,此时 AtomicLong 的自旋会成为瓶颈。

这就是 LongAdder 引入的初衷——解决高并发环境下 AtomicLong 的自旋瓶 颈问题。

AtomicLong 中有个内部变量 value 保存着实际的 long 值,所有的操作都是 针对该变量进行。也就是说,高并发环境下, value 变量其实是一个热点,也就 是 N 个线程竞争一个热点。

LongAdder 的基本思路就是分散热点,将 value 值分散到一个数组中,不同 线程会命中到数组的不同槽中,各个线程只对自己槽中的那个值进行 CAS 操作, 这样热点就被分散了,冲突的概率就小很多。如果要获取真正的 long 值,只要 将各个槽中的变量值累加返回。

对于 LongAdder 来说, 内部有一个 base 变量,一个 Cell[]数组。 base 变量:非竞态条件下,直接累加到该变量上。Cell[]数组:竞态条件下,累加个各个线程自己的槽 Cell[i]中。

LongAdder 的核心功能是原子地进行 “累加” 操作(比如 add(long x) 方法),即多个线程同时调用 add 时,最终的总和必须等于所有线程累加值的总和(不会出现 “丢失更新” 等线程安全问题)。

  • valuevolatile 修饰,保证多线程下的可见性(一个线程修改后,其他线程能看到最新值)。
  • cas 方法通过 Unsafe 的 CAS 操作原子地更新 value,确保对单个 Cell 的修改是 “要么成功、要么失败” 的原子操作。

无论线程映射到哪个槽,对槽内 value 的更新都是通过 CAS 实现的原子操作,因此单个更新不会出现线程安全问题(如丢失更新)。

而 LongAdder 最终结果的求和,并没有使用全局锁,返回值不是绝对准确的, 因为调用这个方法时还有其他线程可能正在进行计数累加, 所以只能得到某个时 刻的近似值,这也就是 LongAdder 并不能完全替代 LongAtomic 的原因之一。

3.6. 其他新增

除了新引入 LongAdder 外,还有引入了它的三个兄弟类: LongAccumulatorDoubleAdderDoubleAccumulator

LongAccumulatorLongAdder 的增强版。LongAdder 只能针对数值的进行加减运算,而LongAccumulator 提供了自定义的函数操作。

通过 LongBinaryOperator,可以自定义对入参的任意操作,并返回结果 (LongBinaryOperator 接收

2 个 long 作为参数,并返回 1 个 long)。

LongAccumulator 内部原理和 LongAdder 几乎完全一样。

DoubleAdderDoubleAccumulator 用于操作 double 原始类型。


网站公告

今日签到

点亮在社区的每一天
去签到