省流版
| 集合 | 底层机制 | 特性 | 扩容机制 | 线程安全 | 优点 |
|---|---|---|---|---|---|
| ArrayList | 动态数组 | 允许存储重复元素和 null 值。随机访问较快,插入删除较慢 | 当元素数量超过当前数组容量时,ArrayList 会自动扩容,新容量 = 旧容量 + 旧容量 / 2(即扩容为原来的 1.5 倍),如果扩容后的容量仍然不足,则直接使用所需的最小容量,将旧数组中的元素复制到新数组中。 | 否 | 其动态扩展、快速随机访问以及灵活的增删操作的优点,适合需要频繁访问元素且数据量动态变化的场景。 |
| LinkedList | 双向链表 | 插入和删除操作上具有更高的效率,但在随机访问上性能较差 | 没有固定的初始容量,需要像 ArrayList 那样进行扩容操作 | 否 | 插入和删除操作高效,尤其在列表中间进行增删时性能优异,因为只需调整指针而无需移动元素。此外,它还支持双向遍历,适合需要频繁从两端访问或修改数据的场景。 |
| Vector | 动态数组 | 允许存储重复元素和 null 值,随机访问较快 | 如果指定了扩容增量(capacityIncrement),则新容量 = 旧容量 + 扩容增量; 如果未指定扩容增量,则新容量 = 旧容量 * 2;如果扩容后的容量仍然不足,则直接使用所需的最小容量;将旧数组中的元素复制到新数组中。 | 是 | 线程安全,所有操作方法都是同步的,适合在多线程环境中使用。此外,它支持动态扩展,能够根据需要自动调整容量,简化了数组大小管理的复杂性。 |
| Stack | Vector实现的栈数据结构 | 后进先出(LIFO, Last In First Out) | 扩容机制与 Vector 相同 | 是 | 遵循后进先出(LIFO)原则,适合需要处理栈式数据结构的场景,如表达式求值、回溯算法等。此外,它基于 Vector 实现,继承了线程安全特性,能够在多线程环境下安全操作栈元素。 |
| CopyOnWriteArrayList | 基于写时复制(Copy-On-Write)机制实现 | 读操作性能极高,写操作性能较低 | CopyOnWriteArrayList 的扩容机制与 ArrayList 类似,每次写操作都会创建一个新的数组副本。新数组的容量根据实际需求动态调整。如果新数组容量不足,则扩容为当前容量的 1.5 倍 | 是 | 读操作上完全线程安全且无需加锁,适合高读低写的并发场景,因为写操作通过复制底层数组实现,避免了对原始列表的直接修改。此外,它简化了多线程环境下的迭代操作,不会抛出ConcurrentModificationException,确保迭代器的安全性。 |
| Collections.synchronizedList | 通过包装现有的List实现类(如ArrayList或LinkedList)来提供线程安全的功能。它内部维护了一个mutex对象(锁对象),所有方法都通过synchronized(mutex)实现同步。 | 线程安全 | 扩容机制取决于被包装的 List 实现类 | 是 | 为现有列表提供线程安全的包装,适合在多线程环境中对已有列表进行同步操作,而无需替换原有数据结构。此外,它比直接使用 Vector 更灵活,允许开发者根据需求选择性地同步特定代码块,提升性能和可控性。 |
| HashSet | HashSet的底层是一个HashMap,元素存储在HashMap的键中,值是一个固定的PRESENT对象。 | HashSet不允许存储重复元素,并且不保证元素的顺序。允许存储null值。 | 当元素数量超过 当前容量 * 负载因子时,HashSet 会自动扩容。新容量 = 旧容量 * 2 | 否 | 基于哈希表实现,添加、删除和查找元素的平均时间复杂度为 O(1),适合需要快速判断元素是否存在或进行集合运算的场景。此外,它不允许存储重复元素,自动保证集合中元素的唯一性。 |
| LinkedHashSet | LinkedHashSet的底层是一个LinkedHashMap,结合了哈希表和双向链表的特性,哈希表用于快速查找和去重,双向链表用于维护元素的插入顺序 | LinkedHashSet不允许存储重复元素,且允许存储null值。它保留了元素的插入顺序 | 当元素数量超过当前容量与负载因子的乘积时,LinkedHashSet会自动扩容,新容量 = 旧容量 * 2 | 否 | 继承了HashSet的高效去重能力,同时通过双向链表维护元素的插入顺序,适合需要兼顾元素唯一性和顺序性的场景。此外,它的迭代顺序与插入顺序一致,便于有序遍历和操作。 |
| TreeSet | 基于红黑树(Red-Black Tree,一种自平衡二叉搜索树) | TreeSet不允许存储重复元素,并且元素按照自然顺序或自定义顺序排序, 不允许存储 null值(除非使用自定义比较器) | TreeSet基于红黑树实现,不需要预先分配内存空间,因此没有固定的初始容量。每次添加元素时,动态调整红黑树的结构以保持平衡。 | 否 | 基于红黑树实现,能够自动对元素进行排序,适合需要维护有序集合的场景,插入、删除和查找操作的时间复杂度为 O(log n)。此外,它支持范围查询和获取最小/最大值等操作,功能强大且高效。 |
| CopyOnWriteArraySet | 基于写时复制(Copy-On-Write)机制实现 | 线程安全,所有写操作都通过加锁保证线程安全,读操作不需要加锁。 | 与CopyOnWriteArrayList相同,每次写操作都会创建一个新的数组副本。新数组的容量根据实际需求动态调整。如果新数组容量不足,则扩容为当前容量的 1.5 倍。 | 是 | 基于CopyOnWriteArrayList实现,提供线程安全的集合操作,特别适合高读低写的并发场景,读操作无需加锁且不会抛出ConcurrentModificationException。此外,它通过写时复制机制确保数据一致性,避免了传统集合在多线程环境下的并发修改问题。 |
| ConcurrentSkipListSet | 基于 跳表(Skip List) 实现 | 不允许存储重复元素,且不允许存储null值,支持元素的有序性和高效的并发操作 | 基于跳表实现,不需要预先分配内存空间,因此没有固定的初始容量。每次添加元素时,动态调整跳表的结构以保持平衡。 | 是 | 提供高度并发的有序集合操作,基于跳表结构实现,支持高效的并发插入、删除和查找操作,时间复杂度为 O(log n)。此外,它在多线程环境下表现出色,无需锁整个数据结构即可保证线程安全,适合高并发场景下的有序集合需求。 |
| EnumSet | 基于 位向量(bit vector) 实现 | 性能极高,且内存占用非常低。 不允许存储null值,且只能存储同一枚举类型的元素 | EnumSet不需要动态扩容,因为枚举类型的常量数量是固定的。 | 否 | 专门为枚举类型设计,内存占用极小且性能高效,适合操作枚举常量集合的场景。此外,它强制只能包含指定枚举类型的元素,确保了类型安全和集合的语义清晰性。 |
| Collections.synchronizedSet | 通过包装现有的 Set实现类(如HashSet或TreeSet)来提供线程安全的功能。它内部维护了一个 mutex对象(锁对象),所有方法都通过synchronized(mutex)实现同步 | 为所有方法添加同步锁(synchronized)来保证线程安全 | 取决于其包装的集合 | 是 | 现有集合提供线程安全的包装,适合在多线程环境中使用,而无需替换原有集合实现。此外,它通过同步方法确保操作的安全性,同时允许开发者手动控制同步块以优化性能。 |
| PriorityQueue | PriorityQueue的底层是一个Object[]数组,用于存储元素。使用堆(Heap)数据结构实现,通常是一个最小堆(Min-Heap)或最大堆(Max-Heap) | PriorityQueue允许存储元素,并且元素按照自然顺序或自定义顺序排序,不允许存储null 值。 | 如果当前容量小于64,则新容量 = 旧容量 * 2。如果当前容量大于等于64,则新容量 = 旧容量 * 1.5。 | 否 | PriorityQueue的优点是基于堆结构实现,能够高效地维护元素的优先级顺序,插入和删除操作的时间复杂度为O(log n),适合需要动态处理优先级队列的场景。此外,它自动对元素进行排序(可自定义比较规则),简化了优先级管理的复杂性。 |
| ArrayDeque | ArrayDeque的底层是一个Object[]数组,用于存储元素。使用两个指针(head和tail)来标记队列的头部和尾部 | 允许在队列的两端插入和删除元素,支持栈(LIFO)和队列(FIFO)的操作,不允许存储null值 | 默认初始容量为 16,当元素数量超过当前容量时,ArrayDeque会自动扩容,新容量 = 旧容量 * 2。 | 否 | 基于数组实现的双端队列,支持在两端高效地进行插入和删除操作,时间复杂度为O(1),适合需要频繁从两端操作数据的场景。此外,它比LinkedList更节省内存,并且在大多数情况下性能更优,适合作为栈或队列的实现选择。 |
| ConcurrentLinkedQueue | 基于链表实现的无界线程安全队列 | 采用非阻塞算法(CAS,Compare-And-Swap)实现高并发场景下的线程安全,无锁设计避免了线程阻塞,吞吐量高。遵循先进先出(FIFO)原则 | 没有显式扩容机制 | 是 | 基于无锁算法实现的线程安全队列,提供高效的并发性能,适合高并发场景下的非阻塞操作。此外,作为FIFO队列,它支持原子性的插入和移除操作,确保数据一致性的同时避免了传统锁机制带来的性能开销。 |
| BlockingQueue | Java并发包(java.util.concurrent)中定义的接口,代表一个线程安全的队列,支持阻塞操作 | 主要用于生产者 - 消费者模型,允许线程在队列满或空时自动阻塞或等待,直到条件满足 | 无 | 是 | 提供线程安全的阻塞队列,支持生产者-消费者模式下的高效并发操作,在队列为空或满时能够自动阻塞线程,简化多线程编程复杂性。此外,它有多种实现(如ArrayBlockingQueue、LinkedBlockingQueue),适用于不同场景,确保灵活且高效的资源管理。 |
| ArrayBlockingQueue | 内部使用Object[] items存储元素,通过两个指针putIndex(入队位置)和takeIndex(出队位置)实现循环数组 | 先进先出(FIFO),有界队列 | 容量在初始化时固定,无法动态扩展 | 是 | 基于固定大小数组实现的阻塞队列,提供线程安全的FIFO操作,适合需要限制队列容量以避免无界增长的场景。此外,它的性能较高且内存占用固定,通过阻塞机制简化了生产者-消费者模型中的同步问题。 |
| LinkedBlockingQueue | 基于链表实现的可选有界阻塞队列 | 先进先出,初始化时可指定容量,若未指定则默认为Integer.MAX_VALUE | 初始化时指定容量,队列满后无法动态扩容,若未指定容量(即capacity = Integer.MAX_VALUE),理论上可无限添加元素(受内存限制) | 是 | 线程安全且高效,适合高并发场景。提供丰富的阻塞和超时方法,便于实现复杂的线程间协作。可选容量上限,灵活性较高。 |
| PriorityBlockingQueue | 基于优先级堆实现的无界阻塞队列 | 元素按自然顺序(Comparable)或自定义Comparator排序 | 当插入元素时,若当前数组已满,触发扩容。若当前容量 < 64,新容量 = 旧容量 * 2 + 2;若当前容量 ≥ 64,新容量 = 旧容量 * 1.5。 | 是 | 线程安全且高效,适合高并发场景。支持优先级排序,便于实现任务调度或资源分配。动态扩展容量,灵活性较高。 |
| SynchronousQueue | 无存储结构 | 本身不存储元素,每个插入操作必须等待一个对应的移除操作 | 不存储元素,容量始终为 0,无需扩容 | 是 | 线程安全且高效,适合直接传递模型。没有存储开销,性能较高。简化了生产者和消费者之间的交互逻辑。 |
| DelayQueue | 基于优先级堆实现的无界阻塞队列 | 元素按延迟时间排序,最早到期的元素优先出队,元素必须实现 Delayed 接口,指定延迟时间 | 当插入元素时,若当前数组已满,触发扩容,与PriorityQueue相同,容量扩展为原大小的 50% | 是 | 线程安全且高效,适合高并发场景。支持延迟任务调度,便于实现任务延时执行。动态扩展容量,灵活性较高。 |
| HashMap | 一个数组 + 链表/红黑树的组合数据结构 | 允许存储键值对,并且键和值都可以为null,不保证元素的顺序 | 元素数量超过当前容量与负载因子的乘积时,会自动扩容,新容量 = 旧容量 * 2 | 否 | 基于哈希表实现,提供平均时间复杂度为O(1) 的快速键值对插入、删除和查找操作,适合需要高效数据检索的场景。此外,它允许存储 null 键和 null 值,并且底层动态扩容机制简化了容量管理。 |
| LinkedHashMap | 底层是一个HashMap,同时维护了一个双向链表 | 不仅具有HashMap的快速查找特性,还保留了元素的插入顺序或访问顺序。允许存储键值对,键和值都可以为null | 完全继承自HashMa | 否 | 继承了HashMap的高效键值对操作(平均时间复杂度 O(1)),同时通过双向链表维护元素的顺序,支持按插入顺序或访问顺序迭代。此外,它非常适合实现缓存机制(如 LRU 缓存),在需要有序性和高性能的场景中表现优异。 |
| TreeMap | SortedMap接口的实现类,基于红黑树(Red-Black Tree)实现的,一种自平衡二叉搜索树 | 不允许存储重复键,且键不允许为null(除非使用自定义比较器),并且键按照自然顺序或自定义顺序排序,插入、删除和查找操作的时间复杂度为O(log n) | 基于红黑树实现,不需要预先分配内存空间 | 否 | 基于红黑树实现,能够自动对键进行排序,支持有序键值对存储和范围查询,插入、删除、查找操作的时间复杂度为O(log n)。此外,它提供了获取最小键、最大键及键的前驱和后继等功能,适合需要有序映射的场景。 |
| ConcurrentHashMap | JDK 1.8:使用数组 + 链表 + 红黑树的结构 | 高并发环境下提供了高效的读写操作,适合多线程场景。基于分段锁(Segment Locking)或 CAS(Compare-And-Swap)机制实现。它允许存储键值对,键和值都不允许为null | 当元素数量超过当前容量与负载因子的乘积时,ConcurrentHashMap 会自动扩容,新容量 = 旧容量 * 2 | 是 | 提供高度线程安全的并发哈希表实现,通过分段锁或无锁机制确保高并发环境下的性能,适合多线程场景下的键值对存储。此外,它支持高效的读操作且无需加锁,同时提供了丰富的原子操作方法(如 computeIfAbsent),简化了并发编程复杂性。 |
| Hashtable | 基于哈希表实现,底层是一个Entry<?,?>[]数组,每个Entry是一个键值对 | 是 Java 中一个古老的集合类,允许存储键值对,键和值都不允许为null,它不保证元素的顺序,线程安全,但性能较低 | 元素数量超过当前容量与负载因子的乘积时,会自动扩容,新容量 = 旧容量 * 2 + 1 | 是 | 线程安全的哈希表实现,所有方法均同步,适合早期 Java 版本中的多线程环境。此外,它不允许存储 null 键或 null 值,确保键值对的完整性,适合需要严格数据约束的场景。 |
| WeakHashMap | 底层是一个Entry<?,?>[]数组,每个Entry是一个键值对 | 键是弱引用,当键不再被强引用时,垃圾回收器会自动回收这些键值对 | 当元素数量超过当前容量与负载因子的乘积时,会自动扩容,新容量 = 旧容量 * 2 | 否 | 基于弱引用实现键值对存储,当键不再被外部引用时,键值对可被垃圾回收机制自动清理,适合需要管理临时对象或避免内存泄漏的场景。此外,它在处理缓存或映射关系时能够动态释放资源,降低了手动管理内存的复杂性。 |
| EnumMap | 基于枚举常量的顺序实现,底层是一个Object[]数组,数组的索引对应枚举常量的顺序 | 专门用于枚举类型的高效集合类,性能极高,且内存占用非常低。EnumMap允许存储键值对,键必须是同一枚举类型的常量,值可以是任意类型 | 不需要动态扩容,因为枚举类型的常量数量是固定的 | 否 | 专为枚举类型键设计,内部使用数组实现,提供接近 O(1) 的高效存取性能,且内存占用极小。此外,它自动按照枚举常量的声明顺序存储键值对,确保迭代顺序一致性,适合需要操作枚举类型映射的场景。 |
| IdentityHashMap | 底层是一个Object[]数组,数组的每个元素交替存储键和值 | 基于对象引用相等性(==)而不是对象值相等性(equals)实现的哈希表,键和值都可以为null,使用System.identityHashCode来计算哈希值,用开放地址法(Open Addressing)解决哈希冲突 | 元素数量超过当前容量与负载因子的乘积时,会自动扩容,新容量 = 旧容量 * 2 | 否 | 基于引用相等性(==)而非equals方法比较键和值,适合需要精确控制对象身份的场景,避免了重写 equals和hashCode方法带来的影响。此外,它在处理循环引用或特殊对象映射时表现优异,确保数据结构的稳定性和准确性。 |
| Collections.synchronizedMap | 通过包装现有的Map实现类来提供线程安全的功能 | 为其包装现有的Map实现类的所有方法添加同步锁,从而保证线程安全,性能较低,在高并发场景下,可能会成为性能瓶颈 | 取决于包装的实现类 | 是 | 为现有 Map 提供线程安全的包装,适合在多线程环境中使用,而无需替换原有实现。此外,它通过同步方法确保操作的安全性,并允许开发者手动控制同步块以优化性能,灵活性较高。 |
一、基础了解
1.1、概念
Java集合框架(Java Collections Framework)是Java标准库中用于存储和操作数据集合的核心组件。它提供了一套丰富的接口、实现类和算法,用于处理各种数据结构(如列表、集合、映射等)。
1.2、体系
1.2.1、概述
Java 集合体系的核心接口和类位于java.util包中,主要分为两大类:
Collection:存储单值元素的集合,所有单值集合类的根接口,定义了集合的基本操作。- 核心方法:
add(E e):添加元素。remove(Object o):删除元素。size():返回集合大小。iterator():返回迭代器。
- 子类:
List:有序集合,允许重复元素。Set:无序集合,不允许重复元素。Queue:队列,支持先进先出(FIFO)或优先级排序。
- 核心方法:
Map:存储键值对的集合,键唯一(基于equals()和hashCode()方法判断键是否重复),值可以重复。- 核心方法:
put(K key, V value):添加键值对。get(Object key):根据键获取值。remove(Object key):删除键值对。keySet():返回所有键的集合。values():返回所有值的集合。
- 核心方法:
1.2.2、图解

