解锁并发编程高性能:原子类、ConcurrentHashMap 与 CompletableFuture 核心揭秘

94 阅读12分钟

一、原子类(Atomic Classes)

1. 核心概念

  • 无锁编程:基于CAS(Compare-And-Swap)机制,避免锁竞争,提升并发性能。

  • 常见原子类

    • AtomicIntegerAtomicLong:数值原子操作。
    • AtomicReference:对象引用原子操作。
    • AtomicStampedReference:解决ABA问题的带版本号原子类。
    • LongAdder:高并发场景下替代AtomicLong,分段累加减少竞争。

2. CAS原理

  • 操作流程

    1. 读取内存值V。
    2. 计算新值B。
    3. 当且仅当内存值仍为V时,将值更新为B,否则重试。
  • 底层实现:通过CPU指令(如x86的CMPXCHG)保证原子性。

在 x86 架构中,CMPXCHG指令用于比较并交换操作数,它通过硬件层面的机制来保证原子性:
  • 使用总线锁

    • 在多处理器系统中,当一个 CPU 执行CMPXCHG指令时,它会在总线上发出一个锁定信号,这个信号会阻止其他 CPU 同时访问共享内存中的同一个地址。
    • 例如,在一个双 CPU 系统中,当 CPU0 执行CMPXCHG指令来操作内存地址 0x1000 时,它会发出总线锁信号,此时 CPU1 就无法访问地址 0x1000,直到 CPU0 完成CMPXCHG操作并释放总线锁。
  • 利用缓存一致性协议

    • x86 架构采用了诸如 MESI 等缓存一致性协议。当执行CMPXCHG指令时,不仅会在总线上进行操作,还会与其他 CPU 的缓存进行交互和协调。
    • 例如,当 CPU0 要对一个在其缓存中处于 Modified 状态的变量执行CMPXCHG操作时,它会根据 MESI 协议,将缓存中的数据写回主存,并使其他 CPU 中对应的缓存行无效。在这个过程中,CMPXCHG操作被确保是原子的,其他 CPU 在该操作完成前无法访问该变量的缓存行。
  • 基于指令流水线和硬件设计

    • x86 处理器的指令流水线和硬件设计确保了CMPXCHG指令在执行过程中不会被中断或干扰。它被设计为一个不可分割的操作,从读取操作数、比较到交换,都在一个连续的硬件操作序列中完成。
    • 例如,在现代的 x86 处理器中,CMPXCHG指令会在一个特定的执行单元中按照严格的顺序执行,在指令执行期间,处理器不会响应其他可能会干扰该操作的指令或事件,直到CMPXCHG完成。

3. 使用示例

AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet(); // 原子递增
counter.compareAndSet(0, 1); // CAS更新

4. 典型问题与解决

  • ABA问题

    • 场景:线程1读取值A,线程2将A→B→A,线程1的CAS误判未变化。
    • 解决:使用AtomicStampedReference添加版本号。
  • 性能瓶颈

    • 场景:高并发下CAS频繁失败,导致自旋开销。
    • 解决:改用LongAdder(分段累加减少竞争)。

LongAdder核心思想

传统的 AtomicLong 在高并发场景下,由于所有线程都要竞争同一个原子变量,会导致大量的 CAS(Compare-And-Swap)操作失败和重试,从而影响性能。LongAdder 的核心思想是将一个总的计数值分散到多个 Cell 对象中,不同的线程可以对不同的 Cell 进行操作,减少了线程之间的竞争,最后求和时将所有 Cell 的值以及基础值相加得到最终结果。

主要组成部分

  1. Cell 数组Cell 是一个内部类,用于存储部分计数值。多个 Cell 组成一个数组,不同的线程可以操作不同的 Cell,从而避免竞争。
  2. base 变量:这是一个基础值,在没有竞争的情况下,直接对 base 进行操作。
  3. cellsBusy 变量:这是一个标志位,用于在初始化或扩容 Cell 数组时进行加锁,保证操作的线程安全。

实现原理步骤

1. 无竞争情况

