并发编程(二):Java原子类(Atomic Classes)全解析

文章目录

在多线程并发编程的浩瀚宇宙中,线程安全 始终是核心议题。当我们需要对一个变量进行简单的累加操作时,volatile 关键字虽然能保证可见性,却无法保证原子性;而 synchronized 锁虽然强大,但在高并发场景下往往伴随着沉重的上下文切换开销。

这时,Java原子类(Atomic Classes) 应运而生。它们位于 java.util.concurrent.atomic 包下,利用底层硬件的 CAS 机制,以一种无锁(Lock-Free)的高效方式,解决了并发场景下的原子性问题。


1. 核心原理:CAS (Compare-And-Swap)

原子类的魔法源自于 CAS。它是一条 CPU 并发原语。

什么是 CAS

关于CAS的原理可以看这篇:https://blog.csdn.net/Tracycoder/article/details/156860586?spm=1011.2415.3001.10575\&sharefrom=mp_manage_link

Java 中的 Unsafe 类

Java 无法直接访问底层操作系统,而是通过 sun.misc.Unsafe 类来操作内存。原子类内部正是调用了 Unsafe 的 compareAndSwap 系列方法。


2. 原子类家族谱系

JDK 提供了丰富的原子类,主要可以分为以下四类:

分类 包含类 描述
基本类型 AtomicInteger, AtomicLong, AtomicBoolean 直接更新基本类型数据。
数组类型 AtomicIntegerArray, AtomicLongArray, AtomicReferenceArray 原子更新数组中的某个元素。
引用类型 AtomicReference, AtomicStampedReference, AtomicMarkableReference 原子更新对象引用,解决ABA问题。
对象属性 AtomicIntegerFieldUpdater, AtomicLongFieldUpdater, AtomicReferenceFieldUpdater 原子更新对象的某个字段。

3. 实战演练:AtomicInteger

AtomicInteger 是最常用的原子类。让我们对比一下使用 intAtomicInteger 在多线程下的表现。

场景:多线程计数器

假设有 100 个线程,每个线程对计数器累加 1000 次。

错误示范:普通 int
java 复制代码
public class UnsafeCounter {
    private static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[100];
        for (int i = 0; i < 100; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    count++; // 非原子操作:读 -> 改 -> 写
                }
            });
            threads[i].start();
        }

        for (Thread t : threads) t.join();
        
        // 预期结果:100000,实际结果通常小于 100000
        System.out.println("Final Count (Unsafe): " + count);
    }
}
正确示范:AtomicInteger
java 复制代码
import java.util.concurrent.atomic.AtomicInteger;

public class SafeCounter {
    // 使用原子类
    private static AtomicInteger atomicCount = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[100];
        for (int i = 0; i < 100; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    // 相当于 i++,但在硬件层面保证原子性
                    atomicCount.getAndIncrement(); 
                }
            });
            threads[i].start();
        }

        for (Thread t : threads) t.join();

        // 结果始终为 100000
        System.out.println("Final Count (Atomic): " + atomicCount.get());
    }
}

源码剖析:getAndIncrement

AtomicIntegergetAndIncrement 是如何实现的?

java 复制代码
// JDK 8 源码片段
public final int getAndIncrement() {
    // this: 当前对象
    // valueOffset: value 字段在内存中的偏移量
    // 1: 增加的值
    return unsafe.getAndAddInt(this, valueOffset, 1);
}

// Unsafe 类中的实现 (模拟)
public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    do {
        // 1. 获取内存中当前的值
        v = getIntVolatile(o, offset);
        // 2. 循环尝试 CAS 操作
        // 如果内存值还是 v,则更新为 v + delta,返回 true 退出循环
        // 否则(说明被其他线程改了),返回 false,继续循环(自旋)
    } while (!compareAndSwapInt(o, offset, v, v + delta));
    return v;
}

5. 高性能利器:LongAdder (JDK 8+)

虽然 AtomicLong 使用 CAS 保证了原子性,但在超高并发下,大量线程同时竞争更新同一个变量,会导致大量 CAS 失败并自旋,消耗 CPU 资源。

JDK 8 引入了 LongAdder

设计思想:分段锁(空间换时间)

LongAdder 内部维护了一个 base 变量和一个 Cell[] 数组。

  • 无竞争时:直接更新 base
  • 有竞争时:线程被哈希映射到 Cell 数组的某个槽位,对该槽位的值进行 CAS 更新。
  • 获取最终结果时:sum = base + sum(Cell[])

AtomicLong vs LongAdder 性能对比示意

LongAdder
CAS
Hash
Hash
Hash
Thread A
Base
Thread B
Cell 1
Thread C
Cell 2
Thread D
AtomicLong
CAS 竞争
CAS 竞争
CAS 竞争
CAS 竞争
Thread 1
Value
Thread 2
Thread 3
Thread 4

代码示例
java 复制代码
import java.util.concurrent.atomic.LongAdder;

public class LongAdderDemo {
    public static void main(String[] args) {
        LongAdder adder = new LongAdder();
        
        // 多个线程同时累加
        adder.increment();
        adder.add(10);
        
        // 获取总和
        System.out.println("Sum: " + adder.sum());
    }
}

注意: LongAddersum() 方法返回的值只是一个近似准确值(在并发极高时,统计过程中可能有新的累加发生),但在统计计数等场景下完全够用且性能极佳。

相关推荐
考虑考虑36 分钟前
JDK25模块导入声明
java·后端·java ee
_小马快跑_2 小时前
Java 的 8 大基本数据类型:为何是不可或缺的设计?
java
Re_zero4 小时前
线上日志被清空?这段仅10行的 IO 代码里竟然藏着3个毒瘤
java·后端
洋洋技术笔记5 小时前
Spring Boot条件注解详解
java·spring boot
程序员清风1 天前
程序员兼职必看:靠谱软件外包平台挑选指南与避坑清单!
java·后端·面试
皮皮林5511 天前
利用闲置 Mac 从零部署 OpenClaw 教程 !
java
华仔啊1 天前
挖到了 1 个 Java 小特性:var,用完就回不去了
java·后端
SimonKing1 天前
SpringBoot整合秘笈:让Mybatis用上Calcite,实现统一SQL查询
java·后端·程序员
日月云棠2 天前
各版本JDK对比:JDK 25 特性详解
java
用户8307196840822 天前
Spring Boot 项目中日期处理的最佳实践
java·spring boot