并发容器
1. JUC包下的并发容器
Java 的集合容器框架中,主要有四大类别:List、Set、Map、Queue。大家熟知的这些集合类,如 ArrayList、LinkedList、HashMap,它们都是非线程安全的。
所以,Java 先提供了同步容器供用户使用。同步容器可以简单地理解为通过 synchronized 来实现同步的容器,比如 Vector、Hashtable 以及 SynchronizedList 等容器。这样做的代价是削弱了并发性,当多个线程共同竞争容器级的锁时,吞吐量就会降低。
因此,为了解决同步容器的性能问题,才有了并发容器。java.util.concurrent 包中提供了多种并发类容器。
并发容器
├── List
│ └── CopyOnWriteArrayList
├── Set
│ ├── CopyOnWriteArraySet
│ └── ConcurrentSkipListSet
├── Map
│ ├── ConcurrentHashMap
│ └── ConcurrentSkipListMap
└── Queue
├── BlockingQueue
│ ├── ArrayBlockingQueue
│ ├── LinkedBlockingQueue
│ ├── SynchronousQueue
│ ├── PriorityBlockingQueue
│ ├── DelayQueue
│ └── LinkedTransferQueue
├── ConcurrentLinkedQueue
├── ConcurrentLinkedDeque
└── BlockingDeque
└── LinkedBlockingDeque2. CopyOnWriteArrayList
CopyOnWriteArrayList 是 Java 中的一种线程安全的 List,它是一个可变的数组,支持并发读和写。
- 对应的非并发容器:ArrayList
- 目标:代替 Vector、SynchronizedList
- 原理简述:利用高并发往往是读多写少的特性,对读操作不加锁;对写操作,先复制一份新的集合,在新的集合上面修改,然后将新集合赋值给旧的引用,并通过
volatile保证其可见性。当然,写操作的锁是必不可少的。它通过在修改操作时创建底层数组的副本来实现线程安全,从而保证了并发访问的一致性。
2.1 应用场景
CopyOnWriteArrayList 的应用场景主要有两个方面:
读多写少的场景
由于 CopyOnWriteArrayList 的读操作不需要加锁,因此它非常适合在读多写少的场景中使用。例如,一个读取频率比写入频率高得多的缓存,使用 CopyOnWriteArrayList 可以提高读取性能,并减少锁竞争的开销。
不需要实时更新的数据
由于 CopyOnWriteArrayList 读取的数据可能不是最新的,因此它适合于不需要实时更新的数据。例如,在日志应用中,为了保证应用的性能,日志记录的操作可能被缓冲,并不是实时写入文件系统,而是在某个时刻批量写入。这种情况下,使用 CopyOnWriteArrayList 可以避免多个线程之间的竞争,提高应用的性能。
2.2 基本使用
和 ArrayList 在使用方式方面很类似。
// 创建一个 CopyOnWriteArrayList 对象
CopyOnWriteArrayList phaser = new CopyOnWriteArrayList();
// 新增
copyOnWriteArrayList.add(1);
// 设置(指定下标)
copyOnWriteArrayList.set(0, 2);
// 获取(查询)
copyOnWriteArrayList.get(0);
// 删除
copyOnWriteArrayList.remove(0);
// 清空
copyOnWriteArrayList.clear();
// 是否为空
copyOnWriteArrayList.isEmpty();
// 是否包含
copyOnWriteArrayList.contains(1);
// 获取元素个数
copyOnWriteArrayList.size();IP 黑名单判定
当应用接入外部请求后,为了防范风险,一般会对请求做一些特征判定,如对请求 IP 是否合法的判定就是一种。IP 黑名单偶尔会被系统运维人员做更新。
import lombok.extern.slf4j.Slf4j;
import java.util.Random;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;
@Slf4j
public class CopyOnWriteArrayListDemo {
private static CopyOnWriteArrayList<String> copyOnWriteArrayList = new CopyOnWriteArrayList<>();
// 模拟初始化的黑名单数据
static {
copyOnWriteArrayList.add("ipAddr0");
copyOnWriteArrayList.add("ipAddr1");
copyOnWriteArrayList.add("ipAddr2");
}
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> {
// 模拟接入用时
try {
TimeUnit.SECONDS.sleep(new Random().nextInt(5));
} catch (Exception e) {
log.error(e.getMessage(), e);
}
String currentIP = "ipAddr" + new Random().nextInt(6);
if (copyOnWriteArrayList.contains(currentIP)) {
System.out.println(Thread.currentThread().getName() + " IP " + currentIP + "命中黑名单,拒绝接入处理");
return;
}
System.out.println(Thread.currentThread().getName() + " IP " + currentIP + "接入处理...");
};
new Thread(task, "请求1").start();
new Thread(task, "请求2").start();
new Thread(task, "请求3").start();
new Thread(() -> {
// 模拟用时
try {
TimeUnit.SECONDS.sleep(new Random().nextInt(2));
} catch (Exception e) {
log.error(e.getMessage(), e);
}
String newBlackIP = "ipAddr3";
copyOnWriteArrayList.add(newBlackIP);
System.out.println(Thread.currentThread().getName() + " 添加了新的非法IP " + newBlackIP);
}, "IP黑名单更新").start();
Thread.sleep(1000000);
}
}运行结果:
请求3 IP ipAddr0命中黑名单,拒绝接入处理
IP黑名单更新 添加了新的非法IP ipAddr3
请求2 IP ipAddr0命中黑名单,拒绝接入处理
请求1 IP ipAddr1命中黑名单,拒绝接入处理2.3 原理
很多时候,我们的系统应对的都是读多写少的并发场景。CopyOnWriteArrayList 容器允许并发读,读操作是无锁的,性能较高。至于写操作,比如向容器中添加一个元素,则首先将当前容器复制一份,然后在新副本上执行写操作,结束之后再将原容器的引用指向新容器。
- 它是线程安全的,多线程环境下可以直接使用,无需加锁;
- 通过 锁 + 数组拷贝 +
volatile关键字保证了线程安全; - 每次数组操作,都会把数组拷贝一份出来,在新数组上进行操作,操作成功之后再赋值回去。

