CAS 在 Java 并发工具中的应用

一、前言

在前面文章中,我们系统学习了 CAS 的核心思想、底层实现以及三大核心问题的解决方案。理论知识最终要服务于实践,而 CAS 作为 Java 无锁并发编程的基石,早已深度融入 JDK 的并发工具体系中。

本文将从实战角度出发,带你剖析CAS 在 原子类家族 、 无锁并发容器 、 JVM 锁优化三大场景中的核心应用,并通过一个分布式 ID 生成器的案例,让你掌握如何在实际业务中落地 CAS 思想。

二、CAS是Java并发体系的"底层支柱"

Java 的 java.util.concurrent 包(简称 JUC)是并发编程的核心工具库,而这个包下的大部分工具,其底层实现都离不开 CAS。

为什么 JUC 如此依赖 CAS?原因很简单:相比传统的synchronized重量级锁,CAS 的无锁特性能够大幅降低并发冲突时的性能开销,提升系统的并发吞吐量。无论是原子类、并发队列,还是锁的优化机制,CAS 都在其中扮演着关键角色。

接下来,我们就通过源码分析和实战案例,揭开 CAS 在这些并发工具中的应用面纱。

应用一:原子类家族

JUC 的 java.util.concurrent.atomic 包下提供了一系列原子类,这些类是 CAS 最直接的应用体现。根据处理的数据类型,我们可以将原子类分为四大类:基本类型原子类 、 引用类型原子类 、 数组类型原子类 、 字段更新器原子类。

1.基本类型原子类

基本类型原子类是最常用的原子类,用于处理 int 、 long 、 boolean 三种基本数据类型的原子操作,核心实现类包括 AtomicInteger 、 AtomicLong 、 AtomicBoolean 。

核心原理回顾

以 AtomicInteger 为例,它的底层通过Unsafe类的compareAndSwapInt方法实现 CAS 操作,配合 volatile 修饰的 value 字段保证可见性,最终实现了无锁的原子性操作。

我们回顾 AtomicInteger 的 incrementAndGet 方法(原子自增并返回新值):

java 复制代码
public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
// Unsafe类的getAndAddInt方法
public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    // 自旋CAS:失败后重试,直到成功
    do {
        v = this.getIntVolatile(o, offset);
    } while(!this.compareAndSwapInt(o, offset, v, v + delta));
    return v;
}

可以看到, getAndAddInt 方法的核心就是自旋 CAS :不断尝试更新 value 值,直到 CAS 操作成功为止。

实战场景:高并发计数器

在高并发场景下,使用 AtomicInteger 实现计数器是最佳选择之一,相比 synchronized 锁,性能提升非常明显。

java 复制代码
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
/**
 * 基于AtomicInteger的高并发计数器
 */
public class AtomicCounterDemo {
    // 原子计数器,初始值为0
    private static final AtomicInteger COUNTER = new AtomicInteger(0);
    // 线程数:模拟高并发
    private static final int THREAD_NUM = 2000;
    // 倒计时门栓:等待所有线程执行完成
    private static final CountDownLatch COUNT_DOWN_LATCH = new CountDownLatch(THREAD_NUM);
    public static void main(String[] args) throws InterruptedException {
        long startTime = System.currentTimeMillis();
        // 启动2000个线程,每个线程执行1000次自增
        for (int i = 0; i < THREAD_NUM; i++) {
            new Thread(() -> {
                try {
                    for (int j = 0; j < 1000; j++) {
                        COUNTER.incrementAndGet();
                    }
                } finally {
                    COUNT_DOWN_LATCH.countDown();
                }
            }).start();
        }
        // 等待所有线程执行完成
        COUNT_DOWN_LATCH.await();
        long endTime = System.currentTimeMillis();
        System.out.println("最终计数结果:" + COUNTER.get());
        System.out.println("执行耗时:" + (endTime - startTime) + "ms");
    }
}

运行结果 :

java 复制代码
最终计数结果:2000000
执行耗时:35ms

这个结果验证了 AtomicInteger 在高并发下的线程安全性和高效性。

2.引用类型原子类

