【Java并发】【原子类】适合初学体质的原子类入门

👋hi,我不是一名外包公司的员工,也不会偷吃茶水间的零食,我的梦想是能写高端CRUD

🔥 2025本人正在沉淀中... 博客更新速度++

👍 欢迎点赞、收藏、关注,跟上我的更新节奏

📚欢迎订阅专栏,专栏名《在2B工作中寻求并发是否搞错了什么》

什么是CAS?

说到原子类,首先就要说到CAS:

CAS(Compare and Swap) 是一种无锁的原子操作,用于实现多线程环境下的安全数据更新。

CAS(Compare and Swap) 的本质是 "无锁更新" 。它的核心思想是:

  1. 先检查:在修改共享变量之前,先检查当前值是否符合预期。
  2. 再更新:如果符合预期,则更新为新值;否则放弃或重试。
  3. 原子性保证 :整个过程由 CPU 硬件指令(如 cmpxchg)直接支持,确保不可中断。

Java 通过 java.util.concurrent.atomic 包中的原子类(如 AtomicIntegerAtomicReference 等)提供 CAS 支持。

简单使用原子类

主播这里挑几个,主播觉得常见的,具体说说。

AtomicInteger

先从一个简单的案例开始,使用AtomicInteger实现线程安全计数器:

java 复制代码
public class AtomicIntegerTest {
    private static AtomicInteger count = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                // 使用 CAS 安全递增
                count.incrementAndGet();
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                // 使用 CAS 安全递增
                count.incrementAndGet();
            }
        });

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

        System.out.println("Final Count: " + count.get()); // 输出 2000
    }
}

一个简单的案例,主播直接下面具体说说AtomicInteger的一些方法:

基本方法:

java 复制代码
int get() // 返回当前值(线程安全获取)。
void set(int newValue) // 直接设置新值(非原子性,但保证可见性)。
void lazySet(int newValue) // 延迟设置新值(最终可见,但不保证其他线程立即看到)。

原子增减:

java 复制代码
int incrementAndGet() // 先自增 +1,再返回新值(等价于 ++i)。
int getAndIncrement() // 先返回当前值,再自增 +1(等价于 i++)。
int decrementAndGet() // 先自减 -1,再返回新值(等价于 --i)。
int getAndDecrement() // 先返回当前值,再自减 -1(等价于 i--)。

原子更新:

java 复制代码
int addAndGet(int delta) // 先增加 delta,再返回新值。
int getAndAdd(int delta) // 先返回当前值,再增加 delta。
boolean compareAndSet(int expect, int update) // CAS 操作:若当前值等于 expect,则更新为 update,返回是否成功。
int updateAndGet(IntUnaryOperator updateFunction) // 用函数式更新值,返回新值(如 x -> x * 2)。
int getAndUpdate(IntUnaryOperator updateFunction) // 用函数式更新值,返回旧值。

其他方法

java 复制代码
int getAndSet(int newValue) // 设置新值并返回旧值。(原子操作的,请放心)
int intValue() // 继承自 Number,返回当前值的 int 形式(等价于 get())。

AtomicReference

在多线程环境中,如果多个线程同时修改一个共享的可变对象,可能会导致数据不一致。传统的解决方法是使用 synchronizedLock 加锁,但锁会导致线程阻塞,降低并发性能。AtomicReference 使用无锁的 CAS(Compare-And-Swap)机制,通过硬件级别的原子指令直接操作内存,既保证线程安全,又避免了锁的开销。

( ̄▽ ̄)"让我们先从一个简单的案例来开始认识AtomicReference 吧!!

【案例】修改不可变对象

java 复制代码
// 1. 定义不可变对象
class ImmutableConfig {
    private final String serverUrl;
    private final int timeout;

    public ImmutableConfig(String serverUrl, int timeout) {
        this.serverUrl = serverUrl;
        this.timeout = timeout;
    }

    public String getServerUrl() { return serverUrl; }
    public int getTimeout() { return timeout; }
}

使用AtomicReference管理配置:

java 复制代码
// 2. 使用 AtomicReference 管理配置
public class ConfigManager {
    private final AtomicReference<ImmutableConfig> configRef;

    public ConfigManager(String initialUrl, int initialTimeout) {
        configRef = new AtomicReference<>(new ImmutableConfig(initialUrl, initialTimeout));
    }

