要在程序中处理数据,离不开集合的帮忙。要想安全操作集合,就需要了解集合的数据结构,以及如何保证并发操作数据和程序的安全性。本文通过常见集合的整理,方便我们了解各个集合的特点。
1.java集合框架一览
(ps:这里网上找了一份大佬的图,如有侵权,联系我删除,先感谢一下大哥🙏)
2. 如上可以看到java体系中包含两大块:List和Map
2.1 List接口
2.1.1 常用子类及实现
2.1.1.1 ArrayList
- 线程安全:非线程安全。
- 数据结构:动态数组。
- 线程安全实现:
可以使用 Collections.synchronizedList() 方法包装为线程安全的 List。
使用 CopyOnWriteArrayList 替代(本质是一个线程安全的 ArrayList 实现)。 - 扩容机制:
默认初始容量为 10。
每次扩容时,新容量为旧容量的 1.5 倍(即 newCapacity = oldCapacity + (oldCapacity >> 1)(ps: >> 1表示大小为原来一半))。 - 操作影响:
add:在尾部插入元素,若容量不足触发扩容,导致数组复制。
remove:删除后,后续元素前移。 - 遍历和递增数据:
普通遍历时,插入或删除元素会导致 ConcurrentModificationException。(实际上称之为:fail-fast。) - 不能删除元素原因:
Iterator 是集合提供的专用迭代工具,内部维护了一个修改计数器 (modCount) 和一个迭代器自己的期望计数器 (expectedModCount)。
当集合结构发生修改(例如添加或删除元素)时,modCount 会增加。
如果通过 Iterator 的 remove() 方法删除元素,expectedModCount 会与 modCount 同步更新,迭代器认为这是合法修改,因此不会抛出异常。而普通for循环操作增删元素只会修改modCount变量,一次系统认为modCount和expectedModCount不想等,代表并发修改,虽然实际上不是并发操作。但是此时会抛出一场给用户。
2.1.1.2 LinkedList
- 线程安全:非线程安全。
- 数据结构:双向链表。
- 线程安全实现:
使用 Collections.synchronizedList() 方法包装为线程安全的 List。 - 扩容机制:链表无需扩容,动态调整。
- 操作影响:
add:插入元素时,创建新节点,调整前后节点引用。
remove:删除元素时,调整前后节点引用。 - 遍历和递增数据:
遍历时插入/删除会导致链表结构变化,应使用 Iterator。
2.1.1.3 Vector
- 线程安全:线程安全(内部暴露出来的方法都是synchronized关键字锁住的,以此来保证线程操作的安全性)。
- 数据结构:动态数组。
- 扩容机制:
默认初始容量为 10。 - 每次扩容时,若未指定增量容量,则扩容为旧容量的两倍。
- 操作影响:
add 和 remove 类似于 ArrayList,但线程安全开销较大。 - 遍历和递增数据:
遍历时线程安全,但可能出现数据不一致的逻辑问题。
2.1.1.4 CopyOnWriteArrayList
- 线程安全:线程安全。
- 如何保证线程安全(概述):
1.在做修改操作的时候加锁
2.每次修改都是将元素copy到一个新的数组中,并且将数组赋值到成员变量array中。
3.利用volatile关键字修饰成员变量array,这样就可以保证array的引用的可见性,每次修改之前都能够拿到最新的array引用。这点很关键。 - 数据结构:基于数组,每次写操作会复制整个数组。
- 线程安全实现:通过写操作时复制整个数组来保证线程安全。
- 扩容机制:同 ArrayList。
- 操作影响:
add:复制数组后插入元素,性能较低。
remove:复制数组后删除元素。 - 遍历和递增数据:
迭代器基于快照,因此不会抛出 ConcurrentModificationException。
2.2 Map接口
2.2.1 常用子类及实现
2.2.1.1 HashMap
- 线程安全:非线程安全。
- 数据结构:数组 + 链表(Java 8 以后链表超过阈值时转换为红黑树)。
- 线程安全实现:
使用 Collections.synchronizedMap() 包装为线程安全。
使用 ConcurrentHashMap 替代。 - 扩容机制:
默认初始容量为 16。
当负载因子(默认 0.75)超出阈值时,扩容为原数组容量的两倍,重新分布元素。当我们new一个HashMap的时候,当我们默认制定一个大小的时候,实际上会向2的幂次方靠,例如我们指定了大小为11,实际上会开辟一个大小为2^4=16的map。因为后续扩容需要基于2的幂次方扩容,即2倍。 - 操作影响:
put:插入元素时,可能触发扩容。
remove:删除元素时调整链表或树。 - 遍历和递增数据:
遍历时插入/删除会抛出 ConcurrentModificationException,应使用 Iterator。
2.2.1.2 LinkedHashMap
- 线程安全:非线程安全。
- 数据结构:基于 HashMap,并维护一个双向链表记录插入顺序。
- 线程安全实现:
使用 Collections.synchronizedMap() 包装。
扩容机制:与 HashMap 相同。 - 操作影响:
put 和 remove 同 HashMap,但链表顺序会变化。 - 遍历和递增数据:
遍历时顺序稳定,可插入/删除但需使用 Iterator。
2.2.1.3 ConcurrentHashMap
- 线程安全:线程安全。
- 数据结构:分段锁机制(将哈希表分为16个区域,即segment,分别对每个区域加锁。Java 8 后使用 CAS 和分区数组 + 红黑树)。
- 线程安全实现:
通过锁分段和 CAS 操作实现高效线程安全。 - 扩容机制:
默认初始容量为 16。
扩容为原容量的两倍,按分区扩展。 - 操作影响:
put 和 remove:高效线程安全,但可能触发扩容。 - 遍历和递增数据:
遍历时可能包含插入/删除后的数据,但不会抛出异常。
2.2.1.4 TreeMap
- 线程安全:非线程安全。
- 数据结构:红黑树。
- 线程安全实现:
使用 Collections.synchronizedSortedMap() 包装。 - 扩容机制:无需扩容,动态调整红黑树结构。
- 操作影响:
put 和 remove 会调整红黑树结构。 - 遍历和递增数据:
顺序按照键的自然顺序或比较器顺序。
3.总结
安全使用 Java 集合需要根据场景选择合适的集合类型,如单线程使用普通集合,多线程使用线程安全集合(如 CopyOnWriteArrayList 或 ConcurrentHashMap)。在遍历中删除元素时,优先使用 Iterator 的 remove() 方法,避免直接修改集合引发 ConcurrentModificationException。对于多线程场景,可通过同步块或线程安全集合避免数据竞争,尽量使用原子操作。初始化时建议预估容量以减少扩容开销,且在不需要修改时使用不可变集合。此外,应根据性能需求选择适当的遍历方式和集合实现,避免死锁及不必要的开销。