引用类型原子类用于处理对象引用的原子操作,核心实现类包括 AtomicReference 、 AtomicStampedReference 、 AtomicMarkableReference 。

  • **AtomicReference:**基础的对象引用原子类,支持对对象引用的 CAS 操作。

  • **AtomicStampedReference:**解决 ABA 问题的引用原子类,通过 值 + 版本号 的组合实现 CAS 操作。

  • **AtomicMarkableReference:**通过 值 + 布尔标记 的组合实现 CAS 操作,只关心值是否被修改过,不关心修改次数。

实战场景:原子更新用户信息

我们可以使用 AtomicReference 实现用户信息的原子更新,保证多线程下用户信息的一致性。

java 复制代码
import java.util.concurrent.atomic.AtomicReference;
/**
 * 基于AtomicReference的用户信息原子更新
 */
public class AtomicReferenceUserDemo {
    // 用户信息类
    static class User {
        private String name;
        private int age;
        public User(String name, int age) {
            this.name = name;
            this.age = age;
        }
        // 省略getter/setter/toString方法
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
        public int getAge() {
            return age;
        }
        public void setAge(int age) {
            this.age = age;
        }
        @Override
        public String toString() {
            return "User{name='" + name + "', age=" + age + "}";
        }
    }
    public static void main(String[] args) {
        // 初始化用户信息
        User initialUser = new User("张三", 20);
        AtomicReference<User> userRef = new AtomicReference<>(initialUser);
        System.out.println("初始用户信息:" + userRef.get());
        // 线程1:更新用户姓名和年龄
        new Thread(() -> {
            User current = userRef.get();
            User newUser = new User("李四", 25);
            boolean result = userRef.compareAndSet(current, newUser);
            System.out.println("线程1更新是否成功:" + result + ",更新后用户:" + userRef.get());
        }).start();
        // 线程2:尝试更新用户信息(会失败,因为线程1已更新)
        new Thread(() -> {
            User current = userRef.get();
            User newUser = new User("王五", 30);
            boolean result = userRef.compareAndSet(current, newUser);
            System.out.println("线程2更新是否成功:" + result + ",当前用户:" + userRef.get());
        }).start();
    }
}

运行结果 :

java 复制代码
初始用户信息:User{name='张三', age=20}
线程1更新是否成功:true,更新后用户:User{name='李四', age=25}
线程2更新是否成功:false,当前用户:User{name='李四', age=25}

3.数组类型原子类

数组类型原子类用于实现数组元素的原子操作,核心实现类包括 AtomicIntegerArray 、 AtomicLongArray 、 AtomicReferenceArray 。

与普通数组相比,数组类型原子类的优势在于:可以对数组中的单个元素进行原子操作,无需锁定整个数组。

实战场景:原子更新数组元素

java 复制代码
import java.util.concurrent.atomic.AtomicIntegerArray;
/**
 * 基于AtomicIntegerArray的数组元素原子更新
 */
public class AtomicArrayDemo {
    public static void main(String[] args) {
        // 初始化数组:[1, 2, 3]
        int[] arr = {1, 2, 3};
        AtomicIntegerArray atomicArr = new AtomicIntegerArray(arr);
        // 原子更新索引0的元素:预期值1,新值10
        boolean result1 = atomicArr.compareAndSet(0, 1, 10);
        System.out.println("索引0更新是否成功:" + result1 + ",更新后数组:" + atomicArr);
        // 原子自增索引1的元素
        int newValue = atomicArr.incrementAndGet(1);
        System.out.println("索引1自增后的值:" + newValue + ",当前数组:" + atomicArr);
    }
}

运行结果 :

java 复制代码
索引0更新是否成功:true,更新后数组:[10, 2, 3]
索引1自增后的值:3,当前数组:[10, 3, 3]

4.字段更新器原子类

字段更新器原子类用于实现对对象的非原子字段的原子操作,核心实现类包括 AtomicIntegerFieldUpdater 、 AtomicLongFieldUpdater 、 AtomicReferenceFieldUpdater 。

它的核心优势在于:无需将字段声明为原子类型,就能实现原子操作,降低了代码耦合度。