    // 原子更新配置(创建新对象并替换引用)
    public void updateConfig(String newUrl, int newTimeout) {
        ImmutableConfig oldConfig;
        ImmutableConfig newConfig;
        do {
            oldConfig = configRef.get();       // 获取当前配置
            newConfig = new ImmutableConfig(newUrl, newTimeout); // 创建新配置
        } while (!configRef.compareAndSet(oldConfig, newConfig)); // CAS 更新
    }

    // 获取当前配置(线程安全)
    public ImmutableConfig getCurrentConfig() {
        return configRef.get();
    }
}

测试类

java 复制代码
public static void main(String[] args) {
    ConfigManager manager = new ConfigManager("http://default-server", 5000);

    // 线程1:更新配置
    new Thread(() -> {
        manager.updateConfig("http://new-server-1", 8000);
        System.out.println("Thread1 updated config: " + manager.getCurrentConfig());
    }).start();

    // 线程2:同时更新配置
    new Thread(() -> {
        manager.updateConfig("http://new-server-2", 10000);
        System.out.println("Thread2 updated config: " + manager.getCurrentConfig());
    }).start();
}

主播也是简单的收集了下,这个类的方法:

java 复制代码
get() // 获取当前对象引用值(保证内存可见性)
set(V newValue) // 原子性设置新引用值(无返回值)
getAndSet(V newValue) // 原子性操作:返回旧值并设置新值
compareAndSet(V expect, V update) // (CAS 操作) 当当前值等于 expect 时,原子性更新为 update(返回是否成功)
lazySet(V newValue) // 延迟设置新值(不保证其他线程立刻看到更新,性能优化用)
updateAndGet(UnaryOperator<V> updateFunction) // 原子性更新引用并返回新值
getAndUpdate(UnaryOperator<V> updateFunction) // 原子性更新引用并返回旧值

AtomicStampedReference&AtomicMarkableReference

说到AtomicStampedReference,那就必须先说说ABA问题勒😁

ABA 问题是 无锁编程(如 CAS 操作) 中一个经典问题,具体表现为:

  • 线程1 读取共享变量的值为 A
  • 线程1 准备修改该值时,线程2 将值从 A 改为 B,随后又改回 A
  • 线程1 执行 CAS 操作时,发现当前值仍是 A,误以为未被修改过,于是继续操作。
    问题本质:值看似未变,但中间经历了其他修改,可能导致逻辑错误。

ABA 问题的根源在于 值被多次修改后还原,但中间过程未被感知 。解决方法是为每次修改附加一个 版本号(或时间戳) ,使得:
即使值相同,版本号不同,CAS 也会失败

1、让我们看看AtomicStampedReference的解决

定义共享资源

java 复制代码
static class Resource {
    String data;
    public Resource(String data) { this.data = data; }
}

主类

java 复制代码
public static void main(String[] args) {
    // 初始引用:resourceA,版本号 0
    Resource resourceA = new Resource("A");
    AtomicStampedReference<Resource> stampedRef = 
        new AtomicStampedReference<>(resourceA, 0);

    // 线程1:尝试修改 ResourceA → B → A,并增加版本号
    new Thread(() -> {
        int[] stampHolder = new int[1];
        Resource current = stampedRef.get(stampHolder); // 获取当前值和版本号

        // 模拟 ABA 操作(A → B → A)
        stampedRef.compareAndSet(current, new Resource("B"), stampHolder[0], stampHolder[0] + 1);
        stampedRef.compareAndSet(stampedRef.getReference(), resourceA, stampedRef.getStamp(), stampedRef.getStamp() + 1);
    }).start();

    // 线程2:检查值是否被修改过(即使值还是A,版本号已变化)
    new Thread(() -> {
        try {
            Thread.sleep(500); // 等待线程1完成ABA操作
        } catch (InterruptedException e) {}

        int[] stampHolder = new int[1];
        Resource current = stampedRef.get(stampHolder);
        boolean success = stampedRef.compareAndSet(
            current, 
            new Resource("C"), 
            stampHolder[0],  // 预期原版本号(此时已不是0)
            stampHolder[0] + 1
        );

        System.out.println("更新是否成功? " + success); // 输出:false(因为版本号已变)
    }).start();
}

主播也是整理了下,这个类的方法:

