当前位置: 首页 > news >正文

【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实现线程安全计数器:

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,版本号 0Resource 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;  // 每个线程的操作次数// 测试 LongAdderprivate 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());}// 测试 AtomicLongprivate 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();    // 先测试 LongAddertestAtomicLong();  // 再测试 AtomicLong}
}

输出结果

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

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

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

AtomicLong的对比

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

后话

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


http://www.mrgr.cn/news/100298.html

相关文章:

  • QT6 源(52)篇二:存储 c 语言字符串的类 QByteArray 的使用举例,
  • Win7 SSL证书问题
  • 如何打包python程序为可执行文件
  • Netmiko 源码解析
  • 精益数据分析(29/126):深入剖析电子商务商业模式
  • 【C++11】右值引用和移动语义:万字总结
  • 论人际关系发展的阶段
  • 毕业项目-Web入侵检测系统
  • CANFD技术在实时运动控制系统中的应用:协议解析、性能测试与未来发展趋势
  • C++如何设计线程池(thread pool)来提高线程的复用率,减少线程创建和销毁的开销
  • 使用MyBatis注解方式的完整示例,涵盖CRUD、动态SQL、分页、事务管理等场景,并附详细注释和对比表格
  • AI大模型学习十一:‌尝鲜ubuntu 25.04 桌面版私有化sealos cloud + devbox+minio,实战运行成功
  • 腾讯一面面经:总结一下
  • [ESP-IDF]:esp32-camera 使用指南 ESP32S3-OV2640 用例测试
  • 【Linux】轻量级命令解释器minishell
  • 【Lua】Lua 入门知识点总结
  • Hbase集群管理与实践
  • AI大模型学习十二:‌尝鲜ubuntu 25.04 桌面版私有化sealos cloud + devbox+minio对象存储测试和漫长修改之路
  • 爬虫-oiwiki
  • 【JavaEE】Spring AOP的注解实现