注意事项

  1. 要更新的字段必须被 volatile 修饰,保证可见性;

  2. 字段不能是 private 的,否则无法通过反射获取字段。

实战场景:原子更新对象的普通字段

java 复制代码
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
/**
 * 基于AtomicIntegerFieldUpdater的字段原子更新
 */
public class AtomicFieldUpdaterDemo {
    static class User {
        private String name;
        // 必须用volatile修饰,且不能是private
        volatile int age;
        public User(String name, int age) {
            this.name = name;
            this.age = age;
        }
        // 省略getter/setter/toString方法
        public String getName() {
            return name;
        }
        public int getAge() {
            return age;
        }
        @Override
        public String toString() {
            return "User{name='" + name + "', age=" + age + "}";
        }
    }
    public static void main(String[] args) {
        // 创建字段更新器:指定要更新的类和字段名
        AtomicIntegerFieldUpdater<User> ageUpdater = AtomicIntegerFieldUpdater.newUpdater(User.class, "age");
        User user = new User("张三", 20);
        System.out.println("初始用户信息:" + user);
        // 原子自增年龄
        ageUpdater.incrementAndGet(user);
        System.out.println("年龄自增后:" + user);
        // 原子更新年龄:预期值21,新值25
        boolean result = ageUpdater.compareAndSet(user, 21, 25);
        System.out.println("年龄更新是否成功:" + result + ",最终用户信息:" + user);
    }
}

运行结果 :

java 复制代码
初始用户信息:User{name='张三', age=20}
年龄自增后:User{name='张三', age=21}
年龄更新是否成功:true,最终用户信息:User{name='张三', age=25}
复制代码

应用二:并发容器中的 CAS

JUC 提供了多个高性能的并发容器,其中无锁并发队列 ConcurrentLinkedQueue 是 CAS 的典型应用场景。它的核心特点是:基于链表实现,通过 CAS 操作完成入队和出队,无需加锁,适合高并发读写场景。

1.ConcurrentLinkedQueue的核心结构

ConcurrentLinkedQueue 是一个单向链表结构,包含两个核心节点:

  1. **head:**队列的头节点,指向队列的第一个元素;

  2. **tail:**队列的尾节点,指向队列的最后一个元素。

这两个节点都被 volatile 修饰,保证多线程下的可见性。

2.入队操作(offer 方法)的 CAS 实现

入队操作的核心目标是:将新节点添加到队列的尾部。由于是无锁实现,需要通过 CAS 操作保证尾节点的原子更新。

我们来看 offer 方法的核心源码(JDK 1.8):

java 复制代码
public boolean offer(E e) {
    checkNotNull(e);
    // 创建新节点
    final Node<E> newNode = new Node<E>(e);
    // 自旋CAS:从尾节点开始,尝试将新节点添加到尾部
    for (Node<E> t = tail, p = t;;) {
        Node<E> q = p.next;
        // 情况1:p是尾节点(q为null)
        if (q == null) {
            // CAS操作:将p的next指向新节点
            if (p.casNext(null, newNode)) {
                // 如果p不等于t,说明尾节点已经被其他线程更新,需要将tail指向新节点
                if (p != t)
                    casTail(t, newNode);
                return true;
            }
        }
        // 情况2:遇到哨兵节点(p == q),重新定位尾节点
        else if (p == q)
            p = (t != (t = tail)) ? t : head;
        // 情况3:p不是尾节点,继续向后遍历
        else
            p = (p != t && t != (t = tail)) ? t : q;
    }
}

核心逻辑解析

  1. **自旋 CAS:**通过 for 循环实现自旋,不断尝试将新节点添加到队列尾部;

  2. **CAS 更新 next 指针:**p.casNext(null, newNode) 是核心 CAS 操作,保证只有一个线程能成功将新节点添加到当前尾节点的后面;

  3. **延迟更新 tail 节点:**为了减少 CAS 操作的次数, tail 节点的更新采用了延迟策略 ------ 只有当 p != t 时,才会通过 casTail 更新 tail 节点。

3.出队操作(poll 方法)的 CAS 实现