当没有线程竞争时,LongAdder 会直接对 base 变量进行操作,就像 AtomicLong 一样,使用 CAS 操作来保证线程安全。例如,调用 increment() 方法时,会尝试使用 CAS 操作将 base 的值加 1。

public void increment() {
    add(1L);
}

public void add(long x) {
    Cell[] as; long b, v; int m; Cell a;
    // 如果 cells 数组不为空 或者 对 base 进行 CAS 操作失败(说明有竞争)
    if ((as = cells) != null || !casBase(b = base, b + x)) { 
        boolean uncontended = true;
        if (as == null || (m = as.length - 1) < 0 ||
            (a = as[getProbe() & m]) == null ||
            !(uncontended = a.cas(v = a.value, v + x)))
            longAccumulate(x, null, uncontended);
    }
}

2. 出现竞争

当多个线程同时访问 LongAdder 时,可能会导致对 base 的 CAS 操作失败,此时会尝试使用 Cell 数组。具体步骤如下:

  • 计算线程哈希值:每个线程都有一个 ThreadLocalRandom 生成的哈希值 probe,通过 getProbe() 方法获取。
  • 定位 Cell:使用线程的哈希值对 Cell 数组的长度取模,得到要操作的 Cell 索引。
  • CAS 更新 Cell 值:尝试使用 CAS 操作对定位到的 Cell 的值进行更新。如果更新成功,则操作完成;如果失败,说明该 Cell 也存在竞争。

3. Cell 数组扩容

当多个线程同时竞争同一个 Cell 时,会触发 Cell 数组的扩容操作。扩容操作会将 Cell 数组的长度翻倍,并重新分配线程到新的 Cell 中,以减少竞争。扩容操作由 longAccumulate() 方法实现,在扩容过程中会使用 cellsBusy 标志位进行加锁,保证线程安全。

4. 获取最终结果

调用 sum() 方法时,会将 base 的值和所有 Cell 的值相加,得到最终的计数值。

public long sum() {
    Cell[] as = cells; Cell a;
    long sum = base;
    if (as != null) {
        for (int i = 0; i < as.length; ++i) {
            if ((a = as[i]) != null)
                sum += a.value;
        }
    }
    return sum;
}

LongAdder总结

LongAdder 通过将计数值分散到多个 Cell 中,减少了线程之间的竞争,提高了并发性能。在高并发场景下,LongAdder 的性能通常比 AtomicLong 要好,但 sum() 方法返回的结果可能不是绝对准确的,因为在求和过程中可能有其他线程正在对 Cell 或 base 进行更新。

二、并发容器(ConcurrentHashMap)

1. 核心设计

  • JDK7实现(分段锁)

    • 将数据分为多个段(Segment),每段独立加锁,提高并发度。
  • JDK8+实现(CAS + synchronized)

    • 使用Node数组+链表/红黑树,锁粒度细化到链表头节点。
    • CAS操作初始化数组或插入头节点,失败时使用synchronized锁住头节点。

2. 关键方法

  • putIfAbsent:不存在则插入,原子操作。
  • computeIfAbsent:惰性计算,避免重复初始化。
  • forEach:并发遍历,弱一致性(可能反映中间状态)。

3. 性能优化

  • 扩容机制

    • 多线程协同扩容,通过ForwardingNode标记迁移状态。
    • 迁移时允许读写操作并行。
  • 统计方法

    • size():通过分段计数累加,结果非精确。
    • mappingCount():返回long类型,避免溢出。

4. 使用场景

  • 高频读写:如缓存系统(Guava Cache底层实现)。
  • 避免全局锁:替代Collections.synchronizedMapHashtable

Guava Cache主要组件与数据结构

1. LocalCache 类

LocalCache 是 Guava Cache 的核心类,它继承自 AbstractMap,实现了大部分缓存操作的逻辑。LocalCache 内部使用了多个数据结构来管理缓存项,包括:

  • ConcurrentHashMap:用于存储缓存项,它是线程安全的哈希表,提供了高效的读写操作。LocalCache 利用 ConcurrentHashMap 的并发特性,确保在多线程环境下缓存的读写操作是线程安全的。
  • 双向链表:用于实现 LRU 策略。每个缓存项在链表中都有一个对应的节点,当缓存项被访问时,会将其移动到链表头部;当缓存达到最大容量时,会从链表尾部移除最近最少使用的缓存项。

