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

简介: 什么是CAS? 说到原子类,首先就要说到CAS: CAS(Compare and Swap) 是一种无锁的原子操作,用于实现多线程环境下的安全数据更新。 CAS(Compare and Swap) 的

👋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 支持。

image.png

简单使用原子类

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

AtomicInteger

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

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的一些方法:

基本方法:

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

原子增减:

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

原子更新:

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) // 用函数式更新值,返回旧值。

其他方法

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

AtomicReference

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

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

【案例】修改不可变对象

// 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管理配置:

// 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();
    }
}

测试类

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();
}

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

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的解决

定义共享资源

static class Resource {
   
    String data;
    public Resource(String data) {
    this.data = data; }
}

主类

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();
}

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

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 发生概率,但无法完全避免,实际使用中需结合场景评估风险。

// 初始值: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)都是原子性的,无需额外同步。\
让我们从一个简单的例子开始吧!!

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));
    }
}

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

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 分段) 的策略减少线程竞争。

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

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
    }
}

输出结果

LongAdder 耗时: 304ms, 结果: 100000000
AtomicLong 耗时: 1782ms, 结果: 100000000

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

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()直接返回当前值
适用场景 高并发写入,低频读取(如统计计数) 低并发或需要实时读取值的场景

后话

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

目录
相关文章
|
14天前
|
存储 Oracle Java
java零基础学习者入门课程
本课程为Java零基础入门教程,涵盖环境搭建、变量、运算符、条件循环、数组及面向对象基础,每讲配示例代码与实践建议,助你循序渐进掌握核心知识,轻松迈入Java编程世界。
124 0
|
2月前
|
Java
java入门代码示例
本文介绍Java入门基础,包含Hello World、变量类型、条件判断、循环及方法定义等核心语法示例,帮助初学者快速掌握Java编程基本结构与逻辑。
310 0
|
2月前
|
Java API 数据库
2025 年最新 Java 实操学习路线,从入门到高级应用详细指南
2025年Java最新实操学习路线,涵盖从环境搭建到微服务、容器化部署的全流程实战内容,助你掌握Java 21核心特性、Spring Boot 3.2开发、云原生与微服务架构,提升企业级项目开发能力,适合从入门到高级应用的学习需求。
503 0
|
2月前
|
前端开发 Java 数据库连接
帮助新手快速上手的 JAVA 学习路线最详细版涵盖从入门到进阶的 JAVA 学习路线
本Java学习路线涵盖从基础语法、面向对象、异常处理到高级框架、微服务、JVM调优等内容,适合新手入门到进阶,助力掌握企业级开发技能,快速成为合格Java开发者。
406 3
|
2月前
|
监控 Java API
2025 年全新出炉的 Java 学习路线:从入门起步到实操精通的详细指南
2025年Java学习路线与实操指南,涵盖Java 21核心特性、虚拟线程、Spring Boot 3、微服务、Spring Security、容器化部署等前沿技术,助你从入门到企业级开发进阶。
500 0
|
3月前
|
NoSQL Java 关系型数据库
Java 从入门到进阶完整学习路线图规划与实战开发最佳实践指南
本文为Java开发者提供从入门到进阶的完整学习路线图,涵盖基础语法、面向对象、数据结构与算法、并发编程、JVM调优、主流框架(如Spring Boot)、数据库操作(MySQL、Redis)、微服务架构及云原生开发等内容,并结合实战案例与最佳实践,助力高效掌握Java核心技术。
322 1
|
3月前
|
Java 测试技术 API
Java IO流(二):文件操作与NIO入门
本文详解Java NIO与传统IO的区别与优势,涵盖Path、Files类、Channel、Buffer、Selector等核心概念,深入讲解文件操作、目录遍历、NIO实战及性能优化技巧,适合处理大文件与高并发场景,助力高效IO编程与面试准备。
|
3月前
|
Java 编译器 API
Java Lambda表达式与函数式编程入门
Lambda表达式是Java 8引入的重要特性,简化了函数式编程的实现方式。它通过简洁的语法替代传统的匿名内部类,使代码更清晰、易读。本文深入讲解Lambda表达式的基本语法、函数式接口、方法引用等核心概念,并结合集合操作、线程处理、事件回调等实战案例,帮助开发者掌握现代Java编程技巧。同时,还解析了面试中高频出现的相关问题,助你深入理解其原理与应用场景。
|
3月前
|
安全 Java 数据库连接
2025 年最新 Java 学习路线图含实操指南助你高效入门 Java 编程掌握核心技能
2025年最新Java学习路线图,涵盖基础环境搭建、核心特性(如密封类、虚拟线程)、模块化开发、响应式编程、主流框架(Spring Boot 3、Spring Security 6)、数据库操作(JPA + Hibernate 6)及微服务实战,助你掌握企业级开发技能。
444 3
|
3月前
|
算法 Java 测试技术
零基础学 Java: 从语法入门到企业级项目实战的详细学习路线解析
本文为零基础学习者提供完整的Java学习路线,涵盖语法基础、面向对象编程、数据结构与算法、多线程、JVM原理、Spring框架、Spring Boot及项目实战,助你从入门到进阶,系统掌握Java编程技能,提升实战开发能力。
167 0