java 复制代码
getReference() // 获取当前存储的引用对象(非原子性组合操作,需结合版本号使用)
getStamp() // 获取当前版本号(非原子性组合操作)
get(int[] stampHolder) // 原子性获取 引用值 + 版本号(通过数组传递版本号)
compareAndSet(V expectedRef, V newRef, int expectedStamp, int newStamp) // CAS 核心操作:当且仅当当前引用值等于 expectedRef 且 版本号等于 expectedStamp 时,更新引用和版本号
set(V newRef, int newStamp) // 直接设置新引用值和新版本号(非原子组合操作,慎用)
attemptStamp(V expectedRef, int newStamp) // 仅当当前引用等于 expectedRef 时,更新版本号(不改变引用)

2.再让我们看看AtomicMarkableReference的解决

AtomicMarkableReference 通过布尔标记降低 ABA 发生概率,但无法完全避免,实际使用中需结合场景评估风险。

java 复制代码
// 初始值:reference = "A", mark = false
AtomicMarkableReference<String> ref = new AtomicMarkableReference<>("A", false);

// 更新时检查值和标记
boolean success = ref.compareAndSet(
    "A",         // 预期原值
    "B",         // 新值
    false,       // 预期原标记
    true         // 新标记
);

设计初衷 并非为彻底解决 ABA 问题,而是提供一种轻量级标记机制,适用于对 ABA 不敏感但需简单版本标识的场景。

AtomicIntegerArray

AtomicIntegerArray 用于在多线程环境下原子性地操作一个整数数组 。它提供了对数组中每个元素的原子性操作(如 getsetcompareAndSetincrementAndGet 等),确保多线程修改数组元素时的线程安全性。每个方法(如 getsetaddAndGet)都是原子性的,无需额外同步。

让我们从一个简单的例子开始吧!!

java 复制代码
public class AtomicIntegerArrayExample {
    private static final int ARRAY_LENGTH = 5;
    private static AtomicIntegerArray atomicArray = new AtomicIntegerArray(ARRAY_LENGTH);

    public static void main(String[] args) throws InterruptedException {
        // 创建两个线程,分别对数组的不同索引进行自增操作
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                atomicArray.incrementAndGet(0); // 原子性地将索引 0 的元素自增 1
            }
        });

        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                atomicArray.incrementAndGet(0); // 两个线程操作同一个索引
            }
        });

        thread1.start();
        thread2.start();

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

        // 输出结果:2000(无竞态条件)
        System.out.println("Final value at index 0: " + atomicArray.get(0));
    }
}

聪明的你一定又学会了吧!主播这里简单整理了下其他方法:

java 复制代码
get(int i) // 获取索引 i 处的当前值(保证内存可见性)。
set(int i, int newValue) // 直接设置索引 i 处的值为 newValue(无原子性保证,但保证写入后对其他线程可见)。
lazySet(int i, int newValue) // 延迟设置值(性能优化,不保证其他线程立刻可见)。
compareAndSet(int i, int expect, int update) // CAS 操作:当索引 i 处的值等于 expect 时,原子性更新为 update,返回是否成功。
getAndSet(int i, int newValue) // 原子性获取旧值并设置新值。

// 复合原子操作
getAndUpdate(int i, IntUnaryOperator updateFunction) // 原子性应用函数到索引 i 处的值,返回旧值。
updateAndGet(int i, IntUnaryOperator updateFunction) // 原子性应用函数到索引 i 处的值,返回新值。
getAndAccumulate(int i, int x, IntBinaryOperator accumulatorFunction) // 原子性将索引 i 处的值与 x 通过函数计算,返回旧值。
accumulateAndGet(int i, int x, IntBinaryOperator accumulatorFunction) // 同上,但返回新值。

// 自增/自减快捷方法
getAndIncrement(int i) // 原子性自增(旧值 +1,返回旧值)。
getAndDecrement(int i) // 原子性自减(旧值 -1,返回旧值)。
getAndAdd(int i, int delta) // 原子性增加 delta,返回旧值。
incrementAndGet(int i) // 自增后返回新值(等价于 ++i)。

LongAdder

LongAdder专门用于高并发场景下的累加操作 。它在多线程环境下性能优于 AtomicLong,尤其是在高竞争(多线程频繁修改值)的场景中,因为它采用了分段锁(Cell 分段) 的策略减少线程竞争。