2. CacheEntry 接口及其实现类

CacheEntry 表示缓存中的一个条目,它包含了键、值、访问时间、写入时间等信息。LocalCache 中有不同的 CacheEntry 实现类,如 StrongEntryWeakEntrySoftEntry 等,分别对应不同的引用类型,用于控制缓存项的生命周期。

缓存操作原理

1. 缓存写入

当调用 put(key, value) 方法向缓存中写入一个新的缓存项时,LocalCache 会执行以下步骤:

  • 计算哈希值:根据键的哈希码计算在 ConcurrentHashMap 中的索引位置。
  • 插入或更新:如果该位置没有缓存项,则创建一个新的 CacheEntry 并插入到 ConcurrentHashMap 中;如果已经存在,则更新该缓存项的值。
  • 更新链表:将新插入或更新的缓存项移动到双向链表的头部,表示它是最近使用的。
  • 检查容量和过期时间:如果缓存达到最大容量或缓存项已过期,会触发缓存回收操作。

2. 缓存读取

当调用 get(key) 方法从缓存中读取一个缓存项时,LocalCache 会执行以下步骤:

  • 计算哈希值:根据键的哈希码计算在 ConcurrentHashMap 中的索引位置。
  • 查找缓存项:在 ConcurrentHashMap 中查找对应的缓存项。
  • 检查有效性:检查缓存项是否过期或已被回收,如果是,则返回 null 或根据配置进行重新加载。
  • 更新链表:如果缓存项存在且有效,将其移动到双向链表的头部,表示它是最近使用的。

3. 缓存回收

Guava Cache 支持多种缓存回收策略,包括基于容量、基于时间和基于引用的回收:

  • 基于容量的回收:当缓存中的条目数量超过指定的最大容量时,会根据 LRU 策略从双向链表的尾部移除最近最少使用的缓存项。

  • 基于时间的回收

    • 过期时间:可以为缓存项设置过期时间,包括写入后过期(expireAfterWrite)和访问后过期(expireAfterAccess)。当缓存项的过期时间到达时,会在下次访问或定期清理时将其移除。
    • 定时清理LocalCache 会在后台线程中定期执行清理任务,检查并移除过期的缓存项。
  • 基于引用的回收:可以使用弱引用(weakKeysweakValues)或软引用(softValues)来管理缓存项,当系统内存不足时,垃圾回收器会自动回收这些引用所指向的对象。

加载机制

Guava Cache 支持自动加载缓存项,通过 CacheLoader 或 Callable 来实现。当调用 get(key) 方法时,如果缓存中不存在该键对应的缓存项,会调用 CacheLoader 或 Callable 来加载该缓存项,并将其放入缓存中。

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;

import java.util.concurrent.ExecutionException;

