一、原子类(Atomic Classes)
1. 核心概念
-
无锁编程:基于CAS(Compare-And-Swap)机制,避免锁竞争,提升并发性能。
-
常见原子类:
AtomicInteger
、AtomicLong
:数值原子操作。AtomicReference
:对象引用原子操作。AtomicStampedReference
:解决ABA问题的带版本号原子类。LongAdder
:高并发场景下替代AtomicLong,分段累加减少竞争。
2. CAS原理
-
操作流程:
- 读取内存值V。
- 计算新值B。
- 当且仅当内存值仍为V时,将值更新为B,否则重试。
-
底层实现:通过CPU指令(如x86的
CMPXCHG
)保证原子性。
在 x86 架构中,CMPXCHG
指令用于比较并交换操作数,它通过硬件层面的机制来保证原子性:
-
使用总线锁
- 在多处理器系统中,当一个 CPU 执行
CMPXCHG
指令时,它会在总线上发出一个锁定信号,这个信号会阻止其他 CPU 同时访问共享内存中的同一个地址。 - 例如,在一个双 CPU 系统中,当 CPU0 执行
CMPXCHG
指令来操作内存地址 0x1000 时,它会发出总线锁信号,此时 CPU1 就无法访问地址 0x1000,直到 CPU0 完成CMPXCHG
操作并释放总线锁。
- 在多处理器系统中,当一个 CPU 执行
-
利用缓存一致性协议
- x86 架构采用了诸如 MESI 等缓存一致性协议。当执行
CMPXCHG
指令时,不仅会在总线上进行操作,还会与其他 CPU 的缓存进行交互和协调。 - 例如,当 CPU0 要对一个在其缓存中处于 Modified 状态的变量执行
CMPXCHG
操作时,它会根据 MESI 协议,将缓存中的数据写回主存,并使其他 CPU 中对应的缓存行无效。在这个过程中,CMPXCHG
操作被确保是原子的,其他 CPU 在该操作完成前无法访问该变量的缓存行。
- x86 架构采用了诸如 MESI 等缓存一致性协议。当执行
-
基于指令流水线和硬件设计
- x86 处理器的指令流水线和硬件设计确保了
CMPXCHG
指令在执行过程中不会被中断或干扰。它被设计为一个不可分割的操作,从读取操作数、比较到交换,都在一个连续的硬件操作序列中完成。 - 例如,在现代的 x86 处理器中,
CMPXCHG
指令会在一个特定的执行单元中按照严格的顺序执行,在指令执行期间,处理器不会响应其他可能会干扰该操作的指令或事件,直到CMPXCHG
完成。
- x86 处理器的指令流水线和硬件设计确保了
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
的值以及基础值相加得到最终结果。
主要组成部分
Cell
数组:Cell
是一个内部类,用于存储部分计数值。多个Cell
组成一个数组,不同的线程可以操作不同的Cell
,从而避免竞争。base
变量:这是一个基础值,在没有竞争的情况下,直接对base
进行操作。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.synchronizedMap
和Hashtable
。
Guava Cache主要组件与数据结构
1. LocalCache
类
LocalCache
是 Guava Cache 的核心类,它继承自 AbstractMap
,实现了大部分缓存操作的逻辑。LocalCache
内部使用了多个数据结构来管理缓存项,包括:
ConcurrentHashMap
:用于存储缓存项,它是线程安全的哈希表,提供了高效的读写操作。LocalCache
利用ConcurrentHashMap
的并发特性,确保在多线程环境下缓存的读写操作是线程安全的。- 双向链表:用于实现 LRU 策略。每个缓存项在链表中都有一个对应的节点,当缓存项被访问时,会将其移动到链表头部;当缓存达到最大容量时,会从链表尾部移除最近最少使用的缓存项。
2. CacheEntry
接口及其实现类
CacheEntry
表示缓存中的一个条目,它包含了键、值、访问时间、写入时间等信息。LocalCache
中有不同的 CacheEntry
实现类,如 StrongEntry
、WeakEntry
、SoftEntry
等,分别对应不同的引用类型,用于控制缓存项的生命周期。
缓存操作原理
1. 缓存写入
当调用 put(key, value)
方法向缓存中写入一个新的缓存项时,LocalCache
会执行以下步骤:
- 计算哈希值:根据键的哈希码计算在
ConcurrentHashMap
中的索引位置。 - 插入或更新:如果该位置没有缓存项,则创建一个新的
CacheEntry
并插入到ConcurrentHashMap
中;如果已经存在,则更新该缓存项的值。 - 更新链表:将新插入或更新的缓存项移动到双向链表的头部,表示它是最近使用的。
- 检查容量和过期时间:如果缓存达到最大容量或缓存项已过期,会触发缓存回收操作。
2. 缓存读取
当调用 get(key)
方法从缓存中读取一个缓存项时,LocalCache
会执行以下步骤:
- 计算哈希值:根据键的哈希码计算在
ConcurrentHashMap
中的索引位置。 - 查找缓存项:在
ConcurrentHashMap
中查找对应的缓存项。 - 检查有效性:检查缓存项是否过期或已被回收,如果是,则返回
null
或根据配置进行重新加载。 - 更新链表:如果缓存项存在且有效,将其移动到双向链表的头部,表示它是最近使用的。
3. 缓存回收
Guava Cache 支持多种缓存回收策略,包括基于容量、基于时间和基于引用的回收:
-
基于容量的回收:当缓存中的条目数量超过指定的最大容量时,会根据 LRU 策略从双向链表的尾部移除最近最少使用的缓存项。
-
基于时间的回收:
- 过期时间:可以为缓存项设置过期时间,包括写入后过期(
expireAfterWrite
)和访问后过期(expireAfterAccess
)。当缓存项的过期时间到达时,会在下次访问或定期清理时将其移除。 - 定时清理:
LocalCache
会在后台线程中定期执行清理任务,检查并移除过期的缓存项。
- 过期时间:可以为缓存项设置过期时间,包括写入后过期(
-
基于引用的回收:可以使用弱引用(
weakKeys
、weakValues
)或软引用(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 | 灵活的任务编排与异步组合 | 微服务调用聚合、复杂异步流程 |
五、最佳实践
-
原子类选择:
- 低竞争场景用
AtomicInteger
,高竞争场景用LongAdder
。
- 低竞争场景用
-
ConcurrentHashMap优化:
- 预初始化容量,避免频繁扩容。
- 使用
computeIfAbsent
替代多次get
和put
。
-
CompletableFuture注意点:
- 避免在回调中阻塞操作。
- 使用自定义线程池隔离关键任务。