【Java并发】【原子类】适合初学体质的原子类入门
👋hi,我不是一名外包公司的员工,也不会偷吃茶水间的零食,我的梦想是能写高端CRUD
🔥 2025本人正在沉淀中… 博客更新速度++
👍 欢迎点赞、收藏、关注,跟上我的更新节奏
📚欢迎订阅专栏,专栏名《在2B工作中寻求并发是否搞错了什么》
什么是CAS?
说到原子类,首先就要说到CAS:
CAS(Compare and Swap) 是一种无锁的原子操作,用于实现多线程环境下的安全数据更新。
CAS(Compare and Swap) 的本质是 “无锁更新” 。它的核心思想是:
- 先检查:在修改共享变量之前,先检查当前值是否符合预期。
- 再更新:如果符合预期,则更新为新值;否则放弃或重试。
- 原子性保证:整个过程由 CPU 硬件指令(如
cmpxchg
)直接支持,确保不可中断。
Java 通过 java.util.concurrent.atomic
包中的原子类(如 AtomicInteger
、AtomicReference
等)提供 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
在多线程环境中,如果多个线程同时修改一个共享的可变对象,可能会导致数据不一致。传统的解决方法是使用 synchronized
或 Lock
加锁,但锁会导致线程阻塞,降低并发性能。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用于在多线程环境下原子性地操作一个整数数组。它提供了对数组中每个元素的原子性操作(如 get
、set
、compareAndSet
、incrementAndGet
等),确保多线程修改数组元素时的线程安全性。每个方法(如 get
、set
、addAndGet
)都是原子性的,无需额外同步。
让我们从一个简单的例子开始吧!!
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的对比
机制 | LongAdder | AtomicLong |
---|---|---|
存储方式 | 分散到多个 Cell 中 | 单一的 volatile long 变量 |
竞争处理 | 线程优先修改各自对应的 Cell ,减少冲突 | 所有线程竞争同一个变量的 CAS 操作 |
读取结果 | 调用 sum() 需要合并所有 Cell 的值 | get() 直接返回当前值 |
适用场景 | 高并发写入,低频读取(如统计计数) | 低并发或需要实时读取值的场景 |
后话
( ̄▽ ̄)"怎么样?聪明的你是否对原子类的使用,拥有了更多的理解。