出队操作的核心目标是:移除队列的头节点,并返回头节点的元素。同样通过 CAS 操作保证头节点的原子更新。

核心源码(JDK 1.8):

java 复制代码
public E poll() {
    restartFromHead:
    // 自旋CAS
    for (;;) {
        for (Node<E> h = head, p = h, q;;) {
            E item = p.item;
            // 情况1:当前节点有元素,尝试通过CAS将item置为null
            if (item != null && p.casItem(item, null)) {
                // 延迟更新head节点
                if (p != h)
                    updateHead(h, ((q = p.next) != null) ? q : p);
                return item;
            }
            // 情况2:当前节点无元素,队列已空
            else if ((q = p.next) == null) {
                updateHead(h, p);
                return null;
            }
            // 情况3:遇到哨兵节点,重新遍历
            else if (p == q)
                continue restartFromHead;
            // 情况4:继续向后遍历
            else
                p = q;
        }
    }
}

核心逻辑解析

  1. CAS 清空 item: p.casItem(item, null) 是核心 CAS 操作,保证只有一个线程能成功获取当前节点的元素;

  2. **延迟更新 head 节点:**与 tail 节点类似, head 节点的更新也采用延迟策略,减少 CAS 操作的开销;

  3. **哨兵节点处理:**当 p == q 时,说明遇到了哨兵节点,需要重新从 head 节点开始遍历。

4.ConcurrentLinkedQueue 的优势

相比基于锁的队列(如 LinkedBlockingQueue ), ConcurrentLinkedQueue 的优势在于:

  • **无锁设计:**避免了锁竞争带来的上下文切换开销;

  • **高并发性能:**在高并发读写场景下,吞吐量远高于基于锁的队列;

  • **无界队列:**理论上可以存储无限多的元素,不会出现队列满的情况。

应用三:锁优化中的 CAS

在 JDK 1.6 之后, synchronized 锁引入了锁升级 机制:无锁 → 偏向锁 → 轻量级锁 → 重量级锁 。其中,偏向锁轻量级锁的实现都依赖于 CAS 操作,这也是 synchronized 锁性能大幅提升的关键原因。

1.锁升级的核心流程

我们先简单回顾锁升级的流程:

  1. **无锁状态:**对象头中没有任何锁信息,多个线程可以自由访问对象;

  2. **偏向锁状态:**当一个线程第一次获取锁时,通过 CAS 操作将对象头中的线程 ID设置为当前线程的 ID,后续该线程再次获取锁时,无需竞争,直接获取;

  3. **轻量级锁状态:**当有其他线程尝试获取偏向锁时,偏向锁会升级为轻量级锁,线程通过 CAS 操作竞争锁,竞争失败时会自旋等待;

  4. **重量级锁状态:**当自旋次数达到阈值,或者有更多线程竞争锁时,轻量级锁会升级为重量级锁,此时线程会被阻塞,等待锁释放。

2.CAS 在偏向锁中的应用

偏向锁的核心目标是:减少无竞争场景下的锁开销。它的实现完全依赖 CAS 操作:

  1. **加锁:**线程第一次获取锁时,通过 CAS 操作将对象头中的 ThreadId 字段设置为当前线程的 ID。如果 CAS 成功,偏向锁设置成功;

  2. **解锁:**线程释放锁时,不需要做任何操作,只有当其他线程尝试获取锁时,才会通过 CAS 操作撤销偏向锁。

3.CAS 在轻量级锁中的应用

轻量级锁的核心目标是:减少重量级锁的阻塞开销。它的实现同样依赖 CAS 操作:

  1. **加锁:**线程获取锁时,会在自己的栈帧中创建一个 锁记录(Lock Record) ,然后通过 CAS 操作将对象头中的 锁记录指针 指向当前线程的锁记录。如果 CAS 成功,轻量级锁获取成功;

  2. **自旋等待:**如果 CAS 失败,说明有其他线程竞争锁,当前线程会自旋等待(通过循环不断尝试 CAS 操作),而不是立即阻塞;

  3. **解锁:**线程释放锁时,通过 CAS 操作将对象头中的锁记录指针置为 null。如果 CAS 成功,轻量级锁释放成功;如果失败,说明有其他线程在自旋等待,需要唤醒这些线程。