1.3、适用场景总结
| 集合类型 | 特点 | 适用场景 |
|---|---|---|
ArrayList | 动态数组,支持随机访问,插入删除较慢。 | 需要频繁访问元素的场景。 |
LinkedList | 双向链表,插入删除较快,随机访问较慢。 | 需要频繁插入删除元素的场景。 |
HashSet | 基于哈希表,元素无序,不允许重复。 | 需要快速查找且不关心元素顺序的场景。 |
TreeSet | 基于红黑树,元素有序,不允许重复。 | 需要元素排序的场景。 |
HashMap | 基于哈希表,键值对无序,允许 null 键和值。 | 需要快速查找键值对的场景。 |
TreeMap | 基于红黑树,键值对有序。 | 需要键值对排序的场景。 |
PriorityQueue | 基于堆,元素按优先级排序。 | 需要优先级队列的场景。 |
二、List 接口及其实现类
2.1、概念
List是Java集合框架中的一个核心接口,表示有序的集合(也称为序列)。List允许存储重复的元素,并且可以通过索引访问元素。
- 特点
- 有序性:元素按照插入顺序存储,可以通过索引访问。
- 允许重复元素:可以存储多个相同的元素。
- 允许 null 元素:可以存储 null 值。
- 核心方法
add(E e):在列表末尾添加元素。add(int index, E element):在指定位置插入元素。get(int index):获取指定位置的元素。remove(int index):删除指定位置的元素。set(int index, E element):替换指定位置的元素。size():返回列表的大小。iterator():返回列表的迭代器。
- 核心实现类:
- 非线程安全:
ArrayList:基于动态数组实现,支持随机访问。LinkedList:基于双向链表实现,支持高效插入和删除。
- 线程安全
Stack:基于 Vector 实现,表示后进先出(LIFO)的栈。Vector:线程安全的动态数组(已过时,推荐使用 Collections.synchronizedList)。CopyOnWriteArrayList:基于写时复制(Copy-On-Write)机制实现。读操作不加锁,写操作加锁并复制新数组。适合读多写少的场景。
- 非线程安全:
Collections.synchronizedList:通过包装非线程安全的 List 实现线程安全,所有方法都使用synchronized修饰。
2.2、ArrayList详解
2.2.1、概述
ArrayList是 Java 中最常用的集合类之一,它基于动态数组实现,提供了高效的随机访问和动态扩容能力。ArrayList 允许存储重复元素和 null 值。随机访问(通过索引)的时间复杂度为 O(1)。ArrayList是线程不安全的,如果多个线程同时修改ArrayList,可能会导致数据不一致或抛出异常。
ArrayList的底层是一个 Object[] 数组,用于存储元素,数组的大小会根据元素的增加动态扩容。- 认初始容量为10,可以通过构造函数指定初始容量(
ArrayList<Integer> list = new ArrayList<>(20); // 初始容量为 20) - 动态扩容,当元素数量超过当前数组容量时,ArrayList 会自动扩容。
- 间复杂度:
- 随机访问:
O(1)(通过索引直接访问)。 - 插入/删除:
- 在末尾插入/删除:
O(1)。 - 在中间或开头插入/删除:
O(n)(需要移动元素)。
- 在末尾插入/删除:
- 搜索:
O(n)(需要遍历数组)。
- 随机访问:
2.2.2、扩容机制详解
1、机制
- 新容量 = 旧容量 + 旧容量 / 2(即扩容为原来的 1.5 倍)。
- 如果扩容后的容量仍然不足,则直接使用所需的最小容量。
- 将旧数组中的元素复制到新数组中。
int newCapacity = oldCapacity + (oldCapacity >> 1); // 1.5倍扩容
2、详解
- 触发条件:当调用
add()方法添加元素时,如果当前元素数量等于数组容量,就会触发扩容。
扩容过程
- 检查是否需要扩容:
if (size + 1 > elementData.length) {
grow(size + 1); // 扩容
}
- 计算新容量:
int newCapacity = oldCapacity + (oldCapacity >> 1); // 1.5 倍扩容
- 如果新容量仍然不足,则直接使用所需的最小容量:
if (newCapacity - minCapacity < 0) {
newCapacity = minCapacity;
}
- 如果新容量超过最大数组大小(Integer.MAX_VALUE - 8),则抛出 OutOfMemoryError。
- 创建新数组并复制元素.
2.2.3、常用方法
add(E e):在末尾添加元素。add(int index, E element):在指定位置插入元素。remove(int index):删除指定位置的元素。remove(Object o):删除指定元素(首次出现)。get(int index):获取指定位置的元素。set(int index, E element):修改指定位置的元素。size():返回元素数量。isEmpty():判断是否为空。contains(Object o):判断是否包含指定元素。clear():清空所有元素。
2.2.4、使用场景
1、适用场景
- 频繁随机访问:ArrayList 基于数组实现,支持通过索引快速访问元素,适合需要频繁读取数据的场景。
- 数据量较小或相对固定:如果数据量较小,ArrayList 的性能优于 LinkedList;当数据量相对固定,可以通过指定初始容量避免频繁扩容。
- 末尾插入/删除操作:在末尾插入或删除元素时,ArrayList 的性能很高(
O(1))。
2、不适用场景
- 频繁在中间或开头插入/删除:在中间或开头插入/删除元素时,ArrayList 需要移动大量元素,性能较差(O(n))。此时更适合使用 LinkedList。
- 数据量巨大且频繁扩容:如果数据量巨大且频繁扩容,ArrayList 的性能会受到影响。可以通过预先指定较大的初始容量来优化。
2.3、LinkedList详解
2.3.1、概述
LinkedList是 Java 中一个常用的集合类,它是List 接口的实现类,基于双向链表实现。与 ArrayList 不同,LinkedList在插入和删除操作上具有更高的效率,但在随机访问上性能较差。LinkedList 允许存储重复元素和 null 值。插入和删除操作时的间复杂度为 O(1)。随机访问的时间复杂度为 O(n)。并且是线程不安全的。
- LinkedList 的底层是一个双向链表,每个节点(Node)包含三个部分:
- 前驱节点(prev)
- 当前元素(item)
- 后继节点(next)
- 节点定义:
private static class Node<E> {
E item;
Node<E> prev;
Node<E> next;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.prev = prev;
this.next = next;
}
}
- 动态扩容
- LinkedList 不需要预先分配内存空间,因此没有固定的初始容量。
- 每次添加元素时,动态创建新节点并链接到链表中。
- 由于链表的特性,LinkedList 不需要像 ArrayList 那样进行扩容操作。
- 时间复杂度
- 插入/删除:
- 在开头或末尾插入/删除:
O(1)。 - 在中间插入/删除:
O(n)(需要遍历链表找到指定位置)。
- 在开头或末尾插入/删除:
- 随机访问:
O(n)(需要从头或尾遍历链表)。 - 搜索:
O(n)(需要遍历链表)。
- 插入/删除:
2.3.2、常用方法
add(E e):在末尾添加元素。addFirst(E e):在开头添加元素。addLast(E e):在末尾添加元素。add(int index, E element):在指定位置插入元素。remove():删除并返回第一个元素。removeFirst():删除并返回第一个元素。removeLast():删除并返回最后一个元素。remove(int index):删除指定位置的元素。remove(Object o):删除指定元素(首次出现)。get(int index):获取指定位置的元素。getFirst():获取第一个元素。getLast():获取最后一个元素。set(int index, E element):修改指定位置的元素。size():返回元素数量。isEmpty():判断是否为空。contains(Object o):判断是否包含指定元素。clear():清空所有元素。
2.3.3、使用场景
1、适用场景
- 频繁插入/删除操作:在开头或末尾插入/删除元素时,LinkedList 的性能很高(O(1)), 适合需要频繁修改数据的场景。
- 实现队列或栈:LinkedList 实现了 Deque 接口,可以用作队列(FIFO)或栈(LIFO)。
// 作为队列使用
Queue<Integer> queue = new LinkedList<>();
queue.offer(1); // 入队
queue.poll(); // 出队
// 作为栈使用
Deque<Integer> stack = new LinkedList<>();
stack.push(1); // 入栈
stack.pop(); // 出栈
- 数据量较大且需要动态增长:LinkedList 不需要预先分配内存空间,适合数据量较大且不确定的场景。
2、不适用场景
- 频繁随机访问:
LinkedList的随机访问性能较差(O(n))。如果需要频繁通过索引访问元素,更适合使用ArrayList。 - 内存占用敏感:
LinkedList的每个节点需要额外的内存空间存储前驱和后继指针,内存占用较高。
2.3.4、LinkedList 与 ArrayList 的对比
| 特性 | ArrayList | LinkedList |
|---|---|---|
| 底层数据结构 | 动态数组 | 双向链表 |
| 随机访问性能 | O(1) | O(n) |
| 插入/删除性能 | 末尾 O(1),中间 O(n) | 开头/末尾 O(1),中间 O(n) |
| 内存占用 | 较低(仅存储数据) | 较高(存储数据和指针) |
| 适用场景 | 频繁随机访问,数据量较小 | 频繁插入/删除,数据量较大 |
2.4、Stack详解
2.4.1、概述
Stack 是 Java 中一个基于 Vector 实现的栈数据结构。栈是一种后进先出(LIFO, Last In First Out)的集合,常用于需要后进先出逻辑的场景。Stack 是线程安全的,所有方法都使用synchronized关键字修饰,但由于同步锁的开销,性能较低。现代Java开发中更推荐使用Deque接口的实现类(如 ArrayDeque)来替代 Stack。由于 Stack 继承自Vector,因此具有Vector的所有特性,包括动态扩容和线程安全。
- 底层数据结构:Stack 的底层是一个
Object[]数组,继承自Vector。 - 栈的操作(如
push、pop、peek)都是基于数组实现的。 - 初始容量,默认初始容量为 10(继承自 Vector),可以通过构造函数指定初始容量。
- 动态扩容,Stack 的扩容机制与 Vector 相同
- 时间复杂度
- 入栈(push):
O(1)(在数组末尾添加元素)。 - 出栈(pop):
O(1)(从数组末尾移除元素)。 - 查看栈顶元素(peek):
O(1)(访问数组末尾元素)。 - 搜索元素(search):
O(n)(需要遍历数组)。
- 入栈(push):
2.4.2、常用方法
push(E item):将元素压入栈顶。pop():移除并返回栈顶元素。peek():返回栈顶元素,但不移除。isEmpty():判断栈是否为空。search(Object o):返回元素在栈中的位置(从栈顶开始计数,栈顶为 1)。size():返回栈中元素的数量。clear():清空栈。
2.4.3、使用场景
1、适用场景
- 后进先出(
LIFO)逻辑:栈适合需要后进先出逻辑的场景- 函数调用栈(方法调用和返回)。
- 表达式求值(如括号匹配、逆波兰表达式)。
- 撤销操作(如文本编辑器的撤销功能)。
- 递归算法:递归算法可以使用栈来模拟调用栈。
- 深度优先搜索(
DFS):在图或树的深度优先搜索中,栈用于存储待访问的节点。
2、不适用场景
- 需要先进先出(
FIFO)逻辑:如果需要先进先出逻辑,应该使用队列(如LinkedList或ArrayDeque)。 - 频繁随机访问:栈的设计目标是后进先出,不适合需要频繁随机访问的场景。
2.5、Vector详解
2.5.1、概述
Vector 是 Java 中一个古老的集合类,它是 List 接口的实现类,基于动态数组实现。Vector 与 ArrayList 类似,但它是线程安全的。Vector允许存储重复元素和 null 值。支持随机访问(通过索引),访问时间复杂度为 O(1)。并且线程安全,所有方法都使用 synchronized 关键字修饰。由于同步锁的开销,性能较低,现代 Java 开发中更推荐使用 ArrayList 、Collections.synchronizedList或者CopyOnWriteArrayList。
- 底层数据结构:Vector 的底层是一个 Object[] 数组,用于存储元素。数组的大小会根据元素的增加动态扩容。
- 初始容量:默认初始容量为 10。可以通过构造函数指定初始容量和扩容增量。
Vector<Integer> vector = new Vector<>(20); // 初始容量为 20
Vector<Integer> vector = new Vector<>(20, 10); // 初始容量为 20,扩容增量为 10
- 动态扩容:当元素数量超过当前数组容量时,Vector 会自动扩容。
- 时间复杂度
- 随机访问:
O(1)(通过索引直接访问)。 - 插入/删除:
- 在末尾插入/删除:
O(1)。 - 在中间或开头插入/删除:
O(n)(需要移动元素)。
- 在末尾插入/删除:
- 搜索:
O(n)(需要遍历数组)。
- 随机访问:
2.5.2、扩容机制详解
1、机制
- 如果指定了扩容增量(
capacityIncrement),则新容量 = 旧容量 + 扩容增量。 - 如果未指定扩容增量,则新容量 = 旧容量 * 2。
- 如果扩容后的容量仍然不足,则直接使用所需的最小容量。
- 将旧数组中的元素复制到新数组中。
int newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity);
2、详解
- 扩容触发条件:当调用
add()方法添加元素时,如果当前元素数量等于数组容量,就会触发扩容。
扩容过程
- 检查是否需要扩容:
if (elementCount + 1 > elementData.length) {
grow(elementCount + 1); // 扩容
}
- 计算新容量:
int newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity);
- 如果新容量仍然不足,则直接使用所需的最小容量:
if (newCapacity - minCapacity < 0) {
newCapacity = minCapacity;
}
- 如果新容量超过最大数组大小(Integer.MAX_VALUE - 8),则抛出
OutOfMemoryError。 - 创建新数组并复制元素:
2.5.3、Vector 的常用方法
add(E e):在末尾添加元素。add(int index, E element):在指定位置插入元素。remove(int index):删除指定位置的元素。remove(Object o):删除指定元素(首次出现)。get(int index):获取指定位置的元素。set(int index, E element):修改指定位置的元素。size():返回元素数量。isEmpty():判断是否为空。contains(Object o):判断是否包含指定元素。clear():清空所有元素。
2.5.4、使用场景
1、适用场景
- 多线程环境:Vector 是线程安全的,适合在多线程环境下使用。如果不需要线程安全,建议使用 ArrayList。
- 频繁随机访问:Vector 基于数组实现,支持通过索引快速访问元素,适合需要频繁读取数据的场景。
- 数据量较小或相对固定:如果数据量较小,Vector 的性能可以接受,如果数据量相对固定,可以通过指定初始容量避免频繁扩容。
2、不适用场景
- 单线程环境:在单线程环境下,
Vector的性能不如ArrayList,因为所有方法都使用了synchronized关键字。 - 频繁在中间或开头插入/删除:在中间或开头插入/删除元素时,
Vector需要移动大量元素,性能较差(O(n))。此时更适合使用LinkedList。 - 数据量巨大且频繁扩容:如果数据量巨大且频繁扩容,
Vector的性能会受到影响。可以通过预先指定较大的初始容量来优化。
2.5.5、Vector和ArrayList对比
| 特性 | Vector | ArrayList |
|---|---|---|
| 线程安全 | 是 | 否 |
| 性能 | 较低(同步锁开销 | 较高 |
| 扩容机制 | 指定增量或双倍扩容 | 1.5 倍扩容 |
| 适用场景 | 多线程环境 | 单线程环境 |
2.6、CopyOnWriteArrayList详解
2.6.1、概述
CopyOnWriteArrayList是 java.util.concurrent 包中的一个线程安全集合类,也是 Java 中一个线程安全的 List 实现类,基于写时复制(Copy-On-Write)机制实现,每次修改操作(如添加、删除、修改)都会创建一个新的数组副本。 CopyOnWriteArrayList适合读多写少的场景,读操作性能极高,写操作性能较低,在高并发环境下提供了高效的读取性能。
- 线程安全:
CopyOnWriteArrayList是线程安全的,所有写操作都通过加锁保证线程安全。读操作不需要加锁,因此性能极高。 - 写时复制机制:每次写操作(如添加、删除、修改)都会创建一个新的数组副本,写操作完成后,将新数组替换旧数组,读操作始终访问当前数组,因此不需要加锁。
- 扩容机制:
CopyOnWriteArrayList的扩容机制与ArrayList类似,每次写操作都会创建一个新的数组副本。新数组的容量根据实际需求动态调整。如果新数组容量不足,则扩容为当前容量的 1.5 倍。 - 时间复杂度:
- 读操作:
O(1)(直接访问数组元素)。 - 写操作:
O(n)(需要复制整个数组)。 - 搜索操作:
O(n)(需要遍历数组)。
- 读操作:
2.6.2、常用方法
add(E e):在列表末尾添加元素。add(int index, E element):在指定位置插入元素。remove(int index):删除指定位置的元素。remove(Object o):删除指定元素(首次出现)。get(int index):获取指定位置的元素。set(int index, E element):修改指定位置的元素。size():返回元素数量。isEmpty():判断是否为空。contains(Object o):判断是否包含指定元素。clear():清空所有元素。
2.6.3、使用场景
1、适用场景
- 读多写少
- 缓存系统。
- 配置管理。
- 事件监听器列表。
- 高并发读取:读操作不需要加锁,性能极高,适合高并发读取场景。
- 数据一致性要求高:写操作会创建新的数组副本,读操作始终访问旧数组,因此可以保证数据的一致性。
2、不适用场景
- 写多读少:写操作性能较低,适合写多读少的场景。
- 数据量巨大:每次写操作都需要复制整个数组,如果数据量巨大,可能会导致内存占用过高。
- 实时性要求高:写操作会创建新的数组副本,读操作可能无法立即看到最新的修改。
2.7、Collections.synchronizedList详解
2.7.1、概述
Collections.synchronizedList是java.util.Collections类中的一个静态方法。它接收一个 List 对象作为参数,并返回一个线程安全的同步列表。所有对同步列表的操作都是线程安全的,因为方法内部使用了 synchronized 关键字。适用于需要在多线程环境下共享和修改 List 的场景。
2.7.2、底层实现
Collections.synchronizedList 并不直接实现 List 接口,而是通过包装现有的 List 实现类(如 ArrayList 或 LinkedList)来提供线程安全的功能。它内部维护了一个 mutex 对象(锁对象),所有方法都通过 synchronized(mutex) 实现同步。
2.7.3、扩容机制
Collections.synchronizedList 的扩容机制取决于被包装的 List 实现类。如果包装的是 ArrayList,则扩容机制与 ArrayList 相同(1.5 倍扩容)。如果包装的是LinkedList,则不需要扩容(基于链表实现)。
2.7.4、线程安全问题
- 基本操作线程安全:
Collections.synchronizedList保证了单个方法的线程安全。
synchronizedList.add(1); // 线程安全
synchronizedList.remove(0); // 线程安全
- 复合操作非线程安全:如果需要执行复合操作(如遍历列表并修改),仍然需要手动加锁。
synchronized (synchronizedList) {
for (Integer item : synchronizedList) {
if (item == 1) {
synchronizedList.remove(item);
}
}
}
2.7.5、使用场景
1、适合的场景
- 多线程环境:当多个线程需要共享和修改同一个 List 时,可以使用
Collections.synchronizedList来保证线程安全。 - 简单的同步需求:如果只需要简单的线程安全功能,而不需要复杂的并发控制(如分段锁或读写锁),
Collections.synchronizedList是一个简单易用的选择。 - 兼容现有代码:如果现有代码已经使用了
ArrayList或LinkedList,可以通过Collections.synchronizedList快速实现线程安全,而无需修改大量代码。
2、不适合的场景
- 高并发场景:在高并发场景下,
Collections.synchronizedList的性能较差,因为所有操作都需要竞争同一把锁。此时更适合使用CopyOnWriteArrayList或ConcurrentLinkedQueue。 - 需要高性能读操作:如果读操作远多于写操作,
Collections.synchronizedList的性能不如CopyOnWriteArrayList。 - 需要复杂的并发控制:如果需要更细粒度的并发控制(如读写锁或分段锁),
Collections.synchronizedList无法满足需求。
三、Set接口及其实现类
3.1、概念
Set是Java集合框架中的一个核心接口,继承自Collection接口,但没有定义额外的方法。它表示一个不包含重复元素的集合,因此每个元素在Set中只能出现一次。Set 的实现类通过不同的底层数据结构来保证唯一性。
- 特点
- 唯一性:集合中不能包含相同的元素(通过 equals() 和 hashCode() 判断)。
- 无序性:大多数 Set 实现类不保证元素的顺序(LinkedHashSet 和 TreeSet 除外)。
- 允许 null 元素:大多数 Set 实现类允许存储 null 值(TreeSet 除外)。
- 核心方法
add(E e):向集合中添加元素。remove(Object o):从集合中删除元素。contains(Object o):判断集合是否包含指定元素。size():返回集合的大小。iterator():返回集合的迭代器。
- 主要实现类
- 线程安全
CopyOnWriteArraySet:基于写时复制机制实现,线程安全,适合读多写少。ConcurrentSkipListSet:基于并发跳表(ConcurrentSkipListMap的键集合)实现。
- 非线程安全
HashSet:基于哈希表(HashMap 的键集合)实现,元素无序,查找速度快。LinkedHashSet:基于哈希表 + 双向链表实现,元素按插入顺序排列。TreeSet:基于红黑树(NavigableMap 的键集合)实现。元素有序,查找速度较慢。EnumSet:基于位向量实现,专为枚举设计,性能极高。
- 线程安全
Collections.synchronizedSet:提供对现有 Set 实现的线程安全包装。使用同步块来保证线程安全性。
3.2、HashSet详解
3.2.1、概述
HashSet是Java中最常用的集合类之一,它是 Set 接口的实现类,基于哈希表(HashMap)实现。HashSet不允许存储重复元素,并且不保证元素的顺序。允许存储 null 值。线程不安全,如果需要线程安全,可以使用Collections.synchronizedSet 或 ConcurrentHashMap.newKeySet()。
- 底层数据结构:
HashSet的底层是一个HashMap,元素存储在HashMap的键中,值是一个固定的PRESENT对象。
private transient HashMap<E, Object> map;
private static final Object PRESENT = new Object();
- 初始容量和负载因子:默认初始容量为 16,默认负载因子为 0.75,可以通过构造函数指定初始容量和负载因子。
- 动态扩容:当元素数量超过当前容量与负载因子的乘积时,HashSet 会自动扩容。
- 间复杂度
- 添加元素:O(1)(平均情况下)
- 删除元素:O(1)(平均情况下)
- 查找元素:O(1)(平均情况下)
- 遍历元素:O(n)
3.2.2、扩容机制详解
- 扩容触发条件:当元素数量超过 当前容量 * 负载因子时,HashSet 会自动扩容。
扩容过程
- 计算新容量: 新容量 = 旧容量 * 2
int newCapacity = oldCapacity << 1;
- 创建新哈希表:
Node<K,V>[] newTable = new Node[newCapacity];
- 将旧哈希表中的元素重新哈希到新哈希表中:
for (Node<K,V> e : oldTable) {
while (e != null) {
Node<K,V> next = e.next;
int hash = hash(e.key);
int i = indexFor(hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
3.2.3、常用方法
add(E e):添加元素。remove(Object o):删除指定元素。contains(Object o):判断是否包含指定元素。size():返回元素数量。isEmpty():判断是否为空。clear():清空所有元素。
3.2.4、使用场景
1、适用场景
- 去重:
HashSet不允许存储重复元素,适合需要去重的场景。 - 快速查找:
HashSet基于哈希表实现,查找性能极高(O(1)),适合需要频繁查找元素的场景。 - 数据量较大:
HashSet的扩容机制和哈希表特性使其适合存储大量数据。
2、不适用场景
- 需要有序集合:
HashSet不保证元素的顺序,如果需要有序集合,可以使用LinkedHashSet或TreeSet。 - 频繁插入和删除:如果频繁插入和删除元素,可能会导致哈希冲突和扩容,影响性能。
- 内存占用敏感:
HashSet的底层是HashMap,需要额外的内存存储键值对,内存占用较高。
3.3、LinkedHashSet详解
3.3.1、概述
LinkedHashSet 是 Java 中一个基于链表和哈希表实现的集合类,它是HashSet 的子类,同时实现了Set接口。LinkedHashSet不仅具有HashSet的去重和快速查找特性,还保留了元素的插入顺序。LinkedHashSet不允许存储重复元素,且允许存储 null 值。它保留了元素的插入顺序,即遍历顺序与插入顺序一致。LinkedHashSet线程不安全,需要线程安全,可以使用Collections.synchronizedSet。
- 底层数据结构:LinkedHashSet 的底层是一个 LinkedHashMap,结合了哈希表和双向链表的特性,哈希表用于快速查找和去重,双向链表用于维护元素的插入顺序。-- - - 始容量和负载因子:默认初始容量为 16,默认负载因子为 0.75,可以通过构造函数指定初始容量和负载因子。
- 动态扩容:当元素数量超过当前容量与负载因子的乘积时,LinkedHashSet 会自动扩容。
- 扩容过程
- 计算新容量:新容量 = 旧容量 * 2
- 创建新哈希表。
- 将旧哈希表中的元素重新哈希到新哈希表中,并维护双向链表的顺序。
- 时间复杂度:
- 添加元素:O(1)(平均情况下)。
- 删除元素:O(1)(平均情况下)。
- 查找元素:O(1)(平均情况下)。
- 遍历元素:O(n)。
3.3.2、常用方法
add(E e):添加元素remove(Object o):删除指定元素。contains(Object o):判断是否包含指定元素。size():返回元素数量。isEmpty():判断是否为空。clear():清空所有元素。
3.3.3、使用场景
1、适用场景
- 去重且保留插入顺序:
LinkedHashSet不允许存储重复元素,并且保留了元素的插入顺序。 - 需要快速查找和有序遍历: 基于哈希表实现,查找性能极高(
O(1)),同时保留了插入顺序。 - 缓存系统:适合需要去重且保留访问顺序的缓存系统。
2、不适合的场景
- 频繁插入和删除:如果频繁插入和删除元素,可能会导致哈希冲突和扩容,影响性能。
- 内存占用敏感:
LinkedHashSet的底层是LinkedHashMap,需要额外的内存存储链表节点,内存占用较高。
3.4、TreeSet详解
3.4.1、概述
TreeSet是 Java 中一个基于红黑树(Red-Black Tree,一种自平衡二叉搜索树) 实现的集合类,它是 SortedSet接口的实现类。TreeSet不允许存储重复元素,并且元素按照自然顺序或自定义顺序排序。TreeSet 不允许存储 null值(除非使用自定义比较器),并且线程不安全,如果需要线程安全,可以使用 Collections.synchronizedSortedSet。
- 底层数据结构:
TreeSet的底层是一个TreeMap,元素存储在 TreeMap 的键中,值是一个固定的PRESENT对象。 - 元素按照自然顺序(Comparable 接口)或自定义顺序(Comparator 接口)排序。
- 动态扩容:TreeSet 基于红黑树实现,不需要预先分配内存空间,因此没有固定的初始容量。每次添加元素时,动态调整红黑树的结构以保持平衡。
- 时间复杂度
- 添加元素:O(log n)。
- 删除元素:O(log n)。
- 查找元素:O(log n)。
- 遍历元素:O(n)。
3.4.2、常用方法
add(E e):添加元素。remove(Object o):删除指定元素。set.remove(1); // 删除元素 1contains(Object o):判断是否包含指定元素。subSet(E from, E to):返回从 from(包含)到 to(不包含)的子集。headSet(E to):返回小于 to 的子集。tailSet(E from):返回大于等于 from 的子集。size():返回元素数量。isEmpty():判断是否为空。clear():清空所有元素。
3.4.3、使用场景
1、适用场景
- 需要有序集合:TreeSet 保证元素的有序性,适合需要排序的场景。
- 范围查询:TreeSet 提供了高效的范围查询方法(如 subSet、headSet、tailSet)。
- 去重且排序:TreeSet 不允许存储重复元素,并且元素有序,适合需要去重和排序的场景。
2、不适用场景
- 频繁插入和删除:红黑树的平衡调整需要额外开销,频繁插入和删除操作性能较低。
- 内存占用敏感:红黑树需要额外的内存存储节点和指针,内存占用较高。
- 不需要排序:如果不需要排序,使用
HashSet或LinkedHashSet性能更高。
3.5、CopyOnWriteArraySet详解
3.5.1、概述
CopyOnWriteArraySet是 Java 中一个线程安全的 Set 实现类,基于写时复制(Copy-On-Write)机制实现。它是 CopyOnWriteArrayList 的包装类,适合读多写少的场景,在高并发环境下提供了高效的读取性能。线程安全,所有写操作都通过加锁保证线程安全,读操作不需要加锁。
- 线程安全:
CopyOnWriteArraySet是线程安全的,所有写操作都通过加锁保证线程安全。读操作不需要加锁,因此性能极高。 - 写时复制机制:每次写操作(如添加、删除、修改)都会创建一个新的数组副本。写操作完成后,将新数组替换旧数组。读操作始终访问当前数组,因此不需要加锁。
- 初始容量:默认初始容量为 0。可以通过构造函数指定初始容量。
- 扩容机制:
CopyOnWriteArraySet的扩容机制与CopyOnWriteArrayList相同,每次写操作都会创建一个新的数组副本。新数组的容量根据实际需求动态调整。如果新数组容量不足,则扩容为当前容量的 1.5 倍。 - 时间复杂度
- 读操作:O(1)(直接访问数组元素)。
- 写操作:O(n)(需要复制整个数组)。
- 搜索操作:O(n)(需要遍历数组)。
3.5.2、常用方法
add(E e):添加元素。remove(Object o):删除指定元素。contains(Object o):判断是否包含指定元素。size():返回元素数量。isEmpty():判断是否为空。clear():清空所有元素。
3.5.3、使用场景
1、适用场景
- 读多写少:
- 缓存系统。
- 配置管理。
- 事件监听器列表。
- 高并发读取:读操作不需要加锁,性能极高,适合高并发读取场景。
- 数据一致性要求高:写操作会创建新的数组副本,读操作始终访问旧数组,因此可以保证数据的一致性。
2、不适用场景
- 写多读少:写操作性能较低,适合写多读少的场景。
- 数据量巨大每次写操作都需要复制整个数组,如果数据量巨大,可能会导致内存占用过高。
- 实时性要求高:写操作会创建新的数组副本,读操作可能无法立即看到最新的修改。
3.6、ConcurrentSkipListSet 详解
3.6.1、概述
ConcurrentSkipListSet 是 Java 中一个线程安全的 Set 实现类,基于**跳表(Skip List)**实现。它是 SortedSet 接口的实现类,支持元素的有序性和高效的并发操作。ConcurrentSkipListSet是 java.util.concurrent包中的一个线程安全集合类。不允许存储重复元素,且不允许存储 null 值。线程安全,支持高并发操作。
- 底层数据结构:
ConcurrentSkipListSet的底层是一个ConcurrentSkipListMap,元素存储在ConcurrentSkipListMap的键中,值是一个固定的PRESENT对象。 - 排序机制:元素按照自然顺序(
Comparable接口)或自定义顺序(Comparator接口)排序。 - 动态扩容:ConcurrentSkipListSet 基于跳表实现,不需要预先分配内存空间,因此没有固定的初始容量。每次添加元素时,动态调整跳表的结构以保持平衡。
- 时间复杂度
- 添加元素:O(log n)。
- 删除元素:O(log n)。
- 查找元素:O(log n)。
- 遍历元素:O(n)。
3.6.2、跳表详解
跳表(Skip List)是一种概率性数据结构,允许快速查找、插入和删除操作,平均时间复杂度为O(log n)。它通过多层链表实现,每层都是前一层的子集,顶层链表元素最少,底层包含所有元素。
- 多层链表:
- 跳表由多层链表组成,底层包含所有元素,每上一层元素数量减半。
- 每个节点有多个指针,指向同一层的下一个节点和下一层的对应节点。
- 随机层数: 插入新节点时,通过随机算法决定其层数,确保每层元素数量大致减半,维持平衡。
- 查找操作:从顶层开始查找,若当前节点值小于目标值,则向右移动;否则,向下移动,直到找到目标或确定不存在。
- 插入操作:先查找插入位置,然后随机生成层数,在各层链表中插入新节点。
- 删除操作:查找目标节点,从各层链表中移除。
3.6.3、常用方法
add(E e):添加元素。remove(Object o):删除指定元素。contains(Object o):判断是否包含指定元素。subSet(E from, E to):返回从 from(包含)到 to(不包含)的子集。headSet(E to):返回小于 to 的子集。tailSet(E from):返回大于等于 from 的子集。size():返回元素数量。isEmpty():判断是否为空。clear():清空所有元素。
3.6.4、使用场景
1、适用场景
- 需要有序集合:
ConcurrentSkipListSet保证元素的有序性,适合需要排序的场景。 - 高并发环境:
ConcurrentSkipListSet是线程安全的,适合高并发环境下的有序集合操作。 - 范围查询:
ConcurrentSkipListSet提供了高效的范围查询方法(如subSet、headSet、tailSet)。 - 去重且排序:
ConcurrentSkipListSet不允许存储重复元素,并且元素有序,适合需要去重和排序的场景。
2、不适用场景
- 内存占用敏感:跳表需要额外的内存存储索引节点,内存占用较高。
- 不需要排序:如果不需要排序,使用
ConcurrentHashMap.newKeySet()性能更高。
3.7、EnumSet详解
3.7.1、概述
EnumSet是 Java 中一个专门用于枚举类型的高效集合类,它是Set接口的实现类。EnumSet基于位向量(bit vector) 实现,性能极高,且内存占用非常低。 EnumSet不允许存储null值,且只能存储同一枚举类型的元素。基于位向量(bit vector)实现,性能极高,内存占用非常低。线程不安全,如果需要线程安全,可以使用 Collections.synchronizedSet。
- 底层数据结构:
EnumSet的底层是一个位向量(bit vector),每个枚举常量对应位向量中的一个位。 - 初始容量:
EnumSet的容量固定为枚举类型的常量数量。不需要动态扩容,因为枚举类型的常量数量是固定的。 - 动态扩容:
EnumSet不需要动态扩容,因为枚举类型的常量数量是固定的。 - 时间复杂度
- 添加元素:
O(1)。 - 删除元素:
O(1)。 - 查找元素:
O(1)。 - 遍历元素:
O(n)。
- 添加元素:
3.7.2、位向量详解
位向量详解点击查看
位向量(Bit Vector)是一种基于二进制位(bit)的数据结构,用于高效地表示和操作一组布尔值或状态。每个位(bit)可以表示一个状态(0 或 1),常用于集合操作、压缩存储和快速查询等场景。
1、概念
- 二进制位表示:位向量是一个二进制数组,每个位(bit)表示一个状态。例如,一个 8 位的位向量可以表示为:01011010,其中每一位可以是 0 或 1。
- 索引与位的关系:每个索引(index)对应位向量的一个位。例如,索引 0 对应第 0 位,索引 1 对应第 1 位,依此类推。
- 位操作:位向量支持高效的位操作,如
按位与(&)、按位或(|)、按位异或(^)、按位取反(~)等。这些操作可以用于实现集合的交集、并集、差集等。 - 存储方式:位向量通常使用整数类型(如
int或long)来存储。例如,一个long类型可以存储 64 位。
4、EnumSet 中的应用
- 在
EnumSet中,每个枚举常量对应位向量的一个位。如果该位为 1,表示枚举值存在于集合中;如果为 0,则表示不存在。 - 枚举的序号(
ordinal):每个枚举常量都有一个唯一的序号(ordinal),从 0 开始递增。例如,对于枚举enum Color { RED, GREEN, BLUE },RED.ordinal() = 0,GREEN.ordinal() = 1,BLUE.ordinal() = 2。
3.7.3、实现原理
-
存储结构:
EnumSet内部使用一个long类型的变量(64 位)来存储位向量。如果枚举常量的数量超过 64,则会使用多个long变量。每个枚举常量的ordinal值对应位向量的某一位。例如,RED.ordinal() = 0对应位向量的第 0 位。 -
添加元素:添加枚举值时,将对应位设置为 1。例如,添加
RED时,将位向量的第 0 位设置为 1。 -
删除元素:删除枚举值时,将对应位设置为 0。例如,删除
RED时,将位向量的第 0 位设置为 0。 -
判断元素是否存在:检查对应位是否为 1。例如,检查
RED是否存在时,检查位向量的第 0 位是否为 1。 -
集合操作:
- 并集:使用按位或(
|)操作。 - 交集:使用按位与(
&)操作。 - 差集:使用按位与和非(
& ~)操作。
- 并集:使用按位或(
3.7.4、常用方法
EnumSet.of(E e1, E e2, ...):创建一个包含指定元素的EnumSet。EnumSet.allOf(Class<E> elementType):创建一个包含指定枚举类型所有常量的EnumSet。EnumSet.noneOf(Class<E> elementType):创建一个空的EnumSet。EnumSet.range(E from, E to):创建一个包含从from到to范围内所有常量的EnumSet。add(E e):添加元素。remove(Object o):删除指定元素。contains(Object o):判断是否包含指定元素。size():返回元素数量。isEmpty():判断是否为空。clear():清空所有元素。
3.7.5、使用场景
1、适用场景
- 枚举类型集合:
EnumSet专门用于枚举类型,适合存储同一枚举类型的元素。 - 高效集合操作:
EnumSet基于位向量实现,性能极高,适合需要高效集合操作的场景。 - 内存占用敏感:
EnumSet的内存占用非常低,适合内存占用敏感的场景。
2、不适用场景
- 非枚举类型集合:
EnumSet只能存储枚举类型的元素,不适合存储非枚举类型的元素。 - 需要存储
null值:EnumSet不允许存储null值。
3.8、Collections.synchronizedSet详解
3.8.1、概述
Collections.synchronizedSet是Java提供的一个工具方法,用于将普通的Set转换为线程安全的同步集合。它通过包装现有的Set实现类(如HashSet或TreeSet),并为其所有方法添加同步锁,从而保证线程安全,适用于需要在多线程环境下共享和修改 Set 的场景。
- 线程安全:
Collections.synchronizedSet通过为所有方法添加同步锁(synchronized)来保证线程安全。 - 底层实现:
Collections.synchronizedSet并不直接实现 Set 接口,而是通过包装现有的Set实现类(如HashSet或TreeSet)来提供线程安全的功能。它内部维护了一个mutex对象(锁对象),所有方法都通过synchronized(mutex)实现同步。 - 性能:由于所有方法都使用了同步锁,
Collections.synchronizedSet的性能较低。在高并发场景下,可能会成为性能瓶颈。
3.8.2、使用场景
1、适合场景
- 多线程环境:当多个线程需要共享和修改同一个
Set时,可以使用Collections.synchronizedSet来保证线程安全。 - 简单的同步需求:如果只需要简单的线程安全功能,而不需要复杂的并发控制(如分段锁或读写锁),
Collections.synchronizedSet是一个简单易用的选择。 - 兼容现有代码:如果现有代码已经使用了
HashSet或TreeSet,可以通过Collections.synchronizedSet快速实现线程安全,而无需修改大量代码。
2、不适合的场景
- 高并发场景:在高并发场景下,
Collections.synchronizedSet的性能较差,因为所有操作都需要竞争同一把锁。此时更适合使用CopyOnWriteArraySet或ConcurrentHashMap.newKeySet()。 - 需要高性能读操作:如果读操作远多于写操作,
Collections.synchronizedSet的性能不如CopyOnWriteArraySet。 - 需要复杂的并发控制:如果需要更细粒度的并发控制(如读写锁或分段锁),
Collections.synchronizedSet无法满足需求。
四、Queue接口及其实现类
4.1、概念
Queue是Java集合框架中的一个核心接口,表示一种先进先出(FIFO)或优先级排序的队列。Queue接口继承自Collection接口,并定义了一些队列特有的操作(如插入、删除、检查元素等)。
- 特点
- 先进先出(FIFO):元素按照插入顺序排列,最先插入的元素最先被移除。
- 优先级队列:元素按照自然顺序或自定义比较器排序,优先级最高的元素最先被移除。
- 不允许 null 元素:大多数 Queue 实现类不允许存储 null 值。
- 核心方法
add(E e):向队列中添加元素(如果队列已满,抛出异常)。offer(E e):向队列中添加元素(如果队列已满,返回 false)。remove():移除并返回队列头部的元素(如果队列为空,抛出异常)。poll():移除并返回队列头部的元素(如果队列为空,返回 null)。element():返回队列头部的元素(如果队列为空,抛出异常)。peek():返回队列头部的元素(如果队列为空,返回 null)。
- 主要实现类
- 线程安全
ConcurrentLinkedQueue:基于链表实现的无界线程安全队列。BlockingQueue接口:BlockingQueue是Queue的子接口,表示一种支持阻塞操作的队列。以下是BlockingQueue的主要实现类:ArrayBlockingQueue:基于数组实现的有界阻塞队列。LinkedBlockingQueue:基于链表实现的可选有界阻塞队列。PriorityBlockingQueue:基于堆实现的无界阻塞队列,元素按自然顺序或自定义比较器排序。SynchronousQueue:一种不存储元素的阻塞队列,每个插入操作必须等待另一个线程的移除操作。DelayQueue:基于优先级队列实现的无界阻塞队列,元素只有在延迟到期后才能被移除。
- 非线程安全
LinkedList:基于双向链表实现,可以作为队列或双端队列(Deque)使用。PriorityQueue:基于堆(通常是小顶堆)实现,元素按自然顺序或自定义比较器排序。ArrayDeque:基于动态数组实现的双端队列,可以作为队列或栈使用。
- 线程安全
4.2、PriorityQueue详解
4.2.1、概述
PriorityQueue是Java中一个基于优先级堆(Priority Heap)实现的集合类,它是 Queue接口的实现类。PriorityQueue允许存储元素,并且元素按照自然顺序或自定义顺序排序,不允许存储 null 值。但它线程不安全,如果需要线程安全,可以使用PriorityBlockingQueue。
- 底层数据结构:
PriorityQueue的底层是一个Object[]数组,用于存储元素。使用堆(Heap)数据结构实现,通常是一个最小堆(Min-Heap)或最大堆(Max-Heap)。 - 初始容量:默认初始容量为 11。
- 动态扩容:当元素数量超过当前容量时,
PriorityQueue会自动扩容。 - 扩容机制:
- 如果当前容量小于64,则新容量 = 旧容量 * 2。
- 如果当前容量大于等于64,则新容量 = 旧容量 * 1.5。
- 将旧数组中的元素复制到新数组中。
- 时间复杂度
- 添加元素:
O(log n)。 - 删除元素:
O(log n)。 - 查找元素:
O(1)(获取堆顶元素)。 - 遍历元素:
O(n)。
- 添加元素:
4.2.2、常用方法
add(E e):添加元素。poll():删除并返回堆顶元素。peek():返回堆顶元素,但不删除。size():返回元素数量。isEmpty():判断是否为空。clear():清空所有元素。
4.2.3、使用场景
1、适用场景
- 优先级任务调度:PriorityQueue 适合用于优先级任务调度,例如任务队列中高优先级任务优先执行。
Top K问题:适合解决 Top K 问题,例如从大量数据中找出前 K 个最大或最小的元素。- 排序需求:适合需要动态排序的场景,例如实时数据流中的排序。
2、不适用场景
- 频繁插入和删除:如果频繁插入和删除元素,可能会导致堆的调整开销较大,影响性能。
- 内存占用敏感:
PriorityQueue需要额外的内存存储堆结构,内存占用较高。
4.3、ArrayDeque详解
4.3.1、概述
ArrayDeque是Java中一个基于动态数组实现的双端队列(Deque),它是Deque接口的实现类。ArrayDeque支持高效的插入、删除和查找操作,适合用作栈或队列。
ArrayDeque是基于动态数组实现,它允许在队列的两端插入和删除元素,支持栈(LIFO)和队列(FIFO)的操作,不允许存储null值。但它线程不安全,如果需要线程安全,可以使用 Collections.synchronizedDeque。
- 底层数据结构:
ArrayDeque的底层是一个Object[]数组,用于存储元素。使用两个指针(head和tail)来标记队列的头部和尾部。 - 初始容量:默认初始容量为 16。
- 动态扩容:当元素数量超过当前容量时,
ArrayDeque会自动扩容。 - 扩容机制:
- 新容量 = 旧容量 * 2。
- 将旧数组中的元素复制到新数组中。
- 时间复杂度
- 添加元素:
O(1)(平均情况下)。 - 删除元素:
O(1)(平均情况下)。 - 查找元素:
O(1)(获取头部或尾部元素)。 - 遍历元素:
O(n)。
- 添加元素:
4.3.2、常用方法
add(E e):在尾部添加元素。addFirst(E e):在头部添加元素。addLast(E e):在尾部添加元素。remove():删除并返回头部元素。removeFirst():删除并返回头部元素。removeLast():删除并返回尾部元素。peek():返回头部元素,但不删除。peekFirst():返回头部元素,但不删除。peekLast():返回尾部元素,但不删除。size():返回元素数量。isEmpty():判断是否为空。clear():清空所有元素。
4.3.3、使用场景
1、适用场景
- 栈(
LIFO):ArrayDeque可以用作栈,支持push和pop操作。 - 队列(
FIFO):ArrayDeque可以用作队列,支持offer和poll操作。 - 双端队列:
ArrayDeque支持在队列的两端插入和删除元素。 - 缓冲区管理:用于临时存储数据,支持高效的两端操作。
- 算法实现:如广度优先搜索(BFS)、任务调度等需要双端操作的场景。
2、不适用场景
- 频繁插入和删除:如果频繁插入和删除元素,可能会导致数组扩容和复制,影响性能。
- 内存占用敏感:
ArrayDeque需要额外的内存存储数组,内存占用较高。
4.4、ConcurrentLinkedQueue详解
4.4.1、概述
ConcurrentLinkedQueue是Java中一个基于链表实现的无界线程安全队列,属于 java.util.concurrent包。它采用非阻塞算法(CAS,Compare-And-Swap)实现高并发场景下的线程安全,适用于多线程环境下的生产者 - 消费者模型。
- 核心特性
- 无界队列:队列的容量理论上是无界的,元素可以无限添加(受内存限制)。底层基于单向链表实现,每个节点(Node)包含一个元素和指向下一个节点的指针。
- 线程安全:通过 CAS 操作保证线程安全,无需显式加锁,适合高并发场景。支持多线程并发插入(入队)和删除(出队)操作。
- 高性能:无锁设计避免了线程阻塞,吞吐量高。遵循先进先出(FIFO)原则。
- 不支持阻塞操作:若队列为空时调用 poll(),直接返回 null,不会阻塞线程。
- 底层数据结构
- 链表结构:每个节点 Node 包含元素和 volatile 修饰的 next 指针。
- 头尾指针:头指针(head)指向队列的第一个元素。尾指针(tail)指向队列的最后一个元素。头尾指针均为 volatile 修饰,保证多线程可见性。
- 链表结构:每个节点 Node 包含元素和 volatile 修饰的 next 指针。
- 扩容机制:ConcurrentLinkedQueue 没有显式扩容机制。
4.4.2、线程安全原理
ConcurrentLinkedQueue通过 CAS 操作 和 volatile 变量 实现线程安全:
CAS(Compare-And-Swap):CAS是一种无锁原子操作,通过硬件指令(如sun.misc.Unsafe)保证操作的原子性。- 入队(
offer()):- 通过
CAS更新尾节点(tail)的next指针,将新节点追加到链表尾部。 - 如果多个线程同时修改
tail,CAS会确保只有一个线程成功,其他线程重试。
- 通过
- 出队(
poll()):- 通过
CAS更新头节点(head)的item字段为null,并移动头指针。 - 如果多个线程同时出队,
CAS保证只有一个线程成功获取元素。
- 通过
- 入队(
volatile变量head和tail指针均用volatile修饰,确保多线程间的可见性。- 节点的
item和next字段也用volatile修饰,保证修改后对其他线程立即可见。
4.2.3、使用场景
1、适用场景
- 高并发生产者 - 消费者模型:适用于多线程同时生产数据、消费数据的场景(如任务分发系统)。
- 无阻塞任务队列:适合需要高吞吐量且不依赖阻塞操作的系统(如实时数据处理)。
- 替代
synchronized或锁的队列:在需要减少锁竞争的场景下,提供更高的并发性能。
2、不适用场景
- 需要阻塞操作:若队列为空时消费者需要阻塞等待,应使用
BlockingQueue(如LinkedBlockingQueue)。 - 有界队列需求:若需要限制队列容量以防止内存耗尽,应使用有界队列(如 ArrayBlockingQueue)。
4.5、BlockingQueue接口详解
4.5.1、概述
BlockingQueue是Java并发包(java.util.concurrent)中定义的接口,代表一个线程安全的队列,支持阻塞操作。它主要用于生产者 - 消费者模型,允许线程在队列满或空时自动阻塞或等待,直到条件满足。
- 线程安全:所有操作(入队、出队)均保证线程安全。
- 阻塞支持:
- 队列满时入队阻塞:当队列已满,
put()方法会阻塞线程,直到队列有空位。 - 队列空时出队阻塞:当队列为空,
take()方法会阻塞线程,直到队列有元素。
- 队列满时入队阻塞:当队列已满,
- 有界与无界队列:
- 有界队列:需指定容量(如
ArrayBlockingQueue)。 - 无界队列:理论上容量无限(如
LinkedBlockingQueue默认Integer.MAX_VALUE)。
- 有界队列:需指定容量(如
- 公平性选项:部分实现支持公平锁,避免线程饥饿。
4.5.2、常用方法
put(E e):插入元素,若队列满则阻塞线程,直到有空位。take(): 移除并返回头部元素,若队列空则阻塞线程,直到有元素。offer(E e): 插入元素,成功返回 true,队列满时立即返回 false(不阻塞)。poll(): 移除并返回头部元素,队列空时立即返回 null(不阻塞)。offer(E e, long timeout, TimeUnit unit): 插入元素,若队列满则等待指定时间,超时返回 false。poll(long timeout, TimeUnit unit): 移除并返回头部元素,若队列空则等待指定时间,超时返回 null。
4.6、ArrayBlockingQueue详解
4.6.1、概述
ArrayBlockingQueue是Java并发包(java.util.concurrent)中基于数组实现的有界阻塞队列,支持线程安全的生产者 - 消费者模型
- 核心特性
- 有界队列:初始化时需指定固定容量,不可动态扩容。
- 线程安全:通过
ReentrantLock和Condition实现线程安全。 - 阻塞操作:
- 队列满时入队阻塞:
put()方法在队列满时阻塞线程,直到有空位。 - 队列空时出队阻塞:
take()方法在队列空时阻塞线程,直到有元素。
- 队列满时入队阻塞:
- 公平性选项:可配置公平锁(默认非公平),避免线程饥饿。
- 如果构造函数中指定
fair = true,则线程将以 FIFO 的顺序获取锁,确保公平性。 - 如果未指定或
fair = false,则线程获取锁的顺序是非确定性的。
- 如果构造函数中指定
- 先进先出(
FIFO):元素按插入顺序处理。
- 底层数据结构:
- 基于数组:内部使用
Object[] items存储元素。 - 循环队列:通过两个指针
putIndex(入队位置)和takeIndex(出队位置)实现循环数组。
- 基于数组:内部使用
- 扩容机制:容量在初始化时固定,无法动态扩展。防止无限制的资源消耗,适用于需严格控制队列长度的场景(如连接池)。
4.6.2、线程安全原理
- 锁与条件变量
- 单锁机制
- ArrayBlockingQueue 使用一个内部的 ReentrantLock 来保护对队列的访问。
- 所有操作(入队、出队)共享一个 ReentrantLock,从而避免多个线程同时修改队列。
- 条件变量
- 定义了两个条件变量:
notEmpty:用于通知消费者线程队列中有可用元素,队列为空时,消费者线程在此等待。notFull:用于通知生产者线程队列中有可用空间,队列满时,生产者线程在此等待。
- 当队列为空时,消费者线程会被挂起,直到生产者线程插入新元素并唤醒等待的消费者线程。
- 当队列已满时,生产者线程会被挂起,直到消费者线程移除元素并唤醒等待的生产者线程。
- 定义了两个条件变量:
- 单锁机制
- 入队操作(put())
- 流程:
- 获取锁。
- 若队列满,调用
notFull.await()阻塞生产者线程。 - 插入元素到
putIndex位置,更新putIndex(循环递增)。 - 唤醒等待在
notEmpty上的消费者线程。 - 释放锁。
- 代码示例
- 流程:
public void put(E e) throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == items.length)
notFull.await(); // 队列满时阻塞
enqueue(e); // 插入元素
} finally {
lock.unlock();
}
}
private void enqueue(E x) {
items[putIndex] = x;
putIndex = (putIndex + 1) % items.length; // 循环指针
count++;
notEmpty.signal(); // 唤醒消费者
}
- 出队操作(take())
- 流程:
- 获取锁。
- 若队列空,调用
notEmpty.await()阻塞消费者线程。 - 从
takeIndex位置取出元素,更新takeIndex(循环递增)。 - 唤醒等待在 notFull 上的生产者线程。
- 释放锁。
- 代码
- 流程:
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)
notEmpty.await(); // 队列空时阻塞
return dequeue(); // 取出元素
} finally {
lock.unlock();
}
}
private E dequeue() {
E x = (E) items[takeIndex];
items[takeIndex] = null; // 帮助 GC
takeIndex = (takeIndex + 1) % items.length; // 循环指针
count--;
notFull.signal(); // 唤醒生产者
return x;
}
4.6.3、常用方法
put(E e):插入元素,队列满时阻塞线程。take():移除并返回头部元素,队列空时阻塞线程。offer(E e):插入元素,队列满时立即返回false。poll():移除并返回头部元素,队列空时立即返回null。peek():返回头部元素但不移除,队列空时返回null。remainingCapacity():返回队列剩余容量。
4.6.4、适用场景
- 固定资源池:如数据库连接池,限制最大连接数。
- 生产者-消费者模型:控制生产速率,防止内存溢出。
- 流量削峰:在突发流量下缓冲任务,避免系统过载。
4.7、LinkedBlockingQueue详解
4.7.1、概述
LinkedBlockingQueue是Java并发包(java.util.concurrent)中基于链表实现的可选有界阻塞队列,支持高并发的生产者-消费者模型。
- 特性
- 可选有界队列:初始化时可指定容量,若未指定则默认为
Integer.MAX_VALUE(接近无界)。 - 线程安全:通过分离锁(入队锁和出队锁)实现高并发性能。
- 阻塞操作:
- 队列满时入队阻塞:
put()方法在队列满时阻塞线程,直到有空位。 - 队列空时出队阻塞:
take()方法在队列空时阻塞线程,直到有元素。
- 队列满时入队阻塞:
- 先进先出(
FIFO):元素按插入顺序处理。 - 链表结构:基于单向链表实现,动态扩展节点。
- 可选有界队列:初始化时可指定容量,若未指定则默认为
- 底层数据结构
- 链表节点:每个节点包含元素和指向下一个节点的指针。
- 头尾指针
head:指向链表的头部(最早插入的元素)。last:指向链表的尾部(最新插入的元素)。
- 原子计数器:
AtomicInteger记录当前元素数量,保证线程安全
- 扩容机制
- 容量固定:初始化时指定容量,队列满后无法动态扩容。
- 默认无界:若未指定容量(即
capacity = Integer.MAX_VALUE),理论上可无限添加元素(受内存限制)。
4.7.2、线程安全原理
- 分离锁设计
- 入队锁(
putLock):控制插入操作(put()、offer()),用于保护向队列尾部插入元素的操作。 - 出队锁(
takeLock):控制移除操作(take()、poll()),用于保护从队列头部移除元素的操作。 - 分离锁优势:入队和出队操作互不阻塞,显著减少锁竞争。
- 入队锁(
- 条件变量
- 定义了两个条件变量:
notFull:队列满时,生产者线程在此等待(关联putLock)。notEmpty:队列空时,消费者线程在此等待(关联takeLock)。
- 当队列为空时,消费者线程会被挂起,直到生产者线程插入新元素并唤醒等待的消费者线程。
- 当队列已满时,生产者线程会被挂起,直到消费者线程移除元素并唤醒等待的生产者线程。
- 定义了两个条件变量:
- 入队操作(
put())
public void put(E e) throws InterruptedException {
Node<E> node = new Node<>(e);
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
putLock.lockInterruptibly();
try {
while (count.get() == capacity) {
notFull.await(); // 队列满时阻塞
}
enqueue(node); // 插入链表尾部
int c = count.getAndIncrement();
if (c + 1 < capacity) {
notFull.signal(); // 唤醒其他生产者
}
} finally {
putLock.unlock();
}
if (c == 0) {
signalNotEmpty(); // 唤醒消费者
}
}
private void enqueue(Node<E> node) {
last = last.next = node;
}
- 出队操作(
take())
public E take() throws InterruptedException {
final ReentrantLock takeLock = this.takeLock;
final AtomicInteger count = this.count;
takeLock.lockInterruptibly();
try {
while (count.get() == 0) {
notEmpty.await(); // 队列空时阻塞
}
E x = dequeue(); // 移除链表头部
int c = count.getAndDecrement();
if (c > 1) {
notEmpty.signal(); // 唤醒其他消费者
}
return x;
} finally {
takeLock.unlock();
}
}
private E dequeue() {
Node<E> h = head;
Node<E> first = h.next;
h.next = h; // 断开原头节点,帮助 GC
head = first;
E x = first.item;
first.item = null; // 置空以帮助 GC
return x;
}
4.7.3、常用方法
put(E e):插入元素,队列满时阻塞线程。take():移除并返回头部元素,队列空时阻塞线程。offer(E e):插入元素,队列满时立即返回false。poll():移除并返回头部元素,队列空时立即返回null。peek():返回头部元素但不移除,队列空时返回null。remainingCapacity():返回队列剩余容量(无界队列始终返回Integer.MAX_VALUE)。
4.7.4、使用场景
1、适用场景
- 高并发生产者-消费者模型:分离锁设计支持高吞吐量,适合多线程频繁插入和移除操作。
- 任务缓冲池:适用于需要缓冲突发流量的系统(如消息队列)。
- 无界或大容量队列:默认无界,适合任务量不可预测的场景(如日志处理)。
2、不适用场景
- 严格内存控制:默认无界队列可能导致内存耗尽(
OOM)。替代方案:使用有界队列(如ArrayBlockingQueue)。 - 快速随机访问:链表结构访问中间元素的时间复杂度为
O(n),不适合需要随机访问的场景。 - 低延迟要求:动态创建节点和
GC压力可能影响实时性要求高的系统。
4.8、PriorityBlockingQueue详解
4.8.1、概述
PriorityBlockingQueue是Java并发包(java.util.concurrent)中基于优先级堆实现的无界阻塞队列,支持线程安全的生产者 - 消费者模型,元素按优先级排序。
- 核心特性
- 无界队列:理论上容量无限(实际受内存限制),动态扩容。
- 线程安全:通过
ReentrantLock和Condition实现线程安全。 - 优先级排序:
- 元素按自然顺序(
Comparable)或自定义Comparator排序。 - 堆结构:底层基于数组实现的最小堆(默认)或最大堆(通过
Comparator控制)。
- 元素按自然顺序(
- 阻塞操作:
- 队列空时出队阻塞:
take()方法在队列空时阻塞线程,直到有元素。 - 无入队阻塞:由于无界,
put()和offer()永远不会阻塞。
- 队列空时出队阻塞:
- 底层数据结构
- 堆(数组):元素存储在
Object[] queue中,满足堆性质(父节点优先级 ≤ 子节点)。 - 堆操作:
- 插入元素:上浮(
siftUp)调整堆。 - 移除元素:下沉(
siftDown)调整堆。
- 插入元素:上浮(
- 堆(数组):元素存储在
- 动态扩容:
- 扩容触发:当插入元素时,若当前数组已满,触发扩容。
- 扩容策略:
- 若当前容量 < 64,新容量 = 旧容量 * 2 + 2。
- 若当前容量 ≥ 64,新容量 = 旧容量 * 1.5。
4.8.2、线程安全原理
- 锁与条件变量
- 单锁机制:所有操作(入队、出队)共享一个
ReentrantLock。 - 条件变量:
notEmpty用于队列空时阻塞消费者线程。
- 单锁机制:所有操作(入队、出队)共享一个
private final ReentrantLock lock = new ReentrantLock();
private final Condition notEmpty = lock.newCondition();
- 入队操作(
offer())
public boolean offer(E e) {
if (e == null) throw new NullPointerException();
final ReentrantLock lock = this.lock;
lock.lock();
try {
int n = size;
if (n >= queue.length)
grow(n + 1); // 动态扩容
siftUp(n, e); // 上浮调整堆
size = n + 1;
notEmpty.signal(); // 唤醒消费者
return true;
} finally {
lock.unlock();
}
}
3.3 出队操作(take())
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
E result;
try {
while (size == 0)
notEmpty.await(); // 队列空时阻塞
result = dequeue(); // 下沉调整堆
} finally {
lock.unlock();
}
return result;
}
4.8.3、常用方法
put(E e):插入元素(永不阻塞)。take():移除并返回队首元素,队列空时阻塞线程。offer(E e):插入元素,始终返回true(无界队列不会满)。poll():移除并返回队首元素,队列空时返回null。peek():返回队首元素但不移除,队列空时返回null。remainingCapacity():始终返回Integer.MAX_VALUE(无界队列)。
4.8.4、使用场景
1、适用场景
- 优先级任务调度:高优先级任务优先处理(如紧急告警系统)。
- 延迟队列(结合 Delayed 接口):用于定时任务调度(如 DelayQueue 的实现)。
- 高并发场景:由于其高效的锁机制,适合高并发环境下的队列操作。
2、不适用场景
- 内存敏感场景:无界特性可能导致内存耗尽(OOM),需谨慎使用。替代方案使用有界队列(如 ArrayBlockingQueue)。
- 高频次操作:插入和删除的时间复杂度为 O(log n),高频操作可能影响性能。
- 严格顺序要求:同优先级元素的处理顺序不确定(堆不保证稳定性)。
4.9、SynchronousQueue详解
4.9.1、概述
SynchronousQueue是Java并发包(java.util.concurrent)中一种特殊的阻塞队列,它不存储元素,而是直接在生产者和消费者之间传递数据。它并不是一个真正意义上的集合,因为它不存储元素。相反,它是一个用于线程间直接传递数据的同步机制。每个插入操作必须等待另一个线程对应的移除操作,反之亦然。
- 核心特性
- 无存储结构:队列本身不存储元素,每个插入操作必须等待一个对应的移除操作(一个线程往队列放数据的时候,必须等待另一个线程从队列中取走数据,反之亦然)。
- 线程安全:通过
CAS操作和锁实现线程安全。 - 阻塞操作:
- 生产者阻塞:
put()方法在无消费者时阻塞,直到有消费者取走元素。 - 消费者阻塞:
take()方法在无生产者时阻塞,直到有生产者插入元素。
- 生产者阻塞:
- 公平性选项:支持公平模式(
FIFO)和非公平模式(默认)。- 公平模式:基于队列(
FIFO)实现,保证等待时间最长的线程优先执行。 - 非公平模式:基于栈(
LIFO)实现,性能更高,但可能导致线程饥饿。
- 公平模式:基于队列(
- 零容量:队列容量始终为 0,元素直接传递。
- 扩容机制:
SynchronousQueue不存储元素,容量始终为 0,无需扩容。 - 底层数据结构:
SynchronousQueue不维护任何内部存储结构,元素直接在生产者和消费者之间传递。
4.9.2、线程安全原理
CAS操作:CAS(Compare-And-Swap),通过CAS操作实现无锁线程安全,避免锁竞争。- 节点匹配:生产者和消费者通过
CAS操作匹配节点,完成数据传递。
1、公平模式(TransferQueue)
- 队列结构:使用
FIFO队列管理等待线程。 - 匹配机制:
- 生产者插入节点,等待消费者匹配。
- 消费者移除节点,唤醒匹配的生产者。
static final class TransferQueue<E> extends Transferer<E> {
volatile QNode head; // 队列头
volatile QNode tail; // 队列尾
}
2、非公平模式(TransferStack)
- 栈结构:使用
LIFO栈管理等待线程。 - 匹配机制:
- 生产者插入节点,等待消费者匹配。
- 消费者移除节点,唤醒匹配的生产者。
static final class TransferStack<E> extends Transferer<E> {
volatile SNode head; // 栈顶
}
3、入队操作(put())
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
if (transferer.transfer(e, false, 0) == null) {
Thread.interrupted();
throw new InterruptedException();
}
}
4、出队操作(take())
public E take() throws InterruptedException {
E e = transferer.transfer(null, false, 0);
if (e != null)
return e;
Thread.interrupted();
throw new InterruptedException();
}
4.9.3、常用方法
put(E e):插入元素,若无消费者则阻塞线程。take():移除并返回元素,若无生产者则阻塞线程。offer(E e):插入元素,若无消费者则立即返回 false。poll():移除并返回元素,若无生产者则立即返回 null。peek():始终返回 null(无存储结构)。remainingCapacity():始终返回 0(容量为 0)。
4.9.4、使用场景
1、适用场景
- 直接传递任务:生产者必须等待消费者取走元素,适合任务直接传递的场景。
- 线程池任务分配:用于
Executors.newCachedThreadPool的任务队列,动态创建线程处理任务。newCachedThreadPool线程池的核心线程数是0,最大线程数是Integer的最大值,线程存活时间是60秒。如果你newCachedThreadPool线程池,提交的任务会被更快速的处理,因为每次提交任务,都会有一个空闲的线程等着处理任务。如果没有空闲的线程,也会立即创建一个线程处理任务。 - 高吞吐量系统:无锁设计减少竞争,适合高并发场景。
2、不适用场景
- 需要缓冲任务:无法存储元素,不适合需要缓冲任务的场景。
- 替代方案:使用 LinkedBlockingQueue 或 ArrayBlockingQueue。
- 低延迟要求:生产者和消费者必须严格同步,可能增加延迟。
- 任务量波动大:若生产者或消费者数量不均衡,可能导致线程阻塞或资源浪费。
4.10、DelayQueue详解
4.10.1、概述
DelayQueue(延迟队列)是Java并发包(java.util.concurrent)中基于优先级堆实现的无界阻塞队列,用于存储实现了Delayed接口的元素。元素只有在其延迟时间到期后才能被取出。不允许存储null元素。
Delayed接口:Delayed接口是java.util.concurrent包中的一个接口,它继承自Comparable接口。Delayed接口要求实现类提供一个方法getDelay(TimeUnit unit),该方法返回当前元素还需延迟的时间。
- 核心特性
- 无界队列:理论上容量无限(实际受内存限制),动态扩容。
- 延迟元素:元素必须实现 Delayed 接口,指定延迟时间。
- 优先级排序:元素按延迟时间排序,最早到期的元素优先出队。
- 线程安全:通过
ReentrantLock和Condition实现线程安全。 - 阻塞操作:
- 队列空时出队阻塞:
take()方法在队列空时阻塞线程,直到有元素到期。 - 无入队阻塞:由于无界,
put()和offer()永远不会阻塞。
- 队列空时出队阻塞:
- 扩容机制
- 扩容触发:当插入元素时,若当前数组已满,触发扩容。
- 扩容策略:与
PriorityQueue相同,容量扩展为原大小的 50%。
- 底层数据结构
- 优先级堆:元素存储在
PriorityQueue中,按延迟时间排序。 - 延迟接口:元素必须实现
Delayed接口,定义getDelay()和compareTo()方法。
- 优先级堆:元素存储在
public interface Delayed extends Comparable<Delayed> {
long getDelay(TimeUnit unit); // 返回剩余延迟时间
}
4.10.2、线程安全原理
- 锁与条件变量
- 单锁机制:所有操作(入队、出队)共享一个
ReentrantLock。 - 条件变量:
available用于队列空时阻塞消费者线程。
- 单锁机制:所有操作(入队、出队)共享一个
private final transient ReentrantLock lock = new ReentrantLock();
private final Condition available = lock.newCondition();
1、入队操作(offer())
- 在真正执行插入逻辑之前会先上一把可重入锁,确保线程安全,最后在执行完插入逻辑之后释放当前锁;
- 将当前任务放入优先队列中,确保堆顶元素是最小的,即小顶堆;
- 判断当前优先队列中堆顶的任务是否与当前插入任务相等,如果相等,就将
leader设置为null并将Condition信号量设置为signal唤醒一个消费者。
public boolean offer(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
q.offer(e); // 插入优先级堆
if (q.peek() == e) { // 如果新元素是最早到期的
available.signal(); // 唤醒消费者
}
return true;
} finally {
lock.unlock();
}
}
2、出队操作(take())
- 首先还是在真正逻辑的前后加一把可重入锁和执行完逻辑后释放锁;
take方法中使用一个无限循环来获取到期的任务,如果当前没有到期的任务则一直自旋阻塞,直到有满足执行条件的任务为止;- 从优先队列中获取最小堆堆顶元素(
peek方法不会删除元素),如果当前堆顶元素为空,就会进入阻塞状态; - 如果当前堆顶元素不为空,就会判断当前任务是否达到了执行时间,达到执行时间直接返回任务,反之阻塞当前线程,直到堆顶元素到期;
- 如果
leader不为空,表明已经有其他线程正在等待这个堆顶元素; - 如果
leader为空,就将leader设置为当前线程,并将Condition信号量设置为delay纳秒阻塞时间; leader线程被唤醒并获取到锁之后会将leader设置为空。
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
for (;;) {
E first = q.peek(); // 获取堆顶元素
if (first == null) {
available.await(); // 队列空时阻塞
} else {
long delay = first.getDelay(NANOSECONDS);
if (delay <= 0) {
return q.poll(); // 到期,移除元素
}
first = null; // GC
if (leader != null) {
available.await(); // 其他线程正在等待
} else {
Thread thisThread = Thread.currentThread();
leader = thisThread;
try {
available.awaitNanos(delay); // 等待延迟时间
} finally {
if (leader == thisThread) {
leader = null;
}
}
}
}
}
} finally {
if (leader == null && q.peek() != null) {
available.signal(); // 唤醒其他消费者
}
lock.unlock();
}
}
4.10.3、延迟队列原理
1、原理
DelayQueue的泛型参数需要实现Delayed接口,Delayed接口继承了Comparable接口,DelayQueue内部使用非线程安全的优先队列(PriorityQueue),并使用Leader/Followers模式,它是一种常见的多线程同步模式,最小化不必要的等待时间。- 详细的过程查看4.10.2
2、Leader/Followers模式
Leader/Followers就是永远只有一个leader线程处于可消费状态,而其它的follower线程就只有在成为leader才能切换为可消费状态。从上面的offer和take方法来看,每当当前任务不处于执行时间,就会将当前线程设置为leader便于阻塞持续监听,同时也防止其它线程参与不良的竞争。只要堆顶任务已经执行完毕,就会将当前与leader绑定的线程清空,然后开启下一轮leader选举。Leader/Followers模式的基本原则就是,永远最多只有一个leader。而所有follower都在等待成为leader。线程池启动时会自动产生一个Leader负责等待网络IO事件,当有一个事件产生时,Leader线程首先通知一个Follower线程将其提拔为新的Leader,然后自己就去干活了,去处理这个网络事件,处理完毕后加入Follower线程等待队列,等待下次成为Leader。这种方法可以增强CPU高速缓存相似性,及消除动态内存分配和线程间的数据交换。Leader/Followers的线程模型一共有Follower、Candidate和Leader三个状态。- Follower:
- 线程处于等待状态,等待被唤醒或被选为候选者。
- 当Leader线程释放领导权时,
Follower线程有机会被选为新的候选者。
- Candidate:
- 线程尝试获取领导权,可能会成功也可能失败。
- 如果成功,线程将成为新的
Leader。 - 如果失败,线程将回到
Follower状态。
- 如果成功,线程将成为新的
- 线程尝试获取领导权,可能会成功也可能失败。
- Leader:
- 线程拥有领导权,可以执行特定的任务,例如在
DelayQueue中取出到期的元素。 Leader线程在完成任务或等待超时后,将释放领导权,并回到Follower状态。
- 线程拥有领导权,可以执行特定的任务,例如在
- Follower:
4.10.4、常用方法
put(E e):插入元素(永不阻塞)。take():移除并返回队首元素,队列空时阻塞线程。offer(E e):插入元素,始终返回true(无界队列不会满)。poll():移除并返回队首元素,队列空时返回null。peek():返回队首元素但不移除,队列空时返回null。remainingCapacity():始终返回Integer.MAX_VALUE(无界队列)。
4.10.5、使用场景
1、适用场景
- 定时任务调度:用于实现定时任务调度系统(如
ScheduledThreadPoolExecutor)。
DelayQueue<DelayedTask> queue = new DelayQueue<>();
queue.put(new DelayedTask("Task1", 10, TimeUnit.SECONDS));
queue.put(new DelayedTask("Task2", 5, TimeUnit.SECONDS));
DelayedTask task = queue.take(); // 5秒后取出 Task2
- 缓存过期清理:用于管理缓存中的过期条目。
DelayQueue<CacheEntry> cache = new DelayQueue<>();
cache.put(new CacheEntry("key1", "value1", 10, TimeUnit.MINUTES));
CacheEntry entry = cache.take(); // 10分钟后清理
- 延迟消息队列:用于实现延迟消息传递(如消息队列系统)。
2、不适用场景
- 内存敏感场景:无界特性可能导致内存耗尽(
OOM),需谨慎使用。可使用有界队列(如ArrayBlockingQueue)替代。 - 高频次操作:插入和删除的时间复杂度为
O(log n),高频操作可能影响性能。 - 严格顺序要求:同延迟时间的元素处理顺序不确定(堆不保证稳定性)。
五、Map接口及其实现类
5.1、概念
Map是Java集合框架中的一个核心接口,表示键值对的集合。Map中的键不允许重复,每个键最多映射到一个值。
- 特点
- 键值对存储:每个元素包含一个键和一个值。
- 键不允许重复:通过
equals()和hashCode()方法判断键是否重复。 - 允许 null 键和 null 值:大多数 Map 实现类允许存储
null键和null值(TreeMap和ConcurrentHashMap除外)。
- 核心方法
put(K key, V value):添加键值对。get(Object key):根据键获取值。remove(Object key):删除键值对。containsKey(Object key):判断是否包含指定键。containsValue(Object value):判断是否包含指定值。keySet():返回所有键的集合。values():返回所有值的集合。entrySet():返回所有键值对的集合。
- 主要实现类
- 线程安全
Hashtable:基于哈希表实现,键值对无序。(方法使用synchronized修饰)ConcurrentHashMap:基于分段锁(JDK 7)或CAS操作(JDK 8+)实现的高性能线程安全哈希表。
- 非线程安全
HashMap:基于哈希表实现,键值对无序。LinkedHashMap:基于哈希表和链表实现,键值对按插入顺序或访问顺序排列。TreeMap:基于红黑树实现,键值对按自然顺序或自定义比较器排序。WeakHashMap:基于哈希表实现,键是弱引用(WeakReference),当键不再被强引用时,键值对会被自动移除。IdentityHashMap:基于哈希表实现,使用==而不是equals()方法比较键。EnumMap:专为枚举类型设计的哈希表,性能极高。
- 线程安全
Collections.synchronizedMap:通过包装非线程安全的 Map 实现线程安全。所有方法都使用 synchronized 修饰。
5.2、HashMap详解
5.2.1、概述
HashMap是Java中最常用的集合类之一,它是Map接口的实现类,基于哈希表(Hash Table)实现。HashMap 允许存储键值对,并且键和值都可以为 null。HashMap 不保证元素的顺序,元素的存储顺序可能与插入顺序不同。并且它线程不安全,如果需要线程安全,可以使用 Collections.synchronizedMap或 ConcurrentHashMap。
- 底层数据结构:
HashMap内部是一个数组 + 链表/红黑树的组合数据结构:- 数组是核心,用于存储桶(
Bucket)。 - 每个桶中存放的是链表或红黑树,用于解决哈希冲突。
- 哈希表用于快速查找和去重。
- 双向链表用于维护元素的插入顺序或访问顺序。
- 数组是核心,用于存储桶(
- 初始容量和负载因子:默认初始容量为 16,默认负载因子为 0.75。可以通过构造函数指定初始容量和负载因子。
- 动态扩容:当元素数量超过当前容量与负载因子的乘积时,HashMap 会自动扩容。
- 时间复杂度:
- 添加元素:O(1)(平均情况下)。
- 删除元素:O(1)(平均情况下)。
- 查找元素:O(1)(平均情况下)。
- 遍历元素:O(n)。
5.2.2、内部结构
- 节点结构:HashMap 的节点由内部类 Node 表示
static class Node<K,V> implements Map.Entry<K,V> {
final int hash; // 哈希值
final K key; // 键
V value; // 值
Node<K,V> next; // 下一个节点(链表指针)
}
- 红黑树节点:当链表转换为红黑树时,使用 TreeNode 类:
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // 父节点
TreeNode<K,V> left; // 左子节点
TreeNode<K,V> right; // 右子节点
TreeNode<K,V> prev; // 链表前驱节点
boolean red; // 是否为红色节点
}
5.2.3、扩容机制详解
- 扩容触发条件:当元素数量超过当前容量与负载因子的乘积时,HashMap 会自动扩容。
扩容过程
- 计算新容量:新容量 = 旧容量 * 2。
- 建新哈希表。
- 旧哈希表中的元素重新哈希到新哈希表中。
5.2.4、哈希冲突解决方式
1、概念
- 哈希冲突:当多个键的哈希值映射到同一个桶(Bucket)时,会发生哈希冲突。
- 哈希函数:
HashMap使用对象的hashCode()方法生成哈希值,并通过位运算将哈希值映射到数组索引,index = (hash & (capacity - 1))其中 hash 是经过扰动处理后的哈希值,capacity 是数组容量。
2、解决方式
- 链地址法:链表法是哈希冲突解决的最基本方法。它的核心思想是将哈希表的每个数组元素作为一个链表的头节点,所有哈希值相同的键值对都存储在同一个链表中
- 数据结构:哈希表是一个数组,数组的每个元素是一个链表的头节点。每个链表节点存储键值对以及指向下一个节点的指针。
- 操作流程
插入键值对:- 计算键的哈希值,找到对应的数组索引。
- 如果该索引对应的链表为空,直接插入。
- 如果链表不为空,遍历链表检查是否已存在相同的键:如果存在,更新值。如果不存在,将新节点插入链表头部或尾部。
查找键值对:- 计算键的哈希值,找到对应的数组索引。
- 遍历链表,通过
equals()方法比较键是否相等。 - 如果找到相等的键,返回对应的值;否则返回
null。
删除键值对:- 计算键的哈希值,找到对应的数组索引。
- 遍历链表,找到对应的节点并删除。
- 优点:
- 实现简单,易于理解。
- 适合小规模数据或哈希冲突较少的情况。
- 缺点
- 当链表过长时,查找、插入和删除的时间复杂度退化为
O(n)。 - 需要额外的空间存储链表指针。
- 当链表过长时,查找、插入和删除的时间复杂度退化为
- 红黑树优化:是对链表法的优化。当链表长度超过 8 并且数组容量大于等于 64时,将链表转换为红黑树,以提高查找、插入和删除的性能。当链表长度减少到 6 以下时,红黑树会转换回链表。
-
数据结构:哈希表是一个数组,数组的每个元素是一个链表或红黑树的根节点。当链表长度超过阈值(默认是 8)时,链表转换为红黑树。 当红黑树的节点数减少到一定阈值(默认是 6)时,红黑树退化为链表。
-
操作流程
插入键值对:- 计算键的哈希值,找到对应的数组索引。
- 如果该索引对应的链表为空,直接插入。
- 如果链表不为空,遍历链表检查是否已存在相同的键:如果存在,更新值。如果不存在,将新节点插入链表尾部。如果链表长度超过阈值,将链表转换为红黑树。
查找键值对:- 计算键的哈希值,找到对应的数组索引。
- 如果该索引对应的是链表,遍历链表查找。
- 如果该索引对应的是红黑树,使用红黑树的查找算法。
删除键值对:- 计算键的哈希值,找到对应的数组索引。
- 如果该索引对应的是链表,遍历链表删除。
- 如果该索引对应的是红黑树,使用红黑树的删除算法。
- 如果红黑树的节点数减少到阈值以下,将红黑树退化为链表。
-
优点
- 当哈希冲突严重时,红黑树的查找、插入和删除时间复杂度为
O(logn),性能优于链表。 - 适合大规模数据或哈希冲突较多的情况。
- 当哈希冲突严重时,红黑树的查找、插入和删除时间复杂度为
-
缺点
- 实现复杂,需要维护红黑树的平衡性。
- 需要额外的空间存储红黑树的节点信息。
-
5.2.5、常用方法
put(K key, V value):添加键值对。remove(Object key):删除指定键的键值对。get(Object key):获取指定键的值。size():返回键值对数量。isEmpty():判断是否为空。containsKey(Object key):判断是否包含指定键。containsValue(Object value):判断是否包含指定值。clear():清空所有键值对。
5.2.6、使用场景
1、适合的场景
- 键值对存储:
HashMap适合存储键值对,例如缓存系统、配置管理等。 - 快速查找:
HashMap基于哈希表实现,查找性能极高(O(1)),适合需要频繁查找键值对的场景。 - 数据量较大:
HashMap的扩容机制和哈希表特性使其适合存储大量数据。
2、不适合的场景
- 需要有序集合:
HashMap不保证元素的顺序,如果需要有序集合,可以使用LinkedHashMap或TreeMap。 - 频繁插入和删除:如果频繁插入和删除元素,可能会导致哈希冲突和扩容,影响性能。
- 内存占用敏感:
HashMap需要额外的内存存储键值对和链表/红黑树节点,内存占用较高。
5.3、LinkedHashMap详解
5.3.1、概述
LinkedHashMap是Java 中一个基于链表和哈希表实现的集合类,它是 HashMap 的子类,同时实现了Map接口。LinkedHashMap 不仅具有HashMap的快速查找特性,还保留了元素的插入顺序或访问顺序。LinkedHashMap允许存储键值对,键和值都可以为 null。但它线程不安全,如果需要线程安全,可以使用Collections.synchronizedMap。
- 底层数据结构:LinkedHashMap 的底层是一个 HashMap,同时维护了一个双向链表。
- 哈希表用于快速查找和去重。
- 双向链表用于维护元素的插入顺序或访问顺序。
- 初始容量和负载因子:默认初始容量为 16,默认负载因子为 0.75。
- 动态扩容:当元素数量超过当前容量与负载因子的乘积时,LinkedHashMap 会自动扩容。
- 扩容机制:完全继承自 HashMap
- 新容量 = 旧容量 * 2。
- 将旧哈希表中的元素重新哈希到新哈希表中。
- 维护双向链表的顺序。
- 哈希冲突解决方式:与 HashMap 完全一致(
详情请看HashMap 5.2.5章节)。- 使用链地址法解决冲突,即将冲突的键值对存储在链表中。
- 在 Java 8 中,当链表长度超过 8 并且数组容量大于等于 64 时,链表会转换为红黑树。
- 时间复杂度
- 添加元素:O(1)(平均情况下)。
- 删除元素:O(1)(平均情况下)。
- 查找元素:O(1)(平均情况下)。
- 遍历元素:O(n)。
5.3.2、维护顺序
- 插入顺序:默认情况下,LinkedHashMap 按照插入顺序维护元素。每次插入新元素时,都会将其添加到双向链表的尾部。
- 访问顺序:在访问顺序模式下,每当访问一个键值对时,该键值对会被移动到双向链表的尾部。最近最少使用的键值对位于链表的头部,而最近使用的键值对位于链表的尾部。如果希望按照访问顺序维护元素,可以通过构造函数启用访问顺序模式:
LinkedHashMap<K, V> map = new LinkedHashMap<>(initialCapacity, loadFactor, true);//第三个参数 true 表示启用访问顺序模式。
5.3.3、内部结构
- 节点结构:LinkedHashMap 的节点由内部类 LinkedHashMap.Entry 表示,扩展了 HashMap.Node:
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after; // 双向链表指针
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
- 双向链表:LinkedHashMap 使用两个字段维护双向链表的头尾节点:
transient LinkedHashMap.Entry<K,V> head; // 链表头
transient LinkedHashMap.Entry<K,V> tail; // 链表尾
5.3.4、常用方法
put(K key, V value):添加键值对。remove(Object key):删除指定键的键值对。get(Object key):获取指定键的值。size():返回键值对数量。isEmpty():判断是否为空。containsKey(Object key):判断是否包含指定键。containsValue(Object value):判断是否包含指定值。clear():清空所有键值对。
5.3.5、使用的场景
1、适用场景
- 需要保留插入顺序或访问顺序:LinkedHashMap 保留了元素的插入顺序或访问顺序,适合需要有序遍历的场景。
LinkedHashMap<Integer, String> map = new LinkedHashMap<>();
map.put(1, "apple");
map.put(2, "banana");
map.put(3, "cherry");
for (Map.Entry<Integer, String> entry : map.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue()); // 输出顺序:1: apple, 2: banana, 3: cherry
}
- LRU 缓存:
LinkedHashMap可以通过重写removeEldestEntry方法实现LRU(Least Recently Used)缓存。
LinkedHashMap<Integer, String> lruCache = new LinkedHashMap<>(16, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<Integer, String> eldest) {
return size() > 3; // 缓存容量为 3
}
};
lruCache.put(1, "apple");
lruCache.put(2, "banana");
lruCache.put(3, "cherry");
lruCache.get(1); // 访问键为 1 的元素
lruCache.put(4, "date"); // 超出容量,删除最久未使用的元素 (2: banana)
- 快速查找和有序遍历:LinkedHashMap 基于哈希表实现,查找性能极高(O(1)),同时保留了插入顺序或访问顺序。
2、不适合的场景
- 频繁插入和删除:如果频繁插入和删除元素,可能会导致哈希冲突和扩容,影响性能。
- 内存占用敏感:LinkedHashMap 需要额外的内存存储链表节点,内存占用较高。
5.4、TreeMap详解
5.4.1、概述
TreeMap是Java中一个基于红黑树(Red-Black Tree)实现的集合类,一种自平衡二叉搜索树,它是 SortedMap接口的实现类。TreeMap不允许存储重复键,且键不允许为null(除非使用自定义比较器),并且键按照自然顺序或自定义顺序排序。TreeMap线程不安全,如果需要线程安全,可以使用Collections.synchronizedSortedMap。
- 底层数据结构:TreeMap 的底层是一个红黑树(Red-Black Tree),每个节点(Entry)包含键、值、左子节点、右子节点和父节点。红黑树是一种自平衡二叉搜索树,保证了插入、删除和查找操作的时间复杂度为 O(log n)。
- 排序机制:元素按照自然顺序(Comparable 接口)或自定义顺序(Comparator 接口)排序。
TreeMap<Integer, String> map = new TreeMap<>(); // 自然顺序
TreeMap<String, Integer> map = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); // 自定义顺序
- 动态扩容:
TreeMap基于红黑树实现,不需要预先分配内存空间,因此没有固定的初始容量。每次添加元素时,动态调整红黑树的结构以保持平衡。 - 哈希冲突解决方式:
TreeMap不涉及哈希冲突,因为它基于红黑树实现,而不是哈希表。- 键的比较规则:如果没有指定比较器,则使用键的自然顺序(要求键实现
Comparable接口)。如果指定了比较器,则使用比较器定义的顺序。 - 键的唯一性:
TreeMap不允许重复键,如果尝试插入相同键的键值对,新值会覆盖旧值。
- 键的比较规则:如果没有指定比较器,则使用键的自然顺序(要求键实现
- 时间复杂度
- 添加元素:O(log n)。
- 删除元素:O(log n)。
- 查找元素:O(log n)。
- 遍历元素:O(n)。
5.4.2、常用方法
put(K key, V value):添加键值对。remove(Object key):删除指定键的键值对。get(Object key):获取指定键的值。subMap(K fromKey, K toKey):返回从 fromKey(包含)到 toKey(不包含)的子集。size():返回键值对数量。isEmpty():判断是否为空。containsKey(Object key):判断是否包含指定键。containsValue(Object value):判断是否包含指定值。clear():清空所有键值对。
5.4.3、使用场景
1、适用场景
- 需要有序集合:
TreeMap保证元素的有序性,适合需要排序的场景。 - 范围查询:
TreeMap提供了高效的范围查询方法(如subMap、headMap、tailMap)。 - 去重且排序:
TreeMap不允许存储重复键,并且键有序,适合需要去重和排序的场景。 - 统计分析:按某种顺序统计数据,如成绩排名、词频统计等。
- 优先级队列:通过自定义比较器实现优先级排序。
- 日志记录:按时间戳排序的日志存储。
2、不适合的场景
- 频繁插入和删除:红黑树的平衡调整需要额外开销,频繁插入和删除操作性能较低。
- 内存占用敏感:红黑树需要额外的内存存储节点和指针,内存占用较高。
- 不需要排序:如果不需要排序,使用
HashMap或LinkedHashMap性能更高。
5.5、ConcurrentHashMap详解
5.5.1、概述
ConcurrentHashMap是Java中一个线程安全的Map实现类,它是java.util.concurrent 包中的一部分。ConcurrentHashMap在高并发环境下提供了高效的读写操作,适合多线程场景。 ConcurrentHashMap基于分段锁(Segment Locking)或 CAS(Compare-And-Swap)机制实现。它允许存储键值对,键和值都不允许为null。在 JDK 1.7 中,使用分段锁实现;在 JDK 1.8 中,使用CAS和synchronized实现。
- 底层数据结构
JDK 1.7:使用分段锁(Segment Locking)机制,将整个哈希表分成多个段(Segment),每个段独立加锁。每个段是一个小的HashMap,包含一个数组和链表。JDK 1.8:使用数组 + 链表 + 红黑树的结构。使用CAS和synchronized实现线程安全。- 数组是核心,用于存储桶(Bucket)。
- 每个桶中存放的是链表或红黑树,用于解决哈希冲突。
- 初始容量和负载因子:默认初始容量为 16。默认负载因子为 0.75。
- 动态扩容:当元素数量超过当前容量与负载因子的乘积时,ConcurrentHashMap 会自动扩容。
- 扩容机制:
- 新容量 = 旧容量 * 2。
- 将旧哈希表中的元素重新哈希到新哈希表中。
- 并发扩容:
- 多个线程可以同时参与扩容操作。
- 每个线程负责迁移一部分桶(Bucket),从而提高扩容效率。
- 扩容机制:
- 时间复杂度:
- 添加元素:O(1)(平均情况下)。
- 删除元素:O(1)(平均情况下)。
- 查找元素:O(1)(平均情况下)。
- 遍历元素:O(n)。
5.5.2、哈希冲突解决
详情请看5.2.5章节
- 链地址法:在
Java 8及之前的版本中,ConcurrentHashMap使用链地址法解决哈希冲突。- 当多个键映射到同一个桶时,这些键值对会被存储在一个链表中。
- 每个桶中的链表节点由
Node类表示,每个节点包含键、值、哈希值以及指向下一个节点的引用。
- 红黑树优化:在 Java 8 中,
ConcurrentHashMap引入了红黑树优化机制。- 当链表长度超过阈值(默认为 8),并且数组容量大于等于 64 时,
ConcurrentHashMap会调用treeifyBin()方法将链表会转换为红黑树。 - 数组容量小于 64,则会选择扩容而不是转换为红黑树,以减少冲突的可能性。
- 如果红黑树中的节点数减少到 6 以下,红黑树会重新转换为链表
- 红黑树是一种自平衡二叉搜索树,能够提供更高效的查找、插入和删除操作(时间复杂度为 O(log n))。
- 当链表长度超过阈值(默认为 8),并且数组容量大于等于 64 时,
5.5.3、 线程安全的实现原理
1、原理
- JDK 1.7:
- 使用分段锁(
Segment Locking)机制,将整个哈希表分成多个段(Segment),每个段独立加锁。 - 每个段是一个小的
HashMap,包含一个数组和链表。 - 读操作不需要加锁,写操作只需要锁住对应的段。
- 使用分段锁(
- JDK 1.8:
- 使用
CAS和synchronized实现线程安全。 - 每个桶(
bucket)使用synchronized锁住链表或红黑树的头节点。 - 读操作不需要加锁,写操作只需要锁住对应的桶。
- 使用
2、补充概念
-
CAS(
Compare-And-Swap):CAS是一种无锁的并发控制机制,通过原子操作实现线程安全。- 核心思想:比较并交换
- 比较:检查目标内存位置的值是否与预期值相等。
- 交换:如果相等,则将目标内存位置的值更新为新值;如果不相等,则不进行任何操作。
- 原理:CAS 操作依赖于底层硬件的支持,通常由 CPU 提供原子指令实现(如 x86 架构中的 CMPXCHG 指令)。这些指令保证了操作的原子性,即在多线程环境下不会被中断。
- 优点:
- 高效性:CAS 不需要加锁,减少了线程间的阻塞和上下文切换开销。
- 非阻塞性:线程不会因等待锁而被挂起,从而提高了系统的吞吐量。
- 缺点:
- ABA 问题:假设某个变量的值从 A 变为 B,然后又变回 A。在这种情况下,CAS 操作可能会误认为该值没有发生变化,从而导致错误的结果。解决方案包括引入版本号或时间戳(如 AtomicStampedReference)。
- 循环时间过长:如果 CAS 操作失败,线程通常会进入自旋状态(不断重试),这可能导致 CPU 占用率过高。可以通过退避算法降低重试频率。
- 核心思想:比较并交换
-
细粒度锁:细粒度锁是一种锁的设计策略,通过缩小锁的作用范围,减少锁的竞争,从而提高并发性能。
- 优势对比:
- 在传统的粗粒度锁中,整个数据结构会被锁定,所有线程必须等待锁释放后才能访问。
- 在细粒度锁中,锁的作用范围被限制到更小的粒度(如单个桶、节点或对象),允许多个线程同时操作不同的部分。
- 优点:
- 高并发性能:允许多个线程同时操作不同的桶,减少了锁的竞争。
- 灵活性:可以根据实际需求调整锁的粒度,平衡性能和复杂性。
- 缺点:
- 复杂性增加:细粒度锁的设计和实现比粗粒度锁更复杂,可能引入更多的边界条件和潜在问题。
- 死锁风险:如果多个线程需要获取多个锁,并且获取顺序不同,可能会导致死锁。
- 优势对比:
-
转发节点(
Forwarding Node):ConcurrentHashMap在Java 8及之后版本中引入的一种特殊节点,用于在并发扩容过程中协调多个线程的操作。它的主要作用是确保在扩容期间,读写操作能够正确处理正在迁移的桶(Bucket),从而避免死锁或数据不一致问题。- 作用:在
ConcurrentHashMap的扩容过程中- 原数组中的元素需要重新计算哈希值,并迁移到新数组中。
- 如果某个桶正在被迁移,其他线程可能仍然尝试访问该桶。
- 为了保证线程安全,
ConcurrentHashMap使用Forwarding Node标记该桶的状态。
- 功能:
- 标记迁移状态:当一个桶正在被迁移时,
Forwarding Node会替换原来的链表或红黑树,通知其他线程该桶正在迁移。 - 重定向操作:其他线程在访问该桶时,会通过
Forwarding Node被重定向到新数组中对应的位置。 - 避免死锁:通过
Forwarding Node,确保多个线程可以安全地参与扩容操作,而不会因等待锁而导致死锁。
- 标记迁移状态:当一个桶正在被迁移时,
- 工作流程
- 扩容过程中的使用:当某个线程开始扩容时,它会先创建一个新的数组。然后,它会逐个迁移原数组中的桶到新数组中。在迁移某个桶时,该桶会被替换为一个
Forwarding Node,指向新数组。 - 线程的处理:当其他线程尝试访问一个已经被标记为
Forwarding Node的桶时,它会检测到该桶是一个Forwarding Node,然后通过Forwarding Node的nextTable字段,直接跳转到新数组中查找目标键值对,如果目标键值对不存在,则会在新数组中插入新节点。 - 并发扩容的支持:多个线程可以同时参与扩容操作,每个线程负责迁移一部分桶。通过
Forwarding Node,确保所有线程都能正确处理正在迁移的桶,避免数据丢失或重复。
- 扩容过程中的使用:当某个线程开始扩容时,它会先创建一个新的数组。然后,它会逐个迁移原数组中的桶到新数组中。在迁移某个桶时,该桶会被替换为一个
- 优点
- 高效性:通过
Forwarding Node,多个线程可以安全地参与扩容操作,减少了锁的竞争。 - 线程安全性:确保在扩容期间,读写操作能够正确处理正在迁移的桶,避免数据不一致问题。
- 避免死锁:通过重定向机制,防止多个线程因等待锁而导致死锁。
- 高效性:通过
- 作用:在
3、线程安全实现细节
- 插入操作
- 步骤:
- 计算键的哈希值,并定位到对应的桶。
- 如果桶为空,则通过 CAS 尝试初始化该桶。
- 如果桶中有元素,则遍历链表或红黑树,检查是否已存在相同的键。
- 如果存在相同的键,则更新其值。
- 如果不存在相同的键,则尝试通过 CAS 添加新节点。
- 如果 CAS 失败,则使用细粒度锁(仅锁定当前桶)完成插入操作。
- 线程安全性:使用 CAS 实现无锁操作,减少锁的竞争。如果 CAS 失败,则使用细粒度锁,确保操作的原子性。
- 步骤:
- 删除操作
- 步骤:
- 计算键的哈希值,并定位到对应的桶。
- 遍历链表或红黑树,找到需要删除的节点。
- 如果找到目标节点,则通过 CAS 或细粒度锁完成删除操作。
- 线程安全性:删除操作与插入操作类似,优先使用 CAS,失败时使用细粒度锁。
- 步骤:
- 扩容操作
- 步骤:
- 当元素数量超过阈值(
threshold = capacity * loadFactor)时触发扩容。 - 创建一个新的数组,大小为原数组的两倍。
- 将原数组中的所有元素重新计算哈希值,并迁移到新数组中。
- 在迁移过程中,使用转发节点(
Forwarding Node)协调多个线程的操作。
- 当元素数量超过阈值(
- 并发扩容:每个线程负责迁移一部分桶,从而提高扩容效率。转发节点确保其他线程能够正确处理正在迁移的桶。
- 步骤:
- 遍历操作
- 在
Java 8中,ConcurrentHashMap提供了多种遍历方式(如forEach()、keySet()等),这些方法在遍历时不会锁定整个哈希表。 - 遍历操作可能会看到部分未完成的迁移结果,但不会导致数据不一致。
- 在
5.5.4、 常用方法
put(K key, V value):添加键值对。remove(Object key):删除指定键的键值对。get(Object key):获取指定键的值。size():返回键值对数量。isEmpty():判断是否为空。containsKey(Object key):判断是否包含指定键。containsValue(Object value):判断是否包含指定值。clear():清空所有键值对。
5.5.5、使用场景
1、适合的场景
- 高并发环境:ConcurrentHashMap 是线程安全的,适合高并发环境下的键值对存储和操作。
- 缓存系统:适合用作缓存系统,支持高效的并发读写操作。
- 数据量较大:ConcurrentHashMap 的扩容机制和哈希表特性使其适合存储大量数据。
2、不适合的场景
- 需要有序集合:ConcurrentHashMap 不保证元素的顺序,如果需要有序集合,可以使用 ConcurrentSkipListMap。
- 内存占用敏感:ConcurrentHashMap 需要额外的内存存储链表/红黑树节点,内存占用较高。
5.6、Hashtable详解
5.6.1、概述
Hashtable是 Java 中一个古老的集合类,它是Map接口的实现类,基于哈希表(Hash Table)实现。Hashtable是线程安全的,但性能较低,因此在现代Java开发中更推荐使用ConcurrentHashMap。Hashtable允许存储键值对,键和值都不允许为null,它不保证元素的顺序(从Java 8开始,迭代顺序可能与插入顺序一致,但不保证)。由于性能问题,现代Java开发中更推荐使用ConcurrentHashMap。
- 底层数据结构:
Hashtable的底层是一个Entry<?,?>[]数组,每个Entry是一个键值对。当发生哈希冲突时,Hashtable使用链表来存储冲突的键值对。 - 初始容量和负载因子:默认初始容量为 11,默认负载因子为 0.75。
- 动态扩容:当元素数量超过当前容量与负载因子的乘积时,
Hashtable会自动扩容。 - 扩容机制:
- 新容量 = 旧容量 * 2 + 1。
- 将旧哈希表中的元素重新哈希到新哈希表中。
- 哈希冲突解决方式:
Hashtable使用链地址法(Separate Chaining)解决哈希冲突(详情请看5.2.5章节)。 - 时间复杂度
- 添加元素:O(1)(平均情况下)。
- 删除元素:O(1)(平均情况下)。
- 查找元素:O(1)(平均情况下)。
- 遍历元素:O(n)。
5.6.2、常用方法
put(K key, V value):添加键值对。remove(Object key):删除指定键的键值对。get(Object key):获取指定键的值。size():返回键值对数量。isEmpty():判断是否为空。containsKey(Object key):判断是否包含指定键。containsValue(Object value):判断是否包含指定值。clear():清空所有键值对。
5.6.3、使用场景
1、适用场景
- 多线程环境:Hashtable 是线程安全的,适合在多线程环境下使用。如果不需要线程安全,建议使用 HashMap。
- 简单的同步需求:如果只需要简单的线程安全功能,而不需要复杂的并发控制(如分段锁或读写锁),Hashtable 是一个简单易用的选择。
2、不适合的场景
- 高并发场景:在高并发场景下,
Hashtable的性能较差,因为所有操作都需要竞争同一把锁。此时更适合使用ConcurrentHashMap。 - 需要高性能读操作:如果读操作远多于写操作,
Hashtable的性能不如ConcurrentHashMap。 - 需要复杂的并发控制:如果需要更细粒度的并发控制(如读写锁或分段锁),
Hashtable无法满足需求。
5.7、WeakHashMap详解
5.7.1、概述
WeakHashMap是Java中一个特殊的Map实现类,它是基于弱引用(Weak Reference)实现的哈希表。WeakHashMap的键是弱引用,当键不再被强引用时,垃圾回收器会自动回收这些键值对。WeakHashMap 线程不安全,如果需要线程安全,可以使用Collections.synchronizedMap。
- 底层数据结构:
WeakHashMap的底层是一个Entry<?,?>[]数组,每个Entry是一个键值对。键是弱引用(WeakReference),值是强引用。当键不再被强引用时,垃圾回收器会自动回收这些键值对。 - 初始容量和负载因子:默认初始容量为
16,默认负载因子为0.75。 - 动态扩容:当元素数量超过当前容量与负载因子的乘积时,
WeakHashMap会自动扩容。 - 扩容机制:
- 新容量 = 旧容量 * 2。
- 将旧哈希表中的元素重新哈希到新哈希表中。
- 哈希冲突解决方式:
WeakHashMap使用链地址法(Separate Chaining)解决哈希冲突。当发生哈希冲突时,冲突的键值对会存储在同一个桶(bucket)中,形成一个链表(详情请看5.2.5章节)。 - 时间复杂度
- 添加元素:O(1)(平均情况下)。
- 删除元素:O(1)(平均情况下)。
- 查找元素:O(1)(平均情况下)。
- 遍历元素:O(n)。
5.7.2、常用方法
put(K key, V value):添加键值对。remove(Object key):删除指定键的键值对。get(Object key):获取指定键的值。size():返回键值对数量。isEmpty():判断是否为空。containsKey(Object key):判断是否包含指定键。containsValue(Object value):判断是否包含指定值。clear():清空所有键值对。
5.7.3、使用场景
1、适用场景
- 缓存系统:WeakHashMap 适合用作缓存系统,当键不再被强引用时,垃圾回收器会自动回收这些键值对。
- 防止内存泄漏:WeakHashMap 可以防止因键未被释放而导致的内存泄漏。
- 监听器管理:避免因监听器未注销而导致的内存泄漏问题。
- 资源管理:管理临时资源,确保资源不会因长期持有而占用过多内存。
2、不适用场景
- 需要强引用键:如果键需要长期存在,不适合使用
WeakHashMap。 - 频繁插入和删除:如果频繁插入和删除元素,可能会导致哈希冲突和扩容,影响性能。
5.8、EnumMap详解
5.8.1、概述
EnumMap是 Java 中一个专门用于枚举类型的高效集合类,它是Map接口的实现类。EnumMap基于枚举常量的顺序实现,性能极高,且内存占用非常低。EnumMap允许存储键值对,键必须是同一枚举类型的常量,值可以是任意类型。它线程不安全,如果需要线程安全,可以使用Collections.synchronizedMap。
- 底层数据结构:
EnumMap的底层是一个Object[]数组,数组的索引对应枚举常量的顺序。 - 初始容量:
EnumMap的容量固定为枚举类型的常量数量。不需要动态扩容,因为枚举类型的常量数量是固定的。 - 动态扩容:
EnumMap不需要动态扩容,因为枚举类型的常量数量是固定的。 - 哈希冲突解决方式:
EnumMap不涉及哈希冲突,因为它基于枚举常量的顺序实现,而不是哈希表。 - 时间复杂度:
- 添加元素:
O(1)。 - 删除元素:
O(1)。 - 查找元素:
O(1)。 - 遍历元素:
O(n)。
- 添加元素:
5.8.2、常用方法
put(K key, V value):添加键值对。remove(Object key):删除指定键的键值对。get(Object key):获取指定键的值。size():返回键值对数量。isEmpty():判断是否为空。containsKey(Object key):判断是否包含指定键。containsValue(Object value):判断是否包含指定值。clear():清空所有键值对。
5.8.3、使用场景
1、适用场景
- 枚举类型集合:EnumMap 专门用于枚举类型,适合存储同一枚举类型的键值对。
- 高效集合操作:EnumMap 基于枚举常量的顺序实现,性能极高,适合需要高效集合操作的场景。
- 内存占用敏感:EnumMap 的内存占用非常低,适合内存占用敏感的场景。
2、不适用场景
- 非枚举类型集合:EnumMap 只能存储枚举类型的键,不适合存储非枚举类型的键。
- 需要存储 null 键:EnumMap 不允许存储 null 键。
5.9、IdentityHashMap详解
5.9.1、概述
IdentityHashMap是Java中一个特殊的Map实现类,它是基于对象引用相等性(==)而不是对象值相等性(equals)实现的哈希表。IdentityHashMap使用System.identityHashCode来计算哈希值,适合需要区分对象引用的场景。IdentityHashMap允许存储键值对,键和值都可以为null。它线程不安全,如果需要线程安全,可以使用 Collections.synchronizedMap。
- 底层数据结构:
IdentityHashMap的底层是一个Object[]数组,数组的每个元素交替存储键和值。 - 初始容量和负载因子:默认初始容量为 32。默认负载因子为 2/3。
- 动态扩容:当元素数量超过当前容量与负载因子的乘积时,IdentityHashMap 会自动扩容。
- 扩容机制:
- 新容量 = 旧容量 * 2。
- 将旧哈希表中的元素重新哈希到新哈希表中。
- 时间复杂度
- 添加元素:
O(1)(平均情况下)。 - 删除元素:
O(1)(平均情况下)。 - 查找元素:
O(1)(平均情况下)。 - 遍历元素:
O(n)。
- 添加元素:
5.9.2、开放地址法(Open Addressing)解决哈希冲突
当发生哈希冲突时,通过某种探测方法(如线性探测、二次探测、双重哈希等)在哈希表中寻找下一个空闲的位置。所有的键值对都存储在哈希表的数组中,没有额外的链表或树结构。
开放地址法的实现方式
- 线性探测(
Linear Probing):当发生冲突时,依次检查下一个位置,直到找到一个空闲的位置。- 探测公式:
h(k,i) = (h(k)+i) mod m。h(k)是哈希函数。i是探测次数(从 0 开始)。m是哈希表的大小。
- 优点
- 实现简单。
- 缓存友好,因为探测的位置是连续的。
- 缺点
- 容易产生 聚集(
Clustering)现象,即连续的冲突导致数据集中在某个区域,降低性能。
- 容易产生 聚集(
- 示例
- 假设哈希表大小为
10,哈希函数为h(k) = k mod 10:- 插入键值对
(5, A):位置5空闲,直接插入。 - 插入键值对
(15, B):位置5已被占用,检查位置6,空闲,插入。 - 插入键值对
(25, C):位置5和6已被占用,检查位置7,空闲,插入。
- 插入键值对
- 假设哈希表大小为
- 探测公式:
- 二次探测(
Quadratic Probing):当发生冲突时,通过二次函数计算下一个探测位置。- 探测公式:
h(k,i) = (h(k) + c1*i + c2*i*i) mod mc1和c2是常数。i是探测次数(从 0 开始)。m是哈希表的大小。
- 优点
- 减少线性探测的聚集现象。
- 缺点
- 仍然可能产生 二次聚集(
Secondary Clustering)。 - 需要选择合适的
c1和c2以避免探测位置重复。
- 仍然可能产生 二次聚集(
- 示例:假设哈希表大小为
10,哈希函数为h(k) = k mod 10,c1 = 1,c2 = 1:- 插入键值对
(5, A):位置5空闲,直接插入。 - 插入键值对
(15, B):位置5已被占用,检查位置(5+1+1) mod 10 = 7,空闲,插入。 - 插入键值对
(25, C):位置5已被占用,检查位置(5+1+1) mod 10 = 7,已被占用,检查位置(5+2+4) mod 10 = 1,空闲,插入
- 插入键值对
- 探测公式:
- 双重哈希(
Double Hashing):当发生冲突时,使用第二个哈希函数计算探测步长。- 探测公式:
h(k,i)=(h1(k) + i*h2(k)) mod mh1(k)是第一个哈希函数。h2(k)是第二个哈希函数。i是探测次数(从0开始)。m是哈希表的大小。
- 优点:
- 减少聚集现象,探测序列更加分散。
- 性能优于线性探测和二次探测。
- 缺点:
- 需要设计两个哈希函数。
- 实现复杂度较高。
- 举例:假设哈希表大小为 10,哈希函数为
h1(k) = k mod 10,h2(k) = 7 - (k mod 7):- 插入键值对
(5, A):位置5空闲,直接插入。 - 插入键值对
(15, B):位置5已被占用,步长为h2(15) = 7 - ( 15 mod 7 ) = 7 - 1 = 6,检查位置(5 + 6) mod 10 = 1,空闲,插入。 - 插入键值对
(25, C):位置5已被占用,步长为h2(25) = 7 − (25 mod 7 ) = 7 − 4 = 3,检查位置(5+3) mod 10 = 8,空闲,插入。
- 插入键值对
- 探测公式:
5.9.3、常用方法
put(K key, V value):添加键值对。remove(Object key):删除指定键的键值对。get(Object key):获取指定键的值。size():返回键值对数量。isEmpty():判断是否为空。containsKey(Object key):判断是否包含指定键。containsValue(Object value):判断是否包含指定值。clear():清空所有键值对。
5.9.5、使用场景
1、适用场景
- 需要区分对象引用:
IdentityHashMap使用对象引用相等性(==)而不是对象值相等性(equals),适合需要区分对象引用的场景。 - 对象生命周期管理:适合用于管理对象的生命周期,例如缓存系统中需要区分相同值的不同对象。
- 避免递归结构:当键值对中可能存在循环引用或递归结构时,
IdentityHashMap可以避免因equals()方法引发的无限递归问题。 - 图算法:在实现图算法时,可以使用
IdentityHashMap来表示节点之间的关系,避免因equals()方法导致的误判。
2、 不适用场景
- 需要对象值相等性:如果需要使用对象值相等性(
equals),不适合使用IdentityHashMap。 - 内存占用敏感:
IdentityHashMap需要额外的内存存储键值对,内存占用较高。
5.10、Collections.synchronizedMap详解
5.10.1、概述
Collections.synchronizedMap是Java提供的一个工具方法,用于将普通的 Map 转换为线程安全的同步集合。它通过包装现有的Map实现类(如HashMap或TreeMap),并为其所有方法添加同步锁,从而保证线程安全。
- 线程安全:Collections.synchronizedMap 通过为所有方法添加同步锁(synchronized)来保证线程安全。
- 底层实现:
Collections.synchronizedMap并不直接实现Map接口,而是通过包装现有的Map实现类(如HashMap或TreeMap)来提供线程安全的功能。- 它内部维护了一个
mutex对象(锁对象),所有方法都通过synchronized(mutex)实现同步。
- 性能:由于所有方法都使用了同步锁,
Collections.synchronizedMap的性能较低。在高并发场景下,可能会成为性能瓶颈。
5.10.2、使用场景
1、适用场景
- 多线程环境:当多个线程需要共享和修改同一个
Map时,可以使用Collections.synchronizedMap来保证线程安全。 - 简单的同步需求:如果只需要简单的线程安全功能,而不需要复杂的并发控制(如分段锁或读写锁),
Collections.synchronizedMap是一个简单易用的选择。 - 兼容现有代码:如果现有代码已经使用了
HashMap或TreeMap,可以通过Collections.synchronizedMap快速实现线程安全,而无需修改大量代码。
2、不适用场景
- 高并发场景:在高并发场景下,
Collections.synchronizedMap的性能较差,因为所有操作都需要竞争同一把锁。此时更适合使用ConcurrentHashMap。 - 需要高性能读操作:如果读操作远多于写操作,
Collections.synchronizedMap的性能不如ConcurrentHashMap。 - 需要复杂的并发控制:如果需要更细粒度的并发控制(如读写锁或分段锁),
Collections.synchronizedMap无法满足需求。
&spm=1001.2101.3001.5002&articleId=145676199&d=1&t=3&u=d4a82f46416f456fbc753c78c90477b9)
7502

被折叠的 条评论
为什么被折叠?



