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),我们有了更高效的并发容器选择。

下面我们围绕三个核心问题展开:

  1. 并发容器相比传统同步容器有哪些优势?
  2. 各种并发容器的内部实现原理是怎样的?
  3. 如何在实际项目中正确选择并高效使用这些容器?

2. ConcurrentHashMap 深度解析

2.1 进化历程:从分段锁到 CAS+红黑树

ConcurrentHashMap 是并发编程中使用最广泛的 Map 实现,它的设计经历了重大变革:

graph LR
    A[JDK 1.5-1.6] --> |分段锁机制 Segment| B[JDK 1.7]
    B --> |CAS+红黑树| C[JDK 1.8+]
  • 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;
}

这里有几个关键点:

  1. 使用 CAS 操作尝试插入新节点
  2. 只有发生哈希冲突时才使用 synchronized 锁定桶
  3. 当链表长度超过阈值时转换为红黑树(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 操作的关键在于:完全不需要加锁!这是因为:

  1. Node 的 value 和 next 指针使用了 volatile 修饰,保证了内存可见性
  2. Node 节点的不可变性设计 - key 和 hash 值在创建后不可修改,value 只能通过原子操作更新
  3. 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();
    }
}

这个实现的优点:

  1. 利用 ConcurrentHashMap 的高并发特性支持多线程访问
  2. 无需加锁即可进行读写操作
  3. 使用迭代器安全地删除过期数据,避免 ConcurrentModificationException
  4. 使用守护线程处理清理工作,避免阻止 JVM 退出

3. CopyOnWriteArrayList 深度解析

3.1 写时复制机制详解

CopyOnWriteArrayList 是 ArrayList 的线程安全版本,它使用了一种叫"写时复制"的策略:

graph TD
    A[读操作] --> B{直接访问当前数组}
    C[写操作] --> D[复制整个数组]
    D --> E[在副本上进行修改]
    E --> F[替换引用]

核心原理是:

  • 读操作不需要加锁,直接访问内部数组
  • 写操作加锁,并复制整个数组,在副本上修改完成后替换原数组

需要特别注意的是,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;
        }
    }
}

这个实现的优势在于:

  1. 使用弱引用存储监听器,避免因客户端忘记取消注册导致的内存泄漏
  2. 事件触发时不会阻塞注册/注销操作
  3. 定期清理无效引用,避免列表无限增长
  4. 对于需要显式注销的监听器,提供 unregister 方法

4. 并发队列体系与实战应用

4.1 并发队列分类与实现

Java 并发队列可以分为两大类:

Java 并发队列

还有一个重要的有序 Map 容器:

graph LR
    A[并发有序Map] --> B[ConcurrentSkipListMap]
    A --> C[ConcurrentSkipListSet]

主要特点对比:

  1. 阻塞队列:当队列满/空时,入队/出队操作会阻塞等待
  2. 非阻塞队列:入队/出队操作不会阻塞,而是立即返回成功或失败
  3. 跳表容器:基于跳表实现的有序并发容器,提供 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 实现有不同的特点和适用场景:

  1. ArrayBlockingQueue:基于数组的有界队列

    • 适用:有界缓冲区,生产速度与消费速度相近
    • 特点:构造时必须指定容量,不会自动扩容
  2. LinkedBlockingQueue:基于链表的可选有界队列

    • 适用:吞吐量要求高,生产和消费速度差异大
    • 特点:不指定容量时默认为 Integer.MAX_VALUE(21 亿),需要注意 OOM 风险
  3. PriorityBlockingQueue:支持优先级的无界队列

    • 适用:任务优先级处理场景
    • 特点:元素必须实现 Comparable 接口或提供 Comparator
  4. DelayQueue:延迟获取元素的无界队列

    • 适用:定时任务调度
    • 特点:元素必须实现 Delayed 接口,到期时间到达才能取出
  5. SynchronousQueue:没有缓冲的阻塞队列

    • 适用:直接交付场景,生产者必须等待消费者取走元素
    • 特点:没有存储空间,put 必须等待 take
  6. LinkedTransferQueue:融合 SynchronousQueue 和 LinkedBlockingQueue 的特性

    • 适用:既需要队列存储又需要直接交付的场景
    • 特点:支持 tryTransfer 操作,可选择性地等待消费者接收
  7. 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();
    }
}

