Java集合框架在多线程下如何保证安全

来源:IPIPP.com作者:头衔:全栈工程师
导读:本期聚焦于小伙伴创作的《Java集合框架在多线程下如何保证安全》,敬请观看详情,探索知识的价值。以下视频、文章将为您系统阐述其核心内容与价值。如果您觉得《Java集合框架在多线程下如何保证安全》有用,将其分享出去将是对创作者最好的鼓励。

Java集合框架多线程安全的核心问题

Java集合框架提供了丰富的集合实现,比如List体系的ArrayList、LinkedList,Map体系的HashMap、TreeMap,Set体系的HashSet、TreeSet等,这些集合在设计时都没有考虑多线程并发场景,因此都是非线程安全的。在多线程同时读写这些集合时,很容易出现各类问题。

最常见的问题是并发修改异常,比如一个线程在遍历ArrayList,另一个线程同时修改ArrayList的结构(新增、删除元素),就会抛出ConcurrentModificationException。另外还会出现数据覆盖、数据丢失、内部结构损坏等问题,比如多线程同时往HashMap中put元素,可能会导致链表成环,查询时出现死循环,或者元素没有被正确存入集合。

因此当我们需要在多线程场景下使用集合时,必须采取额外措施保证集合的线程安全,下面介绍几种常用的实现方式。

使用同步容器保证线程安全

同步容器是Java早期提供的线程安全集合方案,核心思路是通过 synchronized 关键字对集合的所有方法加锁,保证同一时间只有一个线程能访问集合方法,从而避免并发问题。

Vector和Hashtable

Vector是List接口的同步实现,Hashtable是Map接口的同步实现,这两个类在早期JDK中就已经存在,它们的所有公开方法都用 synchronized 修饰,比如Vector的add方法:

public synchronized boolean add(E e) {
    modCount++;
    ensureCapacityHelper(elementCount + 1);
    elementData[elementCount++] = e;
    return true;
}

Hashtable的put方法同样如此:

public synchronized V put(K key, V value) {
    // 检查key是否为null
    if (value == null) {
        throw new NullPointerException();
    }
    // 省略哈希计算、存入逻辑
    return null;
}

这种实现方式的优点是使用简单,不需要额外操作就能保证线程安全。但缺点也非常明显:所有方法都加全局锁,同一时间只能有一个线程访问集合,并发性能极差,在高并发场景下会成为性能瓶颈,因此现在实际开发中已经很少使用这两个类。

Collections工具类的同步包装方法

JDK的Collections工具类提供了一系列同步包装方法,可以把非线程安全的集合包装成线程安全的集合,比如 synchronizedList、synchronizedMap、synchronizedSet 等。

使用示例:

import java.util.*;

public class SynchronizedCollectionDemo {
    public static void main(String[] args) {
        // 包装ArrayList为线程安全的List
        List<String> syncList = Collections.synchronizedList(new ArrayList<>());
        // 包装HashMap为线程安全的Map
        Map<String, Integer> syncMap = Collections.synchronizedMap(new HashMap<>());
        // 包装HashSet为线程安全的Set
        Set<String> syncSet = Collections.synchronizedSet(new HashSet<>());
        
        // 多线程写入测试
        for (int i = 0; i < 10; i++) {
            final int index = i;
            new Thread(() -> {
                syncList.add("element" + index);
                syncMap.put("key" + index, index);
                syncSet.add("setElement" + index);
            }).start();
        }
    }
}

需要注意的是,虽然这些包装后的集合本身是线程安全的,但如果是遍历操作,仍然需要手动加锁,否则还是可能出现并发修改异常。比如遍历syncList时:

// 正确的遍历方式,需要手动加锁
synchronized (syncList) {
    Iterator<String> it = syncList.iterator();
    while (it.hasNext()) {
        System.out.println(it.next());
    }
}

同步包装类的实现原理和Vector、Hashtable类似,也是通过 synchronized 关键字对方法或者代码块加锁,只不过它们是把原集合作为内部成员变量,所有操作都转发给原集合,同时加锁保证安全,性能问题和早期的同步容器一样,高并发场景下表现不佳。

使用并发容器保证线程安全

为了解决同步容器性能差的问题,JDK 1.5之后在java.util.concurrent包下提供了一系列并发容器,这些容器针对多线程场景做了优化,在保证线程安全的同时,大幅提升了并发性能。

Java集合框架在多线程下如何保证安全

ConcurrentHashMap

ConcurrentHashMap是HashMap的线程安全替代方案,在JDK 1.7和JDK 1.8中的实现原理有所不同:JDK 1.7采用分段锁(Segment)的机制,每个Segment对应一个小的哈希表,锁的粒度是Segment,不同Segment的操作可以并发执行;JDK 1.8之后放弃了分段锁,采用CAS + synchronized 的方式实现,锁的粒度进一步细化到哈希表的每个桶(bucket),并发性能更高。

使用示例:

import java.util.concurrent.ConcurrentHashMap;

public class ConcurrentHashMapDemo {
    public static void main(String[] args) {
        ConcurrentHashMap<String, Integer> concurrentMap = new ConcurrentHashMap<>();
        
        // 多线程写入
        for (int i = 0; i < 10; i++) {
            final int index = i;
            new Thread(() -> {
                concurrentMap.put("key" + index, index);
            }).start();
        }
        
        // 不需要额外加锁就可以安全遍历
        concurrentMap.forEach((key, value) -> {
            System.out.println(key + ":" + value);
        });
    }
}