具体原理下一篇会说,这里我们先来看看差异

java 复制代码
public class PerformanceComparison {
    private static final int THREAD_COUNT = 1000;     // 线程数
    private static final int OPERATIONS = 100000;  // 每个线程的操作次数

    // 测试 LongAdder
    private static void testLongAdder() throws InterruptedException {
        LongAdder adder = new LongAdder();
        ExecutorService executor = Executors.newFixedThreadPool(THREAD_COUNT);

        long start = System.currentTimeMillis();
        for (int i = 0; i < THREAD_COUNT; i++) {
            executor.submit(() -> {
                for (int j = 0; j < OPERATIONS; j++) {
                    adder.increment(); // 无锁分段累加
                }
            });
        }

        executor.shutdown();
        executor.awaitTermination(1, TimeUnit.MINUTES);
        long duration = System.currentTimeMillis() - start;

        System.out.println("LongAdder 耗时: " + duration + "ms, 结果: " + adder.sum());
    }

    // 测试 AtomicLong
    private static void testAtomicLong() throws InterruptedException {
        AtomicLong atomicLong = new AtomicLong(0);
        ExecutorService executor = Executors.newFixedThreadPool(THREAD_COUNT);

        long start = System.currentTimeMillis();
        for (int i = 0; i < THREAD_COUNT; i++) {
            executor.submit(() -> {
                for (int j = 0; j < OPERATIONS; j++) {
                    atomicLong.incrementAndGet(); // 基于 CAS 的原子操作
                }
            });
        }

        executor.shutdown();
        executor.awaitTermination(1, TimeUnit.MINUTES);
        long duration = System.currentTimeMillis() - start;

        System.out.println("AtomicLong 耗时: " + duration + "ms, 结果: " + atomicLong.get());
    }

    public static void main(String[] args) throws InterruptedException {
        testLongAdder();    // 先测试 LongAdder
        testAtomicLong();  // 再测试 AtomicLong
    }
}

输出结果

shell 复制代码
LongAdder 耗时: 304ms, 结果: 100000000
AtomicLong 耗时: 1782ms, 结果: 100000000

主播这里就贴心的准备了其他方法:

java 复制代码
add(long x) // 原子性增加指定值(可正可负),无返回值。
increment() // 原子性自增 1(等价于 add(1))。
decrement() // 原子性自减 1(等价于 add(-1))。
sum() // 返回当前总和(非原子快照,并发时可能不精确)。
reset() // 重置所有计数器为 0(非原子操作,需谨慎使用)。
sumThenReset() // 返回当前总和并重置计数器(类似"获取并清零"操作)。

AtomicLong的对比

机制 LongAdder AtomicLong
存储方式 分散到多个 Cell 单一的 volatile long变量
竞争处理 线程优先修改各自对应的 Cell,减少冲突 所有线程竞争同一个变量的 CAS 操作
读取结果 调用 sum()需要合并所有 Cell的值 get()直接返回当前值
适用场景 高并发写入,低频读取(如统计计数) 低并发或需要实时读取值的场景

后话

( ̄▽ ̄)"怎么样?聪明的你是否对原子类的使用,拥有了更多的理解。

相关推荐
陵易居士几秒前
Spring如何解决项目中的循环依赖问题?
java·后端·spring
铁弹神侯8 分钟前
Maven相关名词及相关配置
java·maven
Aska_Lv13 分钟前
RocketMQ---core原理
后端
AronTing18 分钟前
10-Spring Cloud Alibaba 之 Dubbo 深度剖析与实战
后端·面试·架构
会飞的皮卡丘EI21 分钟前
关于Blade框架对数字类型的null值转为-1问题
java·spring boot
没逻辑22 分钟前
⏰ Redis 在支付系统中作为延迟任务队列的实践
redis·后端
雷渊24 分钟前
如何保证数据库和Es的数据一致性?
java·后端·面试
fjkxyl25 分钟前
Spring的启动流程
java·后端·spring
掘金酱26 分钟前
😊 酱酱宝的推荐:做任务赢积分“拿”华为MatePad Air、雷蛇机械键盘、 热门APP会员卡...
前端·后端·trae
极客先躯27 分钟前
高级java每日一道面试题-2025年4月06日-微服务篇[Nacos篇]-如何诊断和解决Nacos中的常见问题?
java·开发语言·微服务