这个实现的优点:

  1. 利用 BlockingQueue 自动处理生产者和消费者的速度差异
  2. 提供超时机制避免生产者无限阻塞
  3. 优雅地处理关闭逻辑,确保资源释放
  4. 显式指定队列大小,避免潜在的内存溢出问题

5. 并发容器选择指南与性能优化

5.1 如何正确选择并发容器?

如何正确选择并发容器

5.2 分布式环境下的并发容器选择

在分布式系统中,JUC 并发容器仅能保证单 JVM 内的线程安全,对于跨 JVM 的并发访问,需要结合分布式组件:

分布式环境下的并发容器选择

对于需要跨 JVM 共享的数据,可以考虑:

  1. Redis:适用于高性能分布式缓存和数据结构
  2. Hazelcast/Ignite:分布式内存数据网格
  3. ZooKeeper:分布式锁和协调服务
  4. Kafka:分布式消息队列替代 BlockingQueue

5.3 常见问题与解决方案

  1. 内存占用问题

    • CopyOnWriteArrayList 每次写操作都会复制数组,容易导致 GC 压力增大
    • 解决方案:限制集合大小,合并修改操作,必要时考虑其他集合实现
  2. ABA 问题

    • CAS 操作可能遇到 ABA 问题(值被修改后又改回原值)
    • 解决方案:使用 AtomicStampedReference 或 AtomicMarkableReference 添加版本号或标记位
  3. 死锁问题

    • 即使使用并发容器,不当的使用模式仍可能导致死锁
    • 解决方案:避免嵌套锁、使用带超时的操作、遵循固定的锁定顺序
  4. 伪共享问题

    • 多核 CPU 缓存行共享导致的性能下降(一个线程修改变量导致其他 CPU 缓存失效)
    • 解决方案:使用@Contended 注解、填充变量避免共享缓存行

5.4 性能优化技巧

  1. 容器预设容量

    // 预设合理容量避免扩容
    Map<String, String> map = new ConcurrentHashMap<>(1024);
  2. 批量操作优化

    // 使用ConcurrentHashMap的原子复合操作
    map.compute(key, (k, v) -> v == null ? 1 : v + 1);
  3. 选择合适的负载因子

    // 调整负载因子平衡空间与时间
    new ConcurrentHashMap<>(initialCapacity, loadFactor);
  4. 避免不必要的同步

    // 优先使用putIfAbsent而非containsKey+put
    map.putIfAbsent(key, value);
  5. 队列容量设置

    // 设置合理的队列容量,避免生产者等待或消费者空转
    new ArrayBlockingQueue<>(Runtime.getRuntime().availableProcessors() * 2);

6. 总结

容器类型实现线程安全机制适用场景性能特点
MapConcurrentHashMapCAS+synchronized高并发读写 Map读写都有较好性能
MapCollections.synchronizedMap对象锁低并发场景写性能一般,读性能差
MapConcurrentSkipListMapCAS+跳表需要排序的 Map有序,log(n)查找
ListCopyOnWriteArrayList写时复制+ReentrantLock读多写少(10:1 以上)读极快,写很慢
ListCollections.synchronizedList对象锁写频繁场景读写性能一般
QueueConcurrentLinkedQueueCAS 无锁高性能非阻塞队列非阻塞,性能高
QueueArrayBlockingQueueReentrantLock 条件变量有界生产者-消费者有界,可阻塞
QueueLinkedBlockingQueueReentrantLock 条件变量高吞吐量队列可选有界,可阻塞
QueueSynchronousQueue交接槽设计直接传递场景零缓冲,直接交付
QueueLinkedTransferQueueCAS+队列灵活传输场景高性能传输队列

Java 并发容器是多线程编程的重要工具,正确理解它们的实现原理和适用场景,能够帮助我们写出更高效、更可靠的并发程序。在实际使用中,需要根据具体的业务场景和性能需求,选择合适的并发容器实现。


感谢您耐心阅读到这里!如果觉得本文对您有帮助,欢迎点赞 👍、收藏 ⭐、分享给需要的朋友,您的支持是我持续输出技术干货的最大动力!

如果想获取更多 Java 技术深度解析,欢迎点击头像关注我,后续会每日更新高质量技术文章,陪您一起进阶成长~


异常君
1 声望1 粉丝

在 Java 的世界里,永远有下一座技术高峰等着你。我愿做你登山路上的同频伙伴,陪你从看懂代码到写出让自己骄傲的代码。咱们,代码里见!