4.CAS 在锁优化中的价值

CAS 在锁优化中的核心价值在于:**将锁的竞争从 "阻塞等待" 转化为 "自旋重试",大幅降低了锁竞争的性能开销。**只有当自旋次数达到阈值时,才会升级为重量级锁,保证系统的稳定性。

三、实践案例

在实际业务中,分布式 ID 生成器是一个非常常见的需求。一个优秀的分布式 ID 生成器需要满足:唯一性、有序性、高性能、高可用。

基于 CAS 的原子类 AtomicLong ,我们可以实现一个简单高效的分布式 ID 生成器,核心思路是:结合机器 ID、时间戳和原子计数器,生成唯一的 ID。

1.需求分析

我们需要生成的 ID 格式如下(64 位长整型):

  • **时间戳(41 位):**精确到毫秒,可以使用约 69 年;

  • **机器 ID(10 位):**支持最多 1024 台机器部署;

  • **序列号(13 位):**每台机器每毫秒最多生成 8192 个 ID。

代码实现

java 复制代码
import java.util.concurrent.atomic.AtomicLong;
/**
 * 基于CAS的分布式ID生成器
 */
public class CASDistributedIdGenerator {
    // 起始时间戳:2025-01-01 00:00:00(毫秒)
    private static final long START_TIMESTAMP = 1735651200000L;
    // 机器ID的位数
    private static final int MACHINE_ID_BITS = 10;
    // 序列号的位数
    private static final int SEQUENCE_BITS = 13;
    // 机器ID的最大值:2^10 - 1 = 1023
    private static final long MAX_MACHINE_ID = (1 << MACHINE_ID_BITS) - 1;
    // 序列号的最大值:2^13 - 1 = 8191
    private static final long MAX_SEQUENCE = (1 << SEQUENCE_BITS) - 1;
    // 机器ID的偏移量:13
    private static final long MACHINE_ID_SHIFT = SEQUENCE_BITS;
    // 时间戳的偏移量:13 + 10 = 23
    private static final long TIMESTAMP_SHIFT = SEQUENCE_BITS + MACHINE_ID_BITS;
    // 机器ID(通过构造方法传入)
    private final long machineId;
    // 原子计数器:用于生成序列号,基于CAS实现原子自增
    private final AtomicLong sequence = new AtomicLong(0);
    // 上一次生成ID的时间戳
    private long lastTimestamp = -1L;
    /**
     * 构造方法:校验机器ID的合法性
     * @param machineId 机器ID(0~1023)
     */
    public CASDistributedIdGenerator(long machineId) {
        if (machineId < 0 || machineId > MAX_MACHINE_ID) {
            throw new IllegalArgumentException("机器ID必须在0~1023之间");
        }
        this.machineId = machineId;
    }
    /**
     * 生成分布式ID
     * @return 64位分布式ID
     */
    public synchronized long generateId() {
        long currentTimestamp = System.currentTimeMillis();
        // 1. 校验时间戳:如果当前时间小于上一次生成ID的时间,说明时钟回拨,抛出异常
        if (currentTimestamp < lastTimestamp) {
            throw new RuntimeException("时钟回拨,无法生成ID");
        }
        // 2. 处理同一毫秒内的序列号生成
        if (currentTimestamp == lastTimestamp) {
            // 原子自增序列号,并取模最大值
            long currentSequence = sequence.incrementAndGet() & MAX_SEQUENCE;
            // 如果序列号溢出,等待下一个毫秒
            if (currentSequence == 0) {
                currentTimestamp = waitNextMillis(lastTimestamp);
            }
        } else {
            // 3. 不同毫秒,重置序列号为0
            sequence.set(0);
        }
        // 4. 更新上一次的时间戳
        lastTimestamp = currentTimestamp;
        // 5. 拼接ID:时间戳左移23位 + 机器ID左移13位 + 序列号
        return ((currentTimestamp - START_TIMESTAMP) << TIMESTAMP_SHIFT)
                | (machineId << MACHINE_ID_SHIFT)
                | sequence.get();
    }
    /**
     * 等待下一个毫秒
     * @param lastTimestamp 上一次的时间戳
     * @return 下一个毫秒的时间戳
     */
    private long waitNextMillis(long lastTimestamp) {
        long currentTimestamp = System.currentTimeMillis();
        while (currentTimestamp <= lastTimestamp) {
            currentTimestamp = System.currentTimeMillis();
        }
        return currentTimestamp;
    }
    // 测试方法
    public static void main(String[] args) {
        CASDistributedIdGenerator idGenerator = new CASDistributedIdGenerator(1);
        // 启动10个线程,每个线程生成10个ID
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                for (int j = 0; j < 10; j++) {
                    long id = idGenerator.generateId();
                    System.out.println(Thread.currentThread().getName() + " 生成的ID:" + id);
                }
            }).start();
        }
    }
}