public class GuavaCacheExample {
    public static void main(String[] args) {
        // 创建一个 LoadingCache
        LoadingCache<String, String> cache = CacheBuilder.newBuilder()
               .maximumSize(100)
               .build(new CacheLoader<String, String>() {
                    @Override
                    public String load(String key) throws Exception {
                        // 模拟从数据源加载数据
                        return "Value for " + key;
                    }
                });

        try {
            // 从缓存中获取数据,如果不存在则自动加载
            String value = cache.get("key");
            System.out.println(value);
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

Guava Cache总结

Guava Cache 通过 LocalCache 类和相关的数据结构实现了高效的本地缓存,利用 ConcurrentHashMap 保证线程安全,使用双向链表实现 LRU 策略,支持多种缓存回收机制和自动加载功能,为开发者提供了一个方便、灵活的缓存解决方案。

5. 示例代码

ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("key", 1);
map.computeIfAbsent("key", k -> computeExpensiveValue(k)); // 线程安全的惰性计算

三、CompletableFuture

1. 核心功能

  • 异步任务编排:支持链式调用、组合多个任务。
  • 非阻塞编程:通过回调处理结果,避免线程阻塞。

2. 核心方法

方法分类方法示例说明
任务创建supplyAsync/runAsync异步执行有/无返回值的任务
链式处理thenApply/thenAccept转换结果/消费结果
组合任务thenCompose/thenCombine串行组合/并行组合任务
聚合任务allOf/anyOf等待所有/任一任务完成
异常处理exceptionally/handle捕获异常并恢复

3. 使用示例

// 异步执行任务并组合结果
CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> fetchDataFromA());
CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> fetchDataFromB());

CompletableFuture<Integer> combinedFuture = future1.thenCombine(future2, (a, b) -> a + b);
combinedFuture.thenAccept(result -> System.out.println("Total: " + result));

// 异常处理
CompletableFuture.supplyAsync(() -> {
    if (error) throw new RuntimeException("Error");
    return 42;
}).exceptionally(ex -> {
    System.out.println("Error: " + ex.getMessage());
    return 0;
});

4. 线程池管理

  • 默认线程池:使用ForkJoinPool.commonPool(),适用于轻量级任务。

  • 自定义线程池:避免阻塞公共线程池。

    ExecutorService customExecutor = Executors.newFixedThreadPool(4);
    CompletableFuture.supplyAsync(() -> task(), customExecutor);
    

5. 常见问题与解决

  • 回调地狱
    • 场景:嵌套过深的回调链难以维护。
    • 解决:使用方法链扁平化或结合反应式编程(如Reactor)。

方法链扁平化或结合反应式编程概念解释

方法链扁平化就是链式调用

反应式编程是一种面向数据流和变化传播的编程范式,它强调异步、非阻塞和事件驱动的编程模型。Reactor 是 Spring 生态系统中用于反应式编程的库,它基于 Reactive Streams 规范,提供了 Flux 和 Mono 两种类型来处理异步数据流。结合方法链扁平化和反应式编程,意味着在处理异步数据流时,通过链式调用一系列的操作符来完成数据的转换、过滤、合并等操作。

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import reactor.core.publisher.Flux;

public class MethodChainingExample {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

        // 传统方式,可能会有多个中间变量
        // List<Integer> squaredEvenNumbers = numbers.stream()
        //         .filter(n -> n % 2 == 0)
        //         .map(n -> n * n)
        //         .collect(Collectors.toList());

        // 方法链扁平化,将多个操作链式调用
        List<Integer> squaredEvenNumbers = numbers.stream()
               .filter(n -> n % 2 == 0)
               .map(n -> n * n)
               .collect(Collectors.toList());

        System.out.println(squaredEvenNumbers);
    }
}

public class ReactorExample {
    public static void main(String[] args) {
        // 创建一个包含 1 到 5 的 Flux 流
        Flux<Integer> numbers = Flux.range(1, 5);

        // 结合方法链扁平化和反应式编程
        numbers
               .filter(n -> n % 2 == 0) // 过滤出偶数
               .map(n -> n * n) // 对每个偶数进行平方
               .subscribe(System.out::println); // 订阅并打印结果
    }
}

收起

  • 线程泄漏

    • 场景:未正确关闭自定义线程池。
    • 解决:使用try-with-resources或显式调用shutdown()

四、对比与总结

组件核心优势适用场景
原子类无锁高性能,适合简单原子操作计数器、状态标志
ConcurrentHashMap高并发读写,锁粒度细缓存、高频并发数据存储
CompletableFuture灵活的任务编排与异步组合微服务调用聚合、复杂异步流程

五、最佳实践

  1. 原子类选择

    • 低竞争场景用AtomicInteger,高竞争场景用LongAdder
  2. ConcurrentHashMap优化

    • 预初始化容量,避免频繁扩容。
    • 使用computeIfAbsent替代多次getput
  3. CompletableFuture注意点

    • 避免在回调中阻塞操作。
    • 使用自定义线程池隔离关键任务。