1. 并发容器的历史
大家好,今天我们来聊一个 Java 多线程开发中绕不开的核心话题:并发容器。可能你已经发现,当我们在多线程环境中使用 HashMap、ArrayList 这些集合类时,经常会遇到ConcurrentModificationException
或数据不一致的问题,这就是因为这些普通集合类不是线程安全的。
JDK 提供的传统解决方案是Collections.synchronizedXxx()
方法,比如:
Map<String, String> synchronizedMap = Collections.synchronizedMap(new HashMap<>());
List<String> synchronizedList = Collections.synchronizedList(new ArrayList<>());
但这种方式有一个致命问题:它使用的是"一刀切"的粗粒度同步策略,导致多个线程竞争同一把锁,性能非常低。随着 Java 5 引入的 java.util.concurrent 包(简称 JUC),我们有了更高效的并发容器选择。
下面我们围绕三个核心问题展开:
- 并发容器相比传统同步容器有哪些优势?
- 各种并发容器的内部实现原理是怎样的?
- 如何在实际项目中正确选择并高效使用这些容器?
2. ConcurrentHashMap 深度解析
2.1 进化历程:从分段锁到 CAS+红黑树
ConcurrentHashMap 是并发编程中使用最广泛的 Map 实现,它的设计经历了重大变革:
- JDK 1.7 版本:采用分段锁(Segment)机制,将数据分成 16 个段,每段一把锁,本质上是可重入锁(ReentrantLock)
- JDK 1.8 版本:抛弃了 Segment 设计,改用 CAS+synchronized+红黑树的设计
让我们通过一个图解来理解这两个版本的区别:
链表到红黑树的转换机制
JDK 1.8 引入了一个重要的优化:当链表长度超过特定阈值时,会将链表转换为红黑树,显著提升查找性能。
- 链表转红黑树阈值:默认为 8,当链表节点数超过 8 时转为红黑树
- 红黑树退化为链表阈值:默认为 6,当节点数小于 6 时转回链表
- 为什么选择 8 作为阈值?根据泊松分布统计,链表长度超过 8 的概率不到千万分之一,这是一种时间和空间的权衡,避免大多数情况下不必要的红黑树复杂度
2.2 源码解析:put 和 get 操作的实现
让我们看看 JDK 1.8 中 ConcurrentHashMap 的核心方法实现(简化版):
put 操作的关键步骤:
final V putVal(K key, V value, boolean onlyIfAbsent) {
// 不允许空键值
if (key == null || value == null) throw new NullPointerException();
// 计算hash值
int hash = spread(key.hashCode());
int binCount = 0;
// 无限循环,确保CAS操作成功
for (Node<K,V>[] tab = table;;) {
// 省略一些初始化代码...
// 如果对应桶为空,通过CAS操作创建新节点
if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
break; // 成功则跳出循环
}
// 省略其他逻辑...
else {
// 桶不为空,锁定当前桶的首节点
synchronized (f) {
// 再次检查(双重检查锁)
if (tabAt(tab, i) == f) {
// 遍历链表或红黑树,更新/添加节点
// 如果节点数超过TREEIFY_THRESHOLD (8),则转换为红黑树
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
}
}
}
}
return null;
}
这里有几个关键点:
- 使用 CAS 操作尝试插入新节点
- 只有发生哈希冲突时才使用 synchronized 锁定桶
- 当链表长度超过阈值时转换为红黑树(TREEIFY_THRESHOLD = 8)
get 操作的关键步骤:
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
// 计算hash值
int h = spread(key.hashCode());
// 如果表不为空且对应位置有节点
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
// 检查首节点
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
// 特殊节点处理...
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
// 遍历链表或红黑树
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
get 操作的关键在于:完全不需要加锁!这是因为:
- Node 的 value 和 next 指针使用了 volatile 修饰,保证了内存可见性
- Node 节点的不可变性设计 - key 和 hash 值在创建后不可修改,value 只能通过原子操作更新
- JDK 1.8 中 TreeNode(红黑树节点)的 left 和 right 引用也使用了 volatile 修饰,保证树结构变化对所有线程可见
这种设计确保了在不加锁的情况下,读操作也能看到最新的值,大大提升了读性能。
2.3 性能对比实验
我们来看一个简单的性能对比实验,分别测试 HashMap(单线程)、synchronizedMap 和 ConcurrentHashMap 在多线程环境下的性能:
public class MapPerformanceTest {
private static final int THREAD_COUNT = 100;
private static final int ITEM_COUNT = 1000;
public static void main(String[] args) throws Exception {
// 测试HashMap (仅作为对照,实际多线程中会出错)
testMap(new HashMap<>(), "HashMap");
// 测试synchronizedMap
testMap(Collections.synchronizedMap(new HashMap<>()), "synchronizedMap");
// 测试ConcurrentHashMap
testMap(new ConcurrentHashMap<>(), "ConcurrentHashMap");
}
private static void testMap(Map<String, Integer> map, String mapName) throws Exception {
System.out.println("Test: " + mapName);
CountDownLatch latch = new CountDownLatch(THREAD_COUNT);
long startTime = System.nanoTime();
for (int i = 0; i < THREAD_COUNT; i++) {
final int threadNum = i;
new Thread(() -> {
try {
for (int j = 0; j < ITEM_COUNT; j++) {
// 执行put操作
map.put(threadNum + "-" + j, j);
}
} catch (Exception e) {
// HashMap在多线程下可能抛出异常
System.err.println(mapName + " 发生异常: " + e.getMessage());
// 注意:即使没有异常,HashMap在多线程下结果也可能不正确!
} finally {
latch.countDown();
}
}).start();
}
latch.await();
long endTime = System.nanoTime();
System.out.println(mapName + " 耗时: " + (endTime - startTime) / 1000000 + "ms");
System.out.println("Map大小: " + map.size() + " (HashMap结果不可靠,仅作对照)");
System.out.println();
}
}
典型的测试结果(仅供参考,具体会因环境而异):
- HashMap: 约 80ms(但结果不可靠,且可能抛出异常)
- synchronizedMap: 约 450ms
- ConcurrentHashMap: 约 150ms
2.4 实战应用:高并发缓存实现
让我们看一个使用 ConcurrentHashMap 实现的简单高并发缓存,并优化清理过期数据的逻辑:
public class SimpleConcurrentCache<K, V> {
// 使用ConcurrentHashMap存储缓存数据
private final ConcurrentHashMap<K, V> cache = new ConcurrentHashMap<>();
// 缓存过期时间(毫秒)
private final long expireTime;
// 存储键的过期时间
private final ConcurrentHashMap<K, Long> expireMap = new ConcurrentHashMap<>();
// 清理线程
private final ScheduledExecutorService cleanerExecutor;
public SimpleConcurrentCache(long expireTimeMillis) {
this.expireTime = expireTimeMillis;
this.cleanerExecutor = Executors.newSingleThreadScheduledExecutor(r -> {
Thread t = new Thread(r, "cache-cleaner");
t.setDaemon(true); // 设置为守护线程
return t;
});
// 定期清理过期数据
this.cleanerExecutor.scheduleAtFixedRate(
this::cleanExpiredEntries,
expireTime / 2,
expireTime / 2,
TimeUnit.MILLISECONDS
);
}
public void put(K key, V value) {
cache.put(key, value);
expireMap.put(key, System.currentTimeMillis() + expireTime);
}
public V get(K key) {
Long expireTime = expireMap.get(key);
if (expireTime == null) {
return null;
}
// 检查是否过期
if (System.currentTimeMillis() > expireTime) {
// 原子性移除过期数据
cache.remove(key);
expireMap.remove(key);
return null;
}
return cache.get(key);
}
// 优化的清理过期条目方法
private void cleanExpiredEntries() {
long now = System.currentTimeMillis();
// 使用迭代器安全地删除过期条目
Iterator<Map.Entry<K, Long>> iterator = expireMap.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<K, Long> entry = iterator.next();
if (now > entry.getValue()) {
K key = entry.getKey();
cache.remove(key);
// 使用Iterator的remove方法安全删除当前元素
iterator.remove();
}
}
}
public void shutdown() {
cleanerExecutor.shutdown();
}
}
这个实现的优点:
- 利用 ConcurrentHashMap 的高并发特性支持多线程访问
- 无需加锁即可进行读写操作
- 使用迭代器安全地删除过期数据,避免 ConcurrentModificationException
- 使用守护线程处理清理工作,避免阻止 JVM 退出
3. CopyOnWriteArrayList 深度解析
3.1 写时复制机制详解
CopyOnWriteArrayList 是 ArrayList 的线程安全版本,它使用了一种叫"写时复制"的策略:
核心原理是:
- 读操作不需要加锁,直接访问内部数组
- 写操作加锁,并复制整个数组,在副本上修改完成后替换原数组
需要特别注意的是,CopyOnWriteArrayList 提供的是最终一致性而非强一致性:
- 当线程 A 正在进行写操作时,线程 B 的读操作看到的是修改前的数据
- 只有当写操作完成(新数组引用替换旧数组引用)后,新的读操作才能看到更新后的数据
- 这意味着读线程可能会短暂地看到"旧"数据,适用于对实时性要求不高的场景
3.2 源码分析
让我们看看 CopyOnWriteArrayList 的关键方法实现:
// 添加元素
public boolean add(E e) {
// 获取独占锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 获取当前数组
Object[] elements = getArray();
int len = elements.length;
// 创建新数组,长度+1
Object[] newElements = Arrays.copyOf(elements, len + 1);
// 在新数组末尾添加元素
newElements[len] = e;
// 更新数组引用
setArray(newElements);
return true;
} finally {
// 释放锁
lock.unlock();
}
}
// 获取元素
public E get(int index) {
// 直接访问数组,无需加锁
return get(getArray(), index);
}
3.3 伪共享问题与性能影响
在 CopyOnWriteArrayList 中,伪共享问题可能严重影响性能。伪共享是指多个线程修改位于同一缓存行但不同的变量,导致缓存行失效的问题。
举个例子,假设两个线程分别修改数组中相邻的元素:
// 线程1修改索引0的元素
list.set(0, newValue1);
// 线程2修改索引1的元素
list.set(1, newValue2);
如果这两个元素恰好位于同一个 CPU 缓存行(通常为 64 字节),那么当一个线程修改其中一个元素时,会导致整个缓存行失效,另一个线程必须从主内存重新加载数据,即使它实际上修改的是不同的元素。
JDK 9 引入了@Contended
注解来解决这个问题,通过填充字节使变量不共享缓存行:
class PaddedElement {
@Contended
volatile Object value;
// ...其他字段
}
3.4 适用场景与性能特性
CopyOnWriteArrayList 适用于以下场景:
- 读操作远多于写操作的场景(读写比例通常应该在 10:1 以上)
- 集合规模较小(通常少于 1000 个元素)
- 对实时性要求不高的场景,能容忍短暂的数据不一致
内存开销考虑:
- 每次写操作都会创建一个新数组,导致大量临时对象产生
- 集合元素越多,内存开销越大,可能增加 GC 压力
- 写操作频繁时会导致性能急剧下降
让我们通过一个简单的实验来比较 CopyOnWriteArrayList 在不同读写比例下的性能表现:
public class ListPerformanceTest {
private static final int THREAD_COUNT = 10;
private static final int INITIAL_SIZE = 1000;
// 测试不同读写比例
public static void main(String[] args) throws Exception {
// 读多写少 (10:1)
testWithRatio(10, 1, "读多写少(10:1)");
// 读写均衡 (1:1)
testWithRatio(1, 1, "读写均衡(1:1)");
// 写多读少 (1:10)
testWithRatio(1, 10, "写多读少(1:10)");
}
private static void testWithRatio(int readRatio, int writeRatio, String scenario) throws Exception {
System.out.println("===== 测试场景: " + scenario + " =====");
List<Integer> cowList = new CopyOnWriteArrayList<>();
List<Integer> syncList = Collections.synchronizedList(new ArrayList<>());
// 初始化列表
for (int i = 0; i < INITIAL_SIZE; i++) {
cowList.add(i);
syncList.add(i);
}
// 计算读写操作次数
int readCount = 10000 * readRatio / (readRatio + writeRatio);
int writeCount = 10000 * writeRatio / (readRatio + writeRatio);
System.out.println("读操作: " + readCount + ", 写操作: " + writeCount);
// 测试CopyOnWriteArrayList
long cowTime = testList(cowList, "CopyOnWriteArrayList", readCount, writeCount);
// 测试SynchronizedList
long syncTime = testList(syncList, "SynchronizedList", readCount, writeCount);
System.out.println("性能比较: CopyOnWriteArrayList 是 SynchronizedList 的 "
+ String.format("%.2f", (double)syncTime/cowTime) + " 倍");
System.out.println();
}
private static long testList(List<Integer> list, String listName,
int readCount, int writeCount) throws Exception {
CountDownLatch latch = new CountDownLatch(THREAD_COUNT);
long startTime = System.nanoTime();
for (int i = 0; i < THREAD_COUNT; i++) {
final int threadNum = i;
new Thread(() -> {
try {
// 执行读操作
for (int j = 0; j < readCount; j++) {
int index = threadNum % INITIAL_SIZE;
list.get(index);
}
// 执行写操作
for (int j = 0; j < writeCount; j++) {
list.add(j);
}
} catch (Exception e) {
System.err.println(listName + " 异常: " + e.getMessage());
} finally {
latch.countDown();
}
}).start();
}
latch.await();
long endTime = System.nanoTime();
long duration = (endTime - startTime) / 1000000;
System.out.println(listName + " 耗时: " + duration + "ms");
return duration;
}
}
典型测试结果表明:
- 读多写少(10:1):CopyOnWriteArrayList 比 SynchronizedList 快约 1.5-2 倍
- 读写均衡(1:1):两者性能相近
- 写多读少(1:10):SynchronizedList 比 CopyOnWriteArrayList 快约 3-5 倍
3.5 实战应用:并发环境中的事件监听器管理
一个典型的 CopyOnWriteArrayList 应用场景是事件监听器管理,使用弱引用防止内存泄漏:
public class WeakEventBus {
// 使用CopyOnWriteArrayList存储监听器的弱引用
private final CopyOnWriteArrayList<WeakReference<EventListener>> listeners =
new CopyOnWriteArrayList<>();
// 用于清理已被GC回收的弱引用
private final ScheduledExecutorService cleanupExecutor =
Executors.newSingleThreadScheduledExecutor();
public WeakEventBus() {
// 定期清理已被GC回收的弱引用
cleanupExecutor.scheduleAtFixedRate(
this::removeStaleListeners,
10, 10, TimeUnit.SECONDS
);
}
// 注册监听器
public void register(EventListener listener) {
// 使用弱引用包装监听器
listeners.add(new WeakReference<>(listener));
}
// 显式移除监听器
public void unregister(EventListener listener) {
for (int i = 0; i < listeners.size(); i++) {
WeakReference<EventListener> ref = listeners.get(i);
EventListener refListener = ref.get();
// 如果引用已失效或等于要移除的监听器
if (refListener == null || refListener == listener) {
listeners.remove(i);
i--; // 调整索引
}
}
}
// 触发事件
public void post(Event event) {
// 遍历所有监听器并通知
for (WeakReference<EventListener> ref : listeners) {
EventListener listener = ref.get();
if (listener != null) {
try {
listener.onEvent(event);
} catch (Exception e) {
// 处理异常
e.printStackTrace();
}
}
}
}
// 清理已被GC回收的弱引用
private void removeStaleListeners() {
listeners.removeIf(ref -> ref.get() == null);
}
// 关闭清理执行器
public void shutdown() {
cleanupExecutor.shutdown();
}
// 事件接口
public interface EventListener {
void onEvent(Event event);
}
// 事件类
public static class Event {
private final String type;
private final Object data;
public Event(String type, Object data) {
this.type = type;
this.data = data;
}
public String getType() {
return type;
}
public Object getData() {
return data;
}
}
}
这个实现的优势在于:
- 使用弱引用存储监听器,避免因客户端忘记取消注册导致的内存泄漏
- 事件触发时不会阻塞注册/注销操作
- 定期清理无效引用,避免列表无限增长
- 对于需要显式注销的监听器,提供 unregister 方法
4. 并发队列体系与实战应用
4.1 并发队列分类与实现
Java 并发队列可以分为两大类:
还有一个重要的有序 Map 容器:
主要特点对比:
- 阻塞队列:当队列满/空时,入队/出队操作会阻塞等待
- 非阻塞队列:入队/出队操作不会阻塞,而是立即返回成功或失败
- 跳表容器:基于跳表实现的有序并发容器,提供 log(n)级别的查找性能
4.2 ConcurrentLinkedQueue 的无锁实现与 ABA 问题
ConcurrentLinkedQueue 是一个基于链表的无界队列,它使用 CAS 操作实现无锁并发:
public boolean offer(E e) {
if (e == null) throw new NullPointerException();
final Node<E> newNode = new Node<E>(e);
for (Node<E> t = tail, p = t;;) {
Node<E> q = p.next;
if (q == null) {
// p是最后一个节点
if (p.casNext(null, newNode)) {
// 成功插入,尝试更新tail
if (p != t)
casTail(t, newNode);
return true;
}
}
else if (p == q)
// 帮助初始化或处理已删除节点
p = (t != (t = tail)) ? t : head;
else
// 检查下一个节点
p = (p != t && t != (t = tail)) ? t : q;
}
}
这段代码的精髓在于通过 CAS 操作实现了无锁的并发添加,大大提高了性能。
ABA 问题及其解决方案
CAS 操作可能面临 ABA 问题(值被修改后又改回原值)。在 ConcurrentLinkedQueue 中,由于操作的是对象引用,而每个 Node 是唯一的新对象,所以不会出现引用级别的 ABA 问题。
对于可能出现 ABA 问题的场景,JUC 提供了AtomicStampedReference
解决方案:
// 创建带版本号的原子引用
AtomicStampedReference<Integer> atomicRef =
new AtomicStampedReference<>(100, 0); // 初始值100,版本号0
// 获取当前值和版本号
int[] stampHolder = new int[1];
Integer value = atomicRef.get(stampHolder);
int initialStamp = stampHolder[0];
// CAS操作时同时检查值和版本号
boolean success = atomicRef.compareAndSet(
value, newValue, initialStamp, initialStamp + 1);
AtomicStampedReference
通过引入版本号,可以检测值是否被中间修改过,即使最终值相同也能察觉到变化。
4.3 各种 BlockingQueue 对比与选择
不同 BlockingQueue 实现有不同的特点和适用场景:
ArrayBlockingQueue:基于数组的有界队列
- 适用:有界缓冲区,生产速度与消费速度相近
- 特点:构造时必须指定容量,不会自动扩容
LinkedBlockingQueue:基于链表的可选有界队列
- 适用:吞吐量要求高,生产和消费速度差异大
- 特点:不指定容量时默认为 Integer.MAX_VALUE(21 亿),需要注意 OOM 风险
PriorityBlockingQueue:支持优先级的无界队列
- 适用:任务优先级处理场景
- 特点:元素必须实现 Comparable 接口或提供 Comparator
DelayQueue:延迟获取元素的无界队列
- 适用:定时任务调度
- 特点:元素必须实现 Delayed 接口,到期时间到达才能取出
SynchronousQueue:没有缓冲的阻塞队列
- 适用:直接交付场景,生产者必须等待消费者取走元素
- 特点:没有存储空间,put 必须等待 take
LinkedTransferQueue:融合 SynchronousQueue 和 LinkedBlockingQueue 的特性
- 适用:既需要队列存储又需要直接交付的场景
- 特点:支持 tryTransfer 操作,可选择性地等待消费者接收
ConcurrentSkipListMap:基于跳表的并发有序 Map
- 适用:需要按键排序且并发访问的场景
- 特点:提供了与 TreeMap 类似的操作,但是线程安全的
4.4 实战应用:高效生产者-消费者系统
下面是一个使用 BlockingQueue 实现的生产者-消费者模式示例:
public class TaskProcessor {
// 使用有界阻塞队列存储任务
private final BlockingQueue<Task> taskQueue;
// 消费者线程池
private final ExecutorService consumers;
// 运行标志
private volatile boolean running = true;
public TaskProcessor(int queueSize, int consumerCount) {
// 显式指定队列大小,避免无界队列可能的OOM风险
this.taskQueue = new LinkedBlockingQueue<>(queueSize);
this.consumers = Executors.newFixedThreadPool(consumerCount, r -> {
Thread t = new Thread(r, "task-consumer");
t.setUncaughtExceptionHandler((thread, ex) -> {
System.err.println("消费者线程异常: " + ex.getMessage());
});
return t;
});
// 启动消费者线程
for (int i = 0; i < consumerCount; i++) {
consumers.submit(this::consumeTask);
}
}
// 提交任务(生产者)
public boolean submitTask(Task task) {
if (!running) {
return false;
}
try {
// 尝试放入队列,最多等待100ms
return taskQueue.offer(task, 100, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
// 消费者线程逻辑
private void consumeTask() {
while (running) {
try {
// 从队列取任务,最多等待1s
Task task = taskQueue.poll(1, TimeUnit.SECONDS);
if (task != null) {
try {
// 处理任务
task.process();
} catch (Exception e) {
// 处理任务异常
System.err.println("Task process error: " + e.getMessage());
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}
// 返回当前队列大小(用于监控)
public int getQueueSize() {
return taskQueue.size();
}
// 关闭处理器
public void shutdown() {
running = false;
consumers.shutdown();
try {
if (!consumers.awaitTermination(5, TimeUnit.SECONDS)) {
consumers.shutdownNow();
}
} catch (InterruptedException e) {
consumers.shutdownNow();
Thread.currentThread().interrupt();
}
}
// 任务接口
public interface Task {
void process();
}
}
这个实现的优点:
- 利用 BlockingQueue 自动处理生产者和消费者的速度差异
- 提供超时机制避免生产者无限阻塞
- 优雅地处理关闭逻辑,确保资源释放
- 显式指定队列大小,避免潜在的内存溢出问题
5. 并发容器选择指南与性能优化
5.1 如何正确选择并发容器?
5.2 分布式环境下的并发容器选择
在分布式系统中,JUC 并发容器仅能保证单 JVM 内的线程安全,对于跨 JVM 的并发访问,需要结合分布式组件:
对于需要跨 JVM 共享的数据,可以考虑:
- Redis:适用于高性能分布式缓存和数据结构
- Hazelcast/Ignite:分布式内存数据网格
- ZooKeeper:分布式锁和协调服务
- Kafka:分布式消息队列替代 BlockingQueue
5.3 常见问题与解决方案
内存占用问题
- CopyOnWriteArrayList 每次写操作都会复制数组,容易导致 GC 压力增大
- 解决方案:限制集合大小,合并修改操作,必要时考虑其他集合实现
ABA 问题
- CAS 操作可能遇到 ABA 问题(值被修改后又改回原值)
- 解决方案:使用 AtomicStampedReference 或 AtomicMarkableReference 添加版本号或标记位
死锁问题
- 即使使用并发容器,不当的使用模式仍可能导致死锁
- 解决方案:避免嵌套锁、使用带超时的操作、遵循固定的锁定顺序
伪共享问题
- 多核 CPU 缓存行共享导致的性能下降(一个线程修改变量导致其他 CPU 缓存失效)
- 解决方案:使用@Contended 注解、填充变量避免共享缓存行
5.4 性能优化技巧
容器预设容量
// 预设合理容量避免扩容 Map<String, String> map = new ConcurrentHashMap<>(1024);
批量操作优化
// 使用ConcurrentHashMap的原子复合操作 map.compute(key, (k, v) -> v == null ? 1 : v + 1);
选择合适的负载因子
// 调整负载因子平衡空间与时间 new ConcurrentHashMap<>(initialCapacity, loadFactor);
避免不必要的同步
// 优先使用putIfAbsent而非containsKey+put map.putIfAbsent(key, value);
队列容量设置
// 设置合理的队列容量,避免生产者等待或消费者空转 new ArrayBlockingQueue<>(Runtime.getRuntime().availableProcessors() * 2);
6. 总结
容器类型 | 实现 | 线程安全机制 | 适用场景 | 性能特点 |
---|---|---|---|---|
Map | ConcurrentHashMap | CAS+synchronized | 高并发读写 Map | 读写都有较好性能 |
Map | Collections.synchronizedMap | 对象锁 | 低并发场景 | 写性能一般,读性能差 |
Map | ConcurrentSkipListMap | CAS+跳表 | 需要排序的 Map | 有序,log(n)查找 |
List | CopyOnWriteArrayList | 写时复制+ReentrantLock | 读多写少(10:1 以上) | 读极快,写很慢 |
List | Collections.synchronizedList | 对象锁 | 写频繁场景 | 读写性能一般 |
Queue | ConcurrentLinkedQueue | CAS 无锁 | 高性能非阻塞队列 | 非阻塞,性能高 |
Queue | ArrayBlockingQueue | ReentrantLock 条件变量 | 有界生产者-消费者 | 有界,可阻塞 |
Queue | LinkedBlockingQueue | ReentrantLock 条件变量 | 高吞吐量队列 | 可选有界,可阻塞 |
Queue | SynchronousQueue | 交接槽设计 | 直接传递场景 | 零缓冲,直接交付 |
Queue | LinkedTransferQueue | CAS+队列 | 灵活传输场景 | 高性能传输队列 |
Java 并发容器是多线程编程的重要工具,正确理解它们的实现原理和适用场景,能够帮助我们写出更高效、更可靠的并发程序。在实际使用中,需要根据具体的业务场景和性能需求,选择合适的并发容器实现。
感谢您耐心阅读到这里!如果觉得本文对您有帮助,欢迎点赞 👍、收藏 ⭐、分享给需要的朋友,您的支持是我持续输出技术干货的最大动力!
如果想获取更多 Java 技术深度解析,欢迎点击头像关注我,后续会每日更新高质量技术文章,陪您一起进阶成长~
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。