2.核心亮点

  1. **高性能:**基于 AtomicLong 的 CAS 操作实现序列号的原子自增,无锁竞争,性能优异;

  2. **唯一性:**通过时间戳 + 机器 ID + 序列号的组合,保证了 ID 的全局唯一性;

  3. **有序性:**同一台机器生成的 ID,按照时间戳递增,满足有序性需求;

  4. **容错性:**处理了时钟回拨问题,保证 ID 生成的稳定性。

3.运行结果

java 复制代码
Thread-0 生成的ID:1234567890123456789
Thread-0 生成的ID:1234567890123456790
...
Thread-9 生成的ID:1234567890123457888
Thread-9 生成的ID:1234567890123457889

四、总结

本文我们深入剖析了 CAS 在 Java 并发工具中的三大核心应用场景,并通过一个分布式 ID 生成器的案例,实现了 CAS 思想的落地。核心知识点总结如下:

  1. **原子类家族:**CAS 是原子类的底层核心,不同类型的原子类(基本类型、引用类型、数组类型、字段更新器)满足了不同场景的原子操作需求;

  2. **无锁并发容器:**ConcurrentLinkedQueue 通过 CAS 操作实现了无锁的入队和出队,大幅提升了高并发场景下的吞吐量;

  3. **锁优化:**偏向锁和轻量级锁的实现依赖 CAS 操作,将锁竞争从 "阻塞等待" 转化为 "自旋重试",降低了锁开销;

  4. **业务实践:**基于 CAS 的原子类可以实现高性能的分布式 ID 生成器,满足唯一性、有序性等核心需求。

CAS 作为无锁并发编程的基石,其应用远不止于此。在下一篇文章中,我们将对比 CAS 与其他并发方案的优劣,并整理面试中常见的 CAS 相关问题,帮助你全面掌握 CAS 知识体系。

相关推荐
范纹杉想快点毕业2 小时前
嵌入式系统架构之道:告别“意大利面条”,拥抱状态机与事件驱动
java·开发语言·c++·嵌入式硬件·算法·架构·mfc
2501_940315262 小时前
【无标题】2390:从字符串中移除*
java·开发语言·算法
半聋半瞎2 小时前
Flowable快速入门(Spring Boot整合版)
java·spring boot·后端·flowable
散峰而望2 小时前
【算法竞赛】树
java·数据结构·c++·算法·leetcode·贪心算法·推荐算法
毕设源码-邱学长2 小时前
【开题答辩全过程】以 基于SpringBoot的理工学院学术档案管理系统为例,包含答辩的问题和答案
java·spring boot·后端
shejizuopin2 小时前
基于SSM的高校旧书交易系统的设计与实现(毕业论文)
java·mysql·毕业设计·论文·ssm·毕业论文·高校旧书交易系统的设计与实现
修己xj2 小时前
SpringBoot解析.mdb文件实战指南
java·spring boot·后端
咩图3 小时前
Sketchup软件二次开发+Ruby+VisualStudioCode
java·前端·ruby
我命由我123453 小时前
Android 开发问题:Duplicate class android.support.v4.app.INotificationSideChannel...
android·java·开发语言·java-ee·android studio·android-studio·android runtime