ConcurrentHashMap的遍历是弱一致性的,不会抛出并发修改异常,遍历的是集合在某个时刻的快照或者实时状态,具体取决于遍历的方式,适合高并发场景下的Map使用。

CopyOnWriteArrayList和CopyOnWriteArraySet

CopyOnWrite容器也叫写时复制容器,核心思想是:当往容器中添加元素时,不直接修改原容器,而是先将原容器复制一份,在复制的容器上进行修改,修改完成后再将原容器的引用指向新的容器。读操作不需要加锁,因为读的是原容器,写操作会加锁,避免多个线程同时复制容器。

CopyOnWriteArrayList的使用示例:

import java.util.concurrent.CopyOnWriteArrayList;

public class CopyOnWriteDemo {
    public static void main(String[] args) {
        CopyOnWriteArrayList<String> cowList = new CopyOnWriteArrayList<>();
        
        // 写线程
        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                cowList.add("write" + i);
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        
        // 读线程,不需要加锁,也不会抛异常
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                System.out.println("当前列表大小:" + cowList.size());
                try {
                    Thread.sleep(150);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

这种容器的优点是读性能极高,因为读操作完全不需要加锁,适合读多写少的场景。缺点是写操作开销大,因为每次写都要复制整个容器,而且会占用更多的内存,因为旧容器可能还在被读线程使用,无法及时回收。

其他并发容器

除了上述两种常用的并发容器,java.util.concurrent包还提供了很多其他场景的线程安全集合:

  • ConcurrentSkipListMap:线程安全的有序Map,基于跳表实现,支持排序,性能比同步的TreeMap高很多
  • ConcurrentSkipListSet:线程安全的有序Set,基于ConcurrentSkipListMap实现
  • ArrayBlockingQueue:基于数组实现的有界阻塞队列,适合生产者消费者模式
  • LinkedBlockingQueue:基于链表实现的可选有界阻塞队列,吞吐量通常高于ArrayBlockingQueue
  • PriorityBlockingQueue:支持优先级的无界阻塞队列

不同方案的对比和选择

为了帮助开发者选择合适的线程安全集合方案,下面从性能、适用场景等维度做对比:

方案类型代表实现优点缺点适用场景
同步容器Vector、Hashtable、Collections包装类使用简单,理解成本低全局锁,并发性能差并发量极低的场景,几乎没有性能要求的情况
并发容器-哈希类ConcurrentHashMap并发性能高,支持高并发读写遍历弱一致性,部分操作不支持(比如空键值)高并发场景下的Map存储,比如缓存、共享配置等
并发容器-写时复制类CopyOnWriteArrayList、CopyOnWriteArraySet读性能极高,无锁读写开销大,内存占用高读多写少,比如配置列表、黑白名单等场景
并发容器-阻塞队列ArrayBlockingQueue、LinkedBlockingQueue支持阻塞操作,天然适配生产者消费者模式主要用于队列场景,不适合普通集合存储线程池任务队列、消息传递、生产者消费者场景

使用注意事项

即使使用了线程安全的集合,在实际开发中也有不少需要注意的点,避免踩坑:

复合操作需要额外加锁

很多并发容器的单个操作是线程安全的,但复合操作(比如先检查再更新、先获取再修改)不是线程安全的。比如ConcurrentHashMap的putIfAbsent方法是原子操作,但如果我们需要实现如果存在就更新,不存在就插入的逻辑,就需要使用容器提供的原子复合方法,而不是自己写判断逻辑。

错误示例:

ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
// 错误的复合操作,不是线程安全的
if (!map.containsKey("key")) {
    map.put("key", 1);
}

正确示例,使用原子方法:

ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
// 使用putIfAbsent原子方法,保证线程安全
map.putIfAbsent("key", 1);

避免大对象写入CopyOnWrite容器

CopyOnWrite容器每次写都会复制整个底层数组,如果集合中的元素很大,或者集合本身容量很大,写操作会占用大量内存和CPU资源,甚至导致内存溢出,因此这类容器不适合存储大对象或者容量过大的集合。

根据业务场景选择容器

不要盲目选择并发容器,比如如果集合只在单线程中使用,就不需要用线程安全的集合,避免不必要的性能开销。如果读写频率差不多,也不适合用CopyOnWrite容器,这时候ConcurrentHashMap或者同步容器可能更合适。

总结

Java集合框架在多线程下的线程安全保证有多种方案,同步容器是最早期的方案,实现简单但性能差,适合极低并发场景;并发容器是现在的主流选择,其中ConcurrentHashMap适合高并发的Map场景,CopyOnWriteArrayList适合读多写少的List场景,阻塞队列适合生产者消费者场景。开发者需要根据具体的业务场景、并发量、读写比例来选择合适的集合实现,同时注意复合操作的线程安全,避免因为使用不当导致的问题。

Java集合框架多线程安全同步容器并发容器CopyOnWrite修改时间:2026-05-24 20:31:35

免责声明:已尽一切努力确保本网站所含信息的准确性。网站部分内容来源于网络或由用户自行发表,内容观点不代表本站立场。本站是个人网站免费分享,内容仅供个人学习、研究或参考使用,如内容中引用了第三方作品,其版权归原作者所有。若内容触犯了您的权益,请联系我们进行处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。前端、网络、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握网站开发与运维所需的核心技术栈。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端逻辑,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。