从整体架构上来说,CopyOnWriteArrayList 数据结构和 ArrayList 一致的,底层是个数组,只不过 CopyOnWriteArrayList 在对数组进行操作的时候,基本会分四步走:
- 加锁;
- 从原数组中拷贝出新数组;
- 在新数组上进行操作,并把新数组赋值给数组容器;
- 解锁。
除了加锁之外,CopyOnWriteArrayList 的底层数组还被 volatile 关键字修饰,意思是一旦数组被修改,其它线程立马能够感知到,代码如下:
private transient volatile Object[] array;整体上来说,CopyOnWriteArrayList 就是利用 锁 + 数组拷贝 + volatile 关键字保证了 List 的线程安全。
优点
读操作(不加锁)性能很高,因为无需任何同步措施,比较适用于读多写少的并发场景。Java 的 List 在遍历时,若中途有别的线程对 List 容器进行修改,则会抛出 ConcurrentModificationException 异常。而 CopyOnWriteArrayList 由于其“读写分离”的思想,遍历和修改操作分别作用在不同的 List 容器上,所以在使用迭代器进行遍历时候,也就不会抛出 ConcurrentModificationException 异常了。
缺点
- 内存占用问题:毕竟每次执行写操作都要将原容器拷贝一份。数据量大时,对内存压力较大,可能会引起频繁 GC;
- 无法保证实时性:因为 CopyOnWrite 的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存:旧的对象和新写入的对象(注意:在复制的时候只是复制容器里的引用,只是在写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存)。
2.4 扩展知识:迭代器的 fail-fast 与 fail-safe 机制
在 Java 中,迭代器(Iterator)在迭代的过程中,如果底层的集合被修改(添加或删除元素),不同的迭代器对此的表现行为是不一样的,可分为两类:Fail-Fast(快速失败)和 Fail-Safe(安全失败)。
fail-fast 机制
fail-fast 机制是 Java 集合(Collection)中的一种错误机制。当多个线程对同一个集合的内容进行操作时,就可能会产生 fail-fast 事件。例如:当某一个线程 A 通过 iterator 去遍历某集合的过程中,若该集合的内容被其他线程所改变了;那么线程 A 访问集合时,就会抛出 ConcurrentModificationException 异常,产生 fail-fast 事件。
在 java.util 包中的集合,如 ArrayList、HashMap 等,它们的迭代器默认都是采用 Fail-Fast 机制。
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
public class FailFastDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String val = iterator.next();
System.out.println("读取元素: " + val);
// 模拟在迭代过程中修改集合
if ("A".equals(val)) {
list.add("D"); // 这里的修改会触发 Fail-Fast
}
}
}
}运行结果:
读取元素: A
Exception in thread "main" java.util.ConcurrentModificationException
at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1095)
at java.base/java.util.ArrayList$Itr.next(ArrayList.java:1049)
at com.juzicoding.juc.container.list.FailFastDemo.main(FailFastDemo.java:14)fail-fast解决方案
- 方案一:在遍历过程中,所有涉及到改变
modCount值的地方全部加上synchronized,或者直接使用Collection#synchronizedList。这样就可以解决问题,但是不推荐,因为增删造成的同步锁可能会阻塞遍历操作。 - 方案二:使用 CopyOnWriteArrayList 替换 ArrayList,推荐使用该方案(即 fail-safe)。
fail-safe机制
任何对集合结构的修改都会在一个复制的集合上进行,因此不会抛出 ConcurrentModificationException。在 java.util.concurrent 包中的集合,如 CopyOnWriteArrayList、ConcurrentHashMap 等,它们的迭代器一般都是采用 Fail-Safe 机制。
缺点:
采用 Fail-Safe 机制的集合类都是线程安全的,但是它们无法保证数据的实时一致性,它们只能保证数据的最终一致性。在迭代过程中,如果集合被修改了,可能读取到的仍然是旧的数据。
Fail-Safe 机制还存在另外一个问题,就是内存占用。由于这类集合一般都是通过复制来实现读写分离的,因此它们会创建出更多的对象,导致占用更多的内存,甚至可能引起频繁的垃圾回收,严重影响性能。
| 特性 | Fail-Fast (ArrayList) | Fail-Safe (CopyOnWriteArrayList) |
|---|---|---|
| 修改检查 | 检查 modCount | 不检查,操作副本 |
| 异常表现 | 抛出 ConcurrentModificationException | 不抛出异常 |
| 数据一致性 | 强一致性(不允许并发修改) | 弱一致性(迭代时可能读到旧数据) |
| 内存开销 | 低 | 高(每次写操作都要复制数组) |
3. CopyOnWriteArraySet
CopyOnWriteArraySet 是 Java 中的一种线程安全的 Set,它是基于 CopyOnWriteArrayList 实现的。
- 对应的非并发容器:HashSet
- 目标:代替 SynchronizedSet、Collections.synchronizedSet
- 原理简述:内部持有一个 CopyOnWriteArrayList 实例,利用其“写时复制”的特性来保证线程安全。其唯一的不同是在
add时调用的是 CopyOnWriteArrayList 的addIfAbsent方法,从而确保集合中元素的唯一性。
3.1 应用场景
CopyOnWriteArraySet 适用于以下场景:
- 读多写少且集合规模较小:由于每次写操作都需要复制底层数组,因此它只适合数据量较小且写入频率较低的场景。
- 监听器/观察者列表:在发布-订阅模式或观察者模式中,存储监听器的集合通常很少变动,但会被频繁遍历执行回调,这正是 CopyOnWrite 机制的用武之地。
3.2 基本使用
CopyOnWriteArraySet 的 API 与 HashSet 基本一致。
// 创建一个 CopyOnWriteArraySet 对象
CopyOnWriteArraySet<String> set = new CopyOnWriteArraySet<>();
// 添加元素(会自动去重)
set.add("Java");
set.add("Python");
set.add("Java"); // 重复元素不会被再次添加
// 判定是否包含
set.contains("Java");
// 删除元素
set.remove("Python");
// 获取元素个数
set.size();
// 迭代器遍历(fail-safe,不会抛出 ConcurrentModificationException)
for (String item : set) {
System.out.println(item);
}3.3 原理
CopyOnWriteArraySet 内部组合了一个 CopyOnWriteArrayList 对象,它的所有操作基本上都是直接委托给这个 list 来实现的。
public class CopyOnWriteArraySet<E> extends AbstractSet<E> implements java.io.Serializable {
private final CopyOnWriteArrayList<E> al;
public CopyOnWriteArraySet() {
al = new CopyOnWriteArrayList<E>();
}
public boolean add(E e) {
return al.addIfAbsent(e);
}
// ... 其他方法也多是委托给 al 实现
}在执行 add(E e) 时,它会调用 CopyOnWriteArrayList 的 addIfAbsent 方法。该方法会先检查元素是否已经存在(通过遍历内部数组),如果不存在,则在加锁的情况下拷贝一份新数组,并在新数组末尾添加元素,最后更新数组引用。
这种实现方式虽然保证了 Set 的唯一性,但由于 addIfAbsent 需要遍历整个数组来去重,所以其写操作的时间复杂度为 $O(n)$,在数据量较大时性能会明显下降。
4. ConcurrentHashMap
ConcurrentHashMap 是 Java 中线程安全的哈希表,它支持高并发并且能够同时进行读写操作。
- 对应的非并发容器:HashMap
- 目标:代替 Hashtable、SynchronizedMap,支持高性能复合操作
- 原理:JDK1.7 采用 Segment“分段锁”机制;JDK1.8 舍弃了分段锁,使用 自旋 + CAS +
synchronized关键字来实现同步。
在 JDK1.8 的官方解释中:一是节省内存空间;二是分段锁需要更多的内存空间,而大多数情况下,并发粒度达不到设置的粒度,竞争概率较小,反而导致更新的长时间等待(因为锁定一段后整个段就无法更新了);三是提高 GC 效率。
4.1 应用场景
ConcurrentHashMap 的应用场景包括但不限于以下几种:
- 共享数据的线程安全:在多线程编程中,如果需要进行共享数据的读写,可以使用 ConcurrentHashMap 保证线程安全。
- 缓存:ConcurrentHashMap 的高并发性能和线程安全能力,使其成为一种很好的缓存实现方案。在多线程环境下,使用 ConcurrentHashMap 作为缓存的数据结构,能够提高程序的并发性能,同时保证数据的一致性。
4.2 ConcurrentHashMap使用
基本用法
// 创建一个 ConcurrentHashMap 对象
ConcurrentHashMap<Object, Object> concurrentHashMap = new ConcurrentHashMap<>();
// 添加键值对
concurrentHashMap.put("key", "value");
// 添加一批键值对
concurrentHashMap.putAll(new HashMap());
// 使用指定的键获取值
concurrentHashMap.get("key");
// 判定是否为空
concurrentHashMap.isEmpty();
// 获取已经添加的键值对个数
concurrentHashMap.size();
// 获取已经添加的所有键的集合
concurrentHashMap.keys();
// 获取已经添加的所有值的集合
concurrentHashMap.values();
// 清空
concurrentHashMap.clear();其他方法:
V putIfAbsent(K key, V value):如果 key 已存在,则不进行插入并返回原有值;如果 key 不存在,则插入新值并返回 null。boolean remove(Object key, Object value):如果 key 对应的值是 value,则移除 K-V,返回 true。否则不移除,返回 false。boolean replace(K key, V oldValue, V newValue):如果 key 对应的当前值是 oldValue,则替换为 newValue,返回 true。否则不替换,返回 false。computeIfAbsent(key, Function):如果存在则返回 key 的值。如果不存在,则 Function 返回值作为 key 的值。merge(key, value, BiFunction):不存在指定的 key 时,将 value 设置为 key 的值。当 key 存在值时,执行 BiFunction 接收 oldKey 和 value,返回结果设置为 key 的值。
统计文件中英文字母出现的总次数
将 26 个英文字母分别循环 200 次,每个字母作为一个单词,一共有 5200 个单词。每个单词中间用 \n 分隔,乱序存入 26 个文件中。生成 26 个线程对 26 个文件中的单词进行计数,存入 map 中。
import java.io.*;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.LongAdder;
import java.util.function.BiConsumer;
import java.util.function.Supplier;
import java.util.stream.Collectors;
/**
* 统计文件中英文字母出现的总次数 - 完整示例代码
*/
public class WordCountDemo {
public static void main(String[] args) throws IOException {
WordCountDemo demo = new WordCountDemo();
// 1. 生成测试数据文件 (1.txt - 26.txt)
demo.produceData();
// 2. 使用 ConcurrentHashMap + LongAdder 进行高效并发统计
System.out.println("开始并发统计 (实现方式1: LongAdder)...");
demo.deal(
() -> new ConcurrentHashMap<String, LongAdder>(),
(map, list) -> {
list.forEach(str -> {
map.computeIfAbsent(str, (key) -> new LongAdder()).increment();
});
}
);
// 3. 使用 ConcurrentHashMap + merge 进行并发统计
System.out.println("\n开始并发统计 (实现方式2: merge)...");
demo.deal(
() -> new ConcurrentHashMap<String, Integer>(),
(map, list) -> {
list.forEach(str -> {
map.merge(str, 1, Integer::sum);
});
}
);
// 4. 清理测试文件
demo.cleanFiles();
}
/**
* 生成测试文件:26个文件,每个文件包含从5200个乱序字母中抽取的200个
*/
public void produceData() throws IOException {
String data = "abcdefghijklmnopqrstuvwxyz";
List<String> list = new ArrayList<>();
for (int i = 0; i < data.length(); i++) {
for (int j = 0; j < 200; j++) {
list.add(String.valueOf(data.charAt(i)));
}
}
Collections.shuffle(list);
for (int i = 0; i < 26; i++) {
try (FileWriter fw = new FileWriter((i + 1) + ".txt")) {
fw.write(list.subList(i * 200, (i + 1) * 200).stream().collect(Collectors.joining("\n")));
}
}
}
/**
* 核心处理逻辑:定义26个线程读取26个文件
*/
public <T> void deal(Supplier<Map<String, T>> supplier, BiConsumer<Map<String, T>, List<String>> consumer) {
Map<String, T> map = supplier.get();
CountDownLatch count = new CountDownLatch(26);
for (int i = 0; i < 26; i++) {
int j = i;
new Thread(() -> {
List<String> list = new ArrayList<>();
read(list, j);
consumer.accept(map, list);
count.countDown();
}).start();
}
try {
count.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("统计结果: " + new TreeMap<>(map));
}
/**
* 读取指定索引的文件内容
*/
private static void read(List<String> list, int i) {
try (BufferedReader bf = new BufferedReader(new FileReader((i + 1) + ".txt"))) {
String data;
while ((data = bf.readLine()) != null) {
list.add(data);
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 清理生成的临时文件
*/
public void cleanFiles() {
for (int i = 1; i <= 26; i++) {
File file = new File(i + ".txt");
if (file.exists()) {
file.delete();
}
}
System.out.println("\n临时测试文件已清理完毕。");
}
}正确结果输出应该是每个字母出现 200 次:
开始并发统计 (实现方式1: LongAdder)...
统计结果: {a=200, b=200, c=200, d=200, e=200, f=200, g=200, h=200, i=200, j=200, k=200, l=200, m=200, n=200, o=200, p=200, q=200, r=200, s=200, t=200, u=200, v=200, w=200, x=200, y=200, z=200}
开始并发统计 (实现方式2: merge)...
统计结果: {a=200, b=200, c=200, d=200, e=200, f=200, g=200, h=200, i=200, j=200, k=200, l=200, m=200, n=200, o=200, p=200, q=200, r=200, s=200, t=200, u=200, v=200, w=200, x=200, y=200, z=200}
临时测试文件已清理完毕。4.3 数据结构
HashTable的数据结构
$$图片 HashTable数据结构原理示意图$$
JDK1.7 中的ConcurrentHashMap
在 JDK1.7 中,结构是用 Segments 数组 + HashEntry 数组 + 链表实现的(写分散的思想)。ConcurrentHashMap 内部维护了一个 Segment 数组。每个 Segment 继承自 ReentrantLock 并且它内部本质上是一个 Hash 表。这样做的好处是能够减小锁的粒度,提高并发访问的效率。默认 Segment 数量为 16,可以通过构造函数来修改默认值。当需要 put 或 get 一个元素时,线程首先通过 hash 定位到具体的 Segment,然后在对应的 Segment 上进行锁定操作。

JDK1.8中的ConcurrentHashMap
JDK1.8 抛弃了 Segments 分段锁的方案,而是改用了和 HashMap 一样的结构操作,也就是 数组 + 链表 + 红黑树结构,比 JDK1.7 中的 ConcurrentHashMap 提高了效率。在并发方面,使用了 CAS + synchronized 的方式保证数据的一致性。

链表转化为红黑树需要满足 2 个条件:
- 链表的节点数量大于等于树化阈值 8
- Node 数组的长度大于等于最小树化容量值 64
// 树化阈值为8
static final int TREEIFY_THRESHOLD = 8;
// 最小树化容量值为64
static final int MIN_TREEIFY_CAPACITY = 64;4.4 ConcurrentHashMap源码分析
5. ConcurrentSkipListMap
ConcurrentSkipListMap 是 Java 中的一种线程安全、基于跳表实现的有序映射(Map)数据结构。它是对 TreeMap 的并发实现,支持高并发读写操作。
- 对应的非并发容器:TreeMap
- 目标:代替 SynchronizedSortedMap(TreeMap)
- 原理:Skip list(跳表)是一种可以代替平衡树的数据结构,默认是按照 Key 值升序的。
ConcurrentSkipListMap 适用于需要高并发性能、支持有序性和区间查询的场景,能够有效地提高系统的性能和可扩展性。
5.1 跳表
跳表是一种基于有序链表的数据结构,支持快速插入、删除、查找操作,其时间复杂度为 $O(\log n)$,比普通链表的 $O(n)$ 更高效。
可视化操作工具:https://cmps-people.ok.ubc.ca/ylucet/DS/SkipList.html

跳表的特性有这么几点:
- 一个跳表结构由很多层数据结构组成。
- 每一层都是一个有序的链表,默认是升序。也可以自定义排序方法。
- 最底层链表(图中所示 Level1)包含了所有的元素。
- 如果每一个元素出现在 LevelN 的链表中(N>1),那么这个元素必定在下层链表出现。
- 每一个节点都包含了两个指针,一个指向同一级链表中的下一个元素,一个指向下一层级别链表中的相同值元素。
跳表的查找

跳表的插入
跳表插入数据的流程如下:
- 找到元素适合的插入层级 K,这里的 K 采用随机的方式。若 K 大于跳表的总层级,那么开辟新的一层,否则在对应的层级插入。
- 申请新的节点。
- 调整对应的指针。
假设我要插入元素 13,原有的层级是 3 级,假设 K = 4:

倘若 K = 2:

5.2 ConcurrentSkipListMap使用
基本用法
public class ConcurrentSkipListMapDemo {
public static void main(String[] args) {
ConcurrentSkipListMap<Integer, String> map = new ConcurrentSkipListMap<>();
// 添加元素
map.put(1, "a");
map.put(3, "c");
map.put(2, "b");
map.put(4, "d");
// 获取元素
String value1 = map.get(2);
System.out.println(value1); // 输出:b
// 遍历元素
for (Integer key : map.keySet()) {
String value = map.get(key);
System.out.println(key + " : " + value);
}
// 删除元素
String value2 = map.remove(3);
System.out.println(value2); // 输出:c
}
}6. 电商场景中并发容器的选择
在电商高并发环境下,容器的选型不仅仅是为了“线程安全”,更是在开发效率、执行性能、内存开销之间寻找最优平衡点。
场景一:活动商品销量的实时统计
- 业务描述:记录促销活动中各商品的实时售卖件数。
- 技术挑战:商品 ID 数量相对固定,但每个 ID 对应的销量(Value)会被成千上万个下单线程高频更新。
- 选型演进:
- HashMap (弃用):虽然速度最快,但在高并发扩容时会导致 CPU 飙升(死循环风险)或数据丢失。
- Hashtable (弃用):使用全局重量级锁,同一时刻只能有一个线程操作,在高并发下单时会成为性能瓶颈。
- ConcurrentHashMap (推荐):采用分段锁(或桶级锁)技术,支持多线程并发更新不同商品的销量。它是绝大多数统计场景下的性能首选。
场景二:海量用户浏览历史与行为排序
- 业务描述:为每个用户实时记录浏览历史,并可能涉及按频率或时间进行排序。
- 技术挑战:用户量级巨大,且浏览数据处于极高频率的动态增删状态。
- 选型演进:
- ConcurrentHashMap (备选):对于纯 KV 存取表现优异,但在数据量极大、频繁触发红黑树重平衡(Rebalance)时,其内部锁竞争的代价会逐渐升高。
- ConcurrentSkipListMap (推荐):基于“跳表”实现。在高并发增删场景下,跳表的局部锁定范围比红黑树更小,性能表现更加稳定;同时,跳表天然有序,非常适合“查询最近 10 次浏览”等区间操作。
场景三:营销黑名单与灰度配置(读多写少)
- 业务描述:维护一份被封禁的用户 ID 列表。该列表由运营人员偶尔更新(低频写),但每个下单请求都要实时比对(极高频读)。
- 技术挑战:读取操作必须绝对流畅,不能因为极个别的更新操作而产生明显的响应毛刺。
- 选型演进:
- Vector/SynchronizedList (弃用):读写共用一把锁,写操作会阻塞所有的读操作,并发性能低下。
- CopyOnWriteArrayList (推荐):利用“写时复制”思想。
- 优势:读取操作完全无锁,性能等同于原生数组,极大地提升了系统的吞吐量。
- 权衡:每次更新都会全量复制一份新数组,因此仅适用于数据量较小(如 < 2000 条)且写操作极少的场景。