Java集合面试题汇总
目录
Collections工具类中的sort()方法如何比较元素?
遍历一个List有哪些不同的方式,每种方法的实现原理是什么,Java中List遍历的最佳实践是什么?
插入数据时,ArrayList、LinkedList、Vector谁速度较快,阐述ArrayList、Vector、LinkedList的存储性能和特性?
为什么ArrayList的elementData加上transient修饰?
HashSet、LinkedHashSet、TreeSet区别?
HashMap为什么不直接使用hashCode()处理后的哈希值直接作为table的下标?
HashMap、HashTable、TreeMap、LinkedHashMap特点与区别?
Queue中add()、offer()、remove()、poll()、element()、peek()的区别?
ConcurrentHashMap与HashMap和Hashtable的差异?
ConcurrentLinkedQueue原理?和BlockingQueue的对比?
ConcurrentSkipListMap/ConcurrentSkipListSet特点?
多线程遍历集合为什么会抛ConcurrentModificationException?
集合概念
什么是集合?
集合是Java用来存放、管理对象数据的容器,位于java.util包下。
数组和集合的区别
- 数组可以存数据,但长度固定、操作麻烦;
-
集合就是长度可变、功能更强大的“动态数组/容器”。
核心特点
- 只能存储引用类型(对象),不能存基本数据类型(要存要用包装类)
- 容量动态变化,用多少自动扩多少
- 提供丰富的API:增、删、改、查、遍历、判断等
- 有多种结构,适应不同场景(List、Set、Map等)
集合与数组的区别
- 数组:固定长度,可存基本类型,功能单一
- 集合:可变长度,只能存对象,方法丰富
使用集合框架的好处?
1.容量动态扩容
不用预先固定长度,随数据自动扩容,比数组灵活。
2.丰富的数据结构实现
提供List、Set、Map、Queue等多种实现,直接使用,无需自己手写。
3.完善的API,简化开发
内置增删改查、排序、遍历、批量操作等方法,代码更简洁。
4.高性能优化
底层经过精心设计(如哈希表、动态数组、红黑树),查询、插入效率高。
5.工具类支持
配合Collections、Arrays实现排序、同步、反转等通用操作。
6.泛型支持,类型安全
编译期检查类型,避免类型转换异常。
7.线程安全实现
提供ConcurrentHashMap、CopyOnWriteArrayList等,适配多线程场景。
8.统一标准,易于维护
一套通用接口和设计规范,代码可读性、可移植性更强。
Collection和Collections有什么区别?
1.Collection
- 是父接口(java.util.Collection)
- 是List、Set、Queue的顶层父接口
- 定义了单列集合的通用方法:add、remove、clear、contains、size等
- 本身是接口,不能直接实例化
2.Collections
- 是一个工具类(java.util.Collections)
- 专门用来操作集合的静态工具方法
- 提供很多静态方法:sort、shuffle、reverse、binarySearch、synchronizedCollection等
- 构造器私有,不能实例化,直接调用静态方法
Iterator是什么?有什么作用?
是什么
- Iterator叫迭代器,是Java集合框架中的一个接口。
- 位于java.util.Iterator
- 用来**遍历集合(Collection系列:List、Set等)**中的元素。
核心作用
1.统一遍历方式
不管是ArrayList、LinkedList还是HashSet,都用同一种迭代方式:hasNext()→next()
2.安全遍历并删除元素
在遍历过程中,通过迭代器的remove()方法删除元素,不会抛并发修改异常。
3.隐藏集合内部结构
使用者不用关心集合是数组还是链表,直接迭代即可。
常用方法
- hasNext():判断是否还有下一个元素
- next():获取下一个元素
- remove():删除刚遍历过的元素
简单示例
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String s = it.next();
System.out.println(s);
}
扩展
- 增强for循环底层就是Iterator
- 不能用集合自身的remove()遍历删除,否则会触发ConcurrentModificationException(fail-fast机制)
- ListIterator是List特有的迭代器,支持向前遍历、添加、修改元素
Iterable和Iterator区别?
定位
- Iterable是“可迭代的”,表示这个集合具备被遍历的资格。
- Iterator是“迭代器”,是真正执行遍历的工具。
区别
1.定义不同
-
Iterable:是一个接口,java.lang.Iterable
-
Iterator:是一个接口,java.util.Iterator
2.核心方法不同
-
Iterable:只有一个方法:
Iterator<T> iterator();
作用:返回一个迭代器对象。
-
Iterator 有三个核心方法:
boolean hasNext();
T next();
void remove();
作用:真正用来遍历、取下一个元素、删除元素。
3.功能定位不同
-
Iterable:表示“这个集合可以被迭代”→所有Collection(List/Set/Queue)都实现了Iterable
-
Iterator:表示“用来迭代的工具”→每次迭代都会创建一个新的Iterator实例
4.和增强for循环的关系
-
只有实现了Iterable的类,才能用:
for (T t : 集合) { ... }
底层就是通过iterator()获取迭代器来遍历。
常用的集合类有哪些?
单列集合Collection
1.List体系(有序、可重复)
- ArrayList
- LinkedList
- Vector(老、线程安全,现在少用)
- CopyOnWriteArrayList(并发)
2.Set体系(无序、不可重复)
- HashSet
- LinkedHashSet
- TreeSet
双列集合Map(key-value)
- HashMap
- LinkedHashMap
- TreeMap
- HashTable(老、线程安全)
- ConcurrentHashMap(高并发常用)
List,Set,Map的区别,存取元素时,各有什么特点?
体系
- List、Set继承自Collection(单列集合,存单个元素)
- Map是独立接口(双列集合,存key-value键值对)
核心区别
1.List(有序、可重复、有索引)
特点:
- 元素有序(存入顺序=取出顺序)
- 元素可重复
- 有下标索引,可以通过index操作
常见实现:
- ArrayList(数组,查询快)
- LinkedList(链表,增删快)
存取特点:
- 存:直接添加,允许重复
- 取:可按索引get(index)取,也可遍历
2.Set(无序、不可重复、无索引)
特点:
- 元素无序(存取顺序不一定一致)
- 元素不可重复(自动去重)
- 没有索引,不能通过下标访问
常见实现:
- HashSet(哈希表,无序)
- TreeSet(红黑树,可排序)
- LinkedHashSet(有序+去重)
存取特点:
- 存:自动根据hashCode+equals去重
- 取:只能遍历,不能按位置取
3.Map(键值对、key唯一、value可重复)
特点:
- 存储key-value键值对
- key不可重复,value可重复
- key无序(取决于实现类)
常见实现:
- HashMap(哈希表,线程不安全)
- TreeMap(可排序)
- LinkedHashMap(保证插入顺序)
- ConcurrentHashMap(线程安全)
存取特点:
- 存:put(key,value),key重复会覆盖旧value
- 取:通过key快速get(key)查找value
总结
- List:有序、可重复、有索引,查找方便
- Set:无序、不可重复、无索引,用于去重
- Map:存储键值对,key唯一,适合快速查找
集合框架底层数据结构?
List
- ArrayList:动态数组
- LinkedList:双向链表(JDK6是循环,JDK7+非循环)
- Vector:动态数组(线程安全,效率低)
Set
- HashSet:HashMap(把元素存在key上)
- LinkedHashSet:LinkedHashMap(哈希表+双向链表)
- TreeSet:TreeMap(红黑树)
Map
- HashMap:JDK7:数组+链表,JDK8+:数组+链表/红黑树(链表长度≥8转红黑树,≤6退链表)
- LinkedHashMap:HashMap+双向链表(保证有序)
- TreeMap:红黑树(可排序)
- Hashtable:数组+链表(线程安全,低效)
- ConcurrentHashMap:JDK7:分段锁(Segment)+数组+链表,JDK8+:数组+链表/红黑树+CAS+synchronized
Queue
- ArrayDeque:动态数组
- PriorityQueue:堆(优先级队列)
哪些集合类是线程安全的?
1.传统线程安全集合
- Vector
- Stack(继承自Vector)
- Hashtable
它们的方法都加了synchronized,但粒度粗、并发性能差,现在基本不推荐使用。
2.并发包下高性能线程安全集合
java.util.concurrent下的,高并发首选
List
- CopyOnWriteArrayList
Set
- CopyOnWriteArraySet
- ConcurrentSkipListSet(有序)
Map
- ConcurrentHashMap
- ConcurrentSkipListMap(有序)
3.通过Collections工具类获得的安全集合
可以把普通集合变成线程安全的:
Collections.synchronizedList(new ArrayList<>());
Collections.synchronizedSet(new HashSet<>());
Collections.synchronizedMap(new HashMap<>());
底层也是用synchronized,性能一般。
介绍下fail-fast与fail-safe区别?
1.概念区别
fail-fast(快速失败)
迭代器遍历集合时,一旦检测到集合被修改(增/删/改),立刻抛出ConcurrentModificationException,不继续执行。
fail-safe(安全失败)
迭代器遍历的是原集合的副本,遍历时修改原集合不会影响遍历,不抛异常,但可能读到旧数据。
2.底层原理
fail-fast
- 遍历前记录集合的modCount(修改次数)
- 每次遍历都对比modCount是否变化
- 变化→直接抛异常
fail-safe
- 不直接操作原集合,而是拷贝一份快照(clone)遍历
- 原集合修改不影响快照,因此不会抛并发修改异常
3.代表集合
fail-fast
ArrayList、HashMap、HashSet等非线程安全集合
fail-safe
JUC包下的并发集合:
- ConcurrentHashMap
- CopyOnWriteArrayList
- CopyOnWriteArraySet
4.优缺点对比
| 特性 | fail-fast | fail-safe |
|---|---|---|
| 异常 | 抛ConcurrentModificationException | 不抛异常 |
| 遍历对象 | 直接遍历原集合 | 遍历集合副本 |
| 数据一致性 | 强一致性,不允许脏数据 | 弱一致性,可能读到旧数据 |
| 内存开销 | 小 | 大(需要拷贝数组/结构) |
| 适用场景 | 单线程、强一致性场景 | 高并发、允许短暂不一致的场景 |
怎么确保一个集合不能被修改?
1.使用Collections.unmodifiableXXX(最常用)
包装成不可修改视图,任何增删改都会抛UnsupportedOperationException。
List<String> list = new ArrayList<>();
List<String> unmodList = Collections.unmodifiableList(list);
Set<String> set = Collections.unmodifiableSet(new HashSet<>());
Map<String, Object> map = Collections.unmodifiableMap(new HashMap<>());
2.使用JDK9+集合工厂方法of()
直接创建不可变集合,本身就不能增删改:
List<String> list = List.of("a", "b");
Set<String> set = Set.of("a", "b");
Map<String, String> map = Map.of("k1", "v1", "k2", "v2");
3.使用Guava的ImmutableXXX(第三方)
ImmutableList<String> list = ImmutableList.of("a", "b");
ImmutableMap<String, String> map = ImmutableMap.of("k", "v");
要点
- 这些都是集合不可修改,但集合里的对象本身还是可以改(如果对象可变)。
- 想完全不可变,还要保证元素是不可变对象(如String、包装类)。
如何边遍历边移除Collection中的元素?
正确方式只有一种:使用迭代器Iterator的remove()方法
其他方式(普通for、增强for)都会抛出ConcurrentModificationException。
1.正确方式:Iterator迭代器
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String s = it.next();
if ("b".equals(s)) {
// 必须用迭代器的 remove()
it.remove();
}
}
2.错误方式(必抛异常)
①增强for循环(foreach)
for (String s : list) {
list.remove(s); // 抛 ConcurrentModificationException
}
②普通for循环(容易漏元素、下标越界)
for (int i = 0; i < list.size(); i++) {
list.remove(i); // 下标错乱,漏删
}
3.JDK8+优雅写法:removeIf
list.removeIf(s -> "b".equals(s));
底层也是用迭代器,推荐使用。
Comparable和Comparator区别?
定位
- Comparable:内部比较器让类自己实现接口,定义默认排序规则。
- Comparator:外部比较器单独写一个比较器,定义临时/灵活排序规则,不改动原类。
接口位置与方法
Comparable
- 包:java.lang.Comparable
- 方法:int compareTo(T o)
- 类自己实现:class User implements Comparable<User>
Comparator
- 包:java.util.Comparator
- 方法:int compare(T o1, T o2)
- 通常以匿名内部类/Lambda使用
使用方式
Comparable(自然排序)
Collections.sort(list, new Comparator<User>() {
@Override
public int compare(User u1, User u2) {
return u2.getAge() - u1.getAge(); // 按年龄降序
}
});
使用:
Collections.sort(list);
Comparator(定制排序)
Collections.sort(list, new Comparator<User>() {
@Override
public int compare(User u1, User u2) {
return u2.getAge() - u1.getAge(); // 按年龄降序
}
});
Lambda简化:
Collections.sort(list, (u1, u2) -> u2.getAge() - u1.getAge());
优先级
Comparator优先级高于Comparable,同时存在时,以Comparator为准。
优缺点对比
| 对比项 | Comparable | Comparator |
|---|---|---|
| 类型 | 内部比较器 | 外部比较器 |
| 侵入性 | 强,必须修改实体类 | 无侵入,不改动原类 |
| 排序规则 | 只有一种默认排序 | 灵活,可定义多种排序 |
| 实现位置 | 实体类内部 | 外部单独实现 |
总结
Comparable是类自己实现的默认排序(compareTo);Comparator是外部定制排序(compare),灵活无侵入,优先级更高。
Collections工具类中的sort()方法如何比较元素?
比较方式
版本1:自然排序,依靠元素自身的Comparable接口
Collections.sort(list);
要求:元素必须实现Comparable接口
重写方法:compareTo(T o)
规则:
- 返回负数:当前对象小→排在前面
- 返回正数:当前对象大→排在后面
- 返回0:相等
适用类:String、Integer、Long、Date等都已实现。
版本2:定制排序(比较器排序),传入Comparator比较器
Collections.sort(list, comparator);
不需要元素实现任何接口
重写方法:compare(T o1, T o2)
规则:
- 返回负数:o1比o2小
- 返回正数:o1比o2大
- 返回0:相等
优先级:Comparator>Comparable
排序算法
JDK6:MergeSort(归并排序)
JDK7+:
- 对象集合:TimSort(优化版归并)
- 基本类型数组:DualPivotQuickSort(双轴快排)
时间复杂度:O(nlogn)
总结
Collections.sort()有两种比较方式:
1.自然排序:元素实现Comparable接口,重写compareTo()
2.定制排序:传入Comparator比较器,重写compare()
比较器优先级高于自然排序。
List
List接口有什么特点?
- 继承自Collection:是单列有序集合的顶层接口。
- 元素存取有序:插入顺序与遍历顺序一致。
- 元素可重复:允许存储相同内容的对象。
- 带有索引:支持通过int类型下标对元素进行访问、插入、替换、删除操作。
- 提供特有遍历方式:支持普通for循环、ListIterator双向迭代器。
- 功能丰富:包含add、get、set、remove、indexOf、subList等索引相关方法。
- 常用实现类:ArrayList、LinkedList、Vector。
- 线程不安全:默认实现都非线程安全,多线程需使用并发包对应类。
遍历一个List有哪些不同的方式,每种方法的实现原理是什么,Java中List遍历的最佳实践是什么?
Java中遍历List的5种方式
1.普通for循环(通过索引)
for (int i = 0; i < list.size(); i++) {
String s = list.get(i);
}
原理:
- 依靠索引下标,直接调用get(i)获取元素
- ArrayList效率极高(数组随机访问)
- LinkedList效率极低(链表每次都从头遍历找节点)
2.增强for循环(foreach)
for (String s : list) {
}
原理:
- 底层是Iterator迭代器的语法糖
- 编译后自动变成迭代器代码
- 简洁、易用,但不能增删元素
3.Iterator迭代器
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String s = it.next();
}
原理:
- 统一的集合遍历方式,不依赖索引
- 底层用modCount检测并发修改
- 唯一支持遍历中删除的方式
4.ListIterator双向迭代器
ListIterator<String> lit = list.listIterator();
while (lit.hasNext()) {
String s = lit.next();
}
原理:
- Iterator的子接口,支持向前/向后遍历
- 支持遍历中add、set、remove操作
- 仅List集合有
5.JDK8+forEach+Lambda
list.forEach(s -> { ... });
原理:
- 基于函数式接口,内部用迭代器实现
- 代码极简,适合简单遍历
5种方式对比
| 方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 普通for | 最快、可操作索引 | 代码繁琐,LinkedList极慢 | ArrayList随机访问 |
| 增强for | 简洁易读 | 不能增删,无法获取索引 | 简单遍历 |
| Iterator | 可删除、通用 | 代码稍多 | 遍历中删除元素 |
| ListIterator | 双向遍历、增删改 | 仅List支持 | 需要增删改、双向遍历 |
| Lambda+forEach | 极简、优雅 | 无法控制流程(break/continue) | JDK8+简单遍历 |
最佳实践
1.通用首选:增强for循环
- 最简洁、可读性最高
- 适合绝大多数只读取、不修改的场景
2.ArrayList追求性能:普通for
- 数组结构,随机访问极快
- 大数据量优先使用
3.LinkedList任何情况:迭代器/增强for
- 绝对不要用普通for,性能极差
4.遍历中要删除:Iterator/removeIf
- 唯一安全方式
5.JDK8+:LambdaforEach
- 简单遍历优先使用,代码最优雅
ArrayList的优缺点?
优点
- 底层是动态数组,支持随机访问,通过索引get(i)速度极快,时间复杂度O(1)。
- 遍历速度快,数组内存连续,对CPU缓存友好。
- 尾部添加元素效率高,不需要移动元素。
- 使用简单、通用性强,是日常开发最常用的集合。
- 按索引查找、更新元素非常方便。
缺点
1.插入、删除效率低
非尾部插入/删除需要移动后续元素,时间复杂度O(n)。
2.扩容有开销
满了会按1.5倍扩容,复制数组,耗时且可能浪费内存。
3.内存连续要求高
大数据量时可能难以找到大块连续内存空间。
4.线程不安全
多线程并发修改会出现数据错乱或并发修改异常。
5.只能存引用类型
不能直接存基本数据类型。
如何实现数组和List之间的转换?
数组→List
1.Arrays.asList()(最常用)
String[] arr = {"a", "b", "c"};
List<String> list = Arrays.asList(arr);
⚠️注意:
- 返回的是Arrays内部类的List,不是ArrayList
- 不能增删,调用add/remove会抛UnsupportedOperationException
- 数组和List共用同一内存,改数组会影响List
2.真正转成ArrayList(推荐)
List<String> list = new ArrayList<>(Arrays.asList(arr));
3.JDK8+Stream方式
List<String> list = Arrays.stream(arr).collect(Collectors.toList());
List→数组
1.toArray()方法(推荐带泛型)
List<String> list = new ArrayList<>();
// 方式1:带类型(正确)
String[] arr = list.toArray(new String[0]);
// 方式2:无类型,需要强转(不推荐)
String[] arr = (String[]) list.toArray();
ArrayList和LinkedList的区别是什么?
1.底层数据结构不同
- ArrayList:动态数组
- LinkedList:双向链表(JDK7+非循环)
2.访问(查询)效率不同
- ArrayList:支持随机访问,get(i)时间复杂度O(1),查询快
- LinkedList:不支持随机访问,查找需要从头遍历,复杂度O(n),查询慢
3.增删效率不同
ArrayList:
- 尾部增删快
- 中间/头部增删需要移动元素,慢
LinkedList:
- 已知节点时,增删只改引用,极快
- 但查找节点仍需遍历,整体不一定更快
4.内存占用不同
- ArrayList:内存连续,占用空间少,只是扩容时可能有冗余
- LinkedList:每个节点除数据外,还要存prev、next指针,内存开销更大
5.支持功能不同
- ArrayList:只实现List接口
- LinkedList:同时实现List、Deque接口,可做队列、双端队列、栈
6.线程安全
两者都线程不安全,多线程需使用CopyOnWriteArrayList或加锁
总结
ArrayList底层是数组,查询快、增删慢、内存紧凑;
LinkedList底层是双向链表,查询慢、增删快、内存开销大。
ArrayList和Vector的区别是什么?
1.线程安全
- ArrayList:线程不安全,方法没有加synchronized
- Vector:线程安全,所有方法都加了synchronized
2.效率
- ArrayList:性能高,适合单线程
- Vector:性能低,高并发场景锁竞争严重
3.扩容机制
- ArrayList:默认扩容为1.5倍
- Vector:默认扩容为2倍(也可手动指定扩容增量)
4.使用场景
- ArrayList:日常开发首选
- Vector:基本废弃,多线程用CopyOnWriteArrayList
5.支持的迭代器
- ArrayList支持fast-fail迭代器
-
Vector也支持,但两者都不适合高并发
总结
ArrayList线程不安全、效率高、扩容1.5倍;
Vector线程安全、效率低、扩容2倍,现在基本不用。
ArrayList扩容机制?
1.底层结构
ArrayList底层基于Object[]数组实现,容量就是数组的长度。
2.初始化逻辑
- 使用无参构造newArrayList<>()时,初始数组为空数组{},并不会立即分配容量。
- 第一次调用add()添加元素时,才会将数组扩容并初始化为默认容量10。
3.扩容触发条件
每次执行add/addAll时,会先判断:当前元素个数+新增元素个数>数组长度满足则触发扩容。
4.扩容计算规则
新容量计算公式:新容量=旧容量+旧容量>>1等价于扩容为原容量的1.5倍。
如果计算后的新容量仍不满足需求,则直接扩容到所需的最小容量。
5.扩容实现方式
通过Arrays.copyOf()方法创建一个新的更大数组,并将原数组中的所有元素复制到新数组中,最后将底层数组引用指向新数组。
6.性能与优化
- 扩容涉及数组复制,属于耗时操作,频繁扩容会降低效率。
-
若能预估数据量,建议在创建时指定初始容量,避免多次自动扩容。
插入数据时,ArrayList、LinkedList、Vector谁速度较快,阐述ArrayList、Vector、LinkedList的存储性能和特性?
1.插入速度对比
- 头部/中间插入:LinkedList最快,ArrayList、Vector较慢(需要移动数组元素)
- 尾部插入:ArrayList≈LinkedList略快于Vector(Vector有synchronized锁开销)
- 批量插入:ArrayList/Vector一次性扩容后复制,通常比LinkedList快
2.存储性能与特性完整总结
ArrayList
- 底层结构:动态Object数组
- 访问特性:支持随机访问,get(i)时间复杂度O(1),查询极快
- 插入删除:
- 尾部增删快
- 头部/中间增删需要移动后续元素,效率低O(n)
- 扩容机制:默认容量10,满后扩容为原容量的1.5倍
- 内存:内存连续、占用小,无额外指针开销
- 线程安全:非线程安全
- 适用场景:读多写少、频繁查询、尾部增删为主
Vector
- 底层结构:动态Object数组(与ArrayList几乎一致)
- 访问特性:支持随机访问,get(i)O(1)
- 插入删除:效率同ArrayList,但方法加锁更慢
- 扩容机制:默认容量10,满后扩容为原容量的2倍
- 内存:内存连续,扩容倍数更大,内存浪费相对更多
- 线程安全:线程安全(方法上加synchronized)
- 性能:锁竞争严重,并发性能差
- 适用场景:基本废弃,多线程场景优先用CopyOnWriteArrayList
LinkedList
- 底层结构:双向链表
- 访问特性:不支持随机访问,查找需遍历,O(n)
- 插入删除:
- 已知节点时增删仅修改引用,效率极高O(1)
- 查找节点仍需遍历,实际不一定更快
- 扩容机制:无扩容,按需创建节点
- 内存:每个节点存data+prev+next,内存开销大
- 线程安全:非线程安全
- 适用场景:频繁头尾增删、用作队列/栈/双端队列
3.核心总结
- 查询速度:ArrayList≈Vector>LinkedList
- 头部/中间插入:LinkedList最快
- 内存效率:ArrayList>Vector>LinkedList
- 线程安全:Vector安全,另两个不安全
- 日常开发首选:ArrayList
多线程场景下如何使用ArrayList?
1.使用Collections.synchronizedList包装
List<String> safeList = Collections.synchronizedList(new ArrayList<>());
- 原理:对每个方法加
synchronized锁 - 优点:简单、兼容原有逻辑
- 缺点:锁粒度大,高并发下性能一般
2.使用CopyOnWriteArrayList(推荐)
List<String> safeList = new CopyOnWriteArrayList<>();
- 属于JUC并发包,读无锁、写时复制新数组
- 适合:读多写少的高并发场景
- 优点:并发性能远好于synchronized方案
- 缺点:写开销大,数据弱一致性
3.手动加锁(synchronized/Lock)
synchronized (lock) {
arrayList.add(xxx);
}
- 灵活控制锁范围,适合需要复合操作的场景
- 缺点:代码侵入性强,容易出错
4.使用线程安全的替代集合
- 高并发读:CopyOnWriteArrayList
- 需要强一致性、高并发写:考虑用队列或其他并发结构
对比与选择
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| Collections.synchronizedList | 简单、兼容ArrayList | 并发性能差 | 低并发、简单同步需求 |
| CopyOnWriteArrayList | 读性能极高、线程安全 | 写开销大、弱一致性 | 读多写少、高并发 |
| 手动加锁 | 灵活控制锁粒度 | 代码复杂、易出错 | 复杂复合操作 |
三、最佳实践
- 低并发、简单场景:用Collections.synchronizedList
- 高并发、读多写少:优先使用CopyOnWriteArrayList
- 不建议在多线程直接使用原生ArrayList,会出现线程安全问题
为什么ArrayList的elementData加上transient修饰?
1.明确两个关键点
elementData是ArrayList底层存储数据的数组:
transient Object[] elementData;
transient关键字作用:被修饰的成员变量不会被Java默认序列化机制序列化。
2.为什么要加transient?
核心原因:节省空间+避免序列化无用数据
ArrayList的elementData是一个动态扩容数组,它的长度(容量)通常大于实际元素个数(size)。
- 数组里很多位置是null(未使用的空间)
- 如果不使用transient,序列化时会把整个数组+所有null值都写出去
- 导致序列化体积变大、速度变慢、浪费大量IO
设计目的:只序列化真正有数据的元素,不序列化空位置。
3.元素如何序列化?
ArrayList自己重写了:
- writeObject()
- readObject()
序列化流程
- elementData被transient修饰→不参与默认序列化
- 序列化时,只遍历size个有效元素写入流
- 反序列化时,再把元素读回来重建数组
这样既安全又高效,只存有用数据。
Set
Set接口有什么特点?
1.继承自Collection,属于单列集合,存储单个元素。
2.不允许元素重复:保证存储的元素唯一性,通过hashCode()和equals()共同判断。
3.大部分实现类无序:存入顺序和取出顺序不保证一致(LinkedHashSet除外)。
4.无索引:不提供通过下标访问元素的方法,不能使用普通for循环遍历。
5.最多存一个null:只允许存入一个null元素,避免重复。
6.遍历方式:只能通过增强for、迭代器、forEach遍历。
7.常用实现类:
- HashSet:底层HashMap,无序,性能最高
- LinkedHashSet:底层LinkedHashMap,保证插入顺序
- TreeSet:底层TreeMap,可自然排序或定制排序
8.线程不安全:默认实现均非线程安全,多线程用CopyOnWriteArraySet或ConcurrentSkipListSet。
HashSet的实现原理?
1.底层依赖
HashSet底层实际是HashMap,HashSet只是在使用HashMap的key存储元素,value统一存一个固定的空对象。
// HashSet 源码
private transient HashMap<E,Object> map;
// 所有 key 对应的 value 都是这个静态空对象
private static final Object PRESENT = new Object();
2.存储结构
- JDK7:哈希表(数组+链表)
- JDK8+:哈希表(数组+链表/红黑树)
3.添加元素原理(add方法)
1.调用add(e)时,实际执行:
map.put(e, PRESENT);
2.根据元素的hashCode()计算哈希值,确定数组下标位置。
3.如果该位置没有元素,直接存入。
4.如果该位置已有元素,通过equals()比较是否相同:
- 相同:视为重复元素,不存入,返回false。
- 不同:挂在链表后面,链表长度≥8转为红黑树。
5.存入成功返回true。
4.唯一性保证机制
元素不重复依靠两个方法:
- hashCode():定位存储位置
- equals():判断是否为同一个元素
要让自定义对象能正确去重,必须同时重写hashCode和equals。
5.基本特性
- 无序:不保证插入顺序与遍历顺序一致
- 无索引:不能通过下标访问
- 不可重复
- 允许存一个null
- 线程不安全
- 查找、添加、删除平均时间复杂度O(1)
6.扩容机制
完全继承HashMap的扩容规则:
- 默认初始容量16,负载因子0.75
- 元素个数达到容量×负载因子时,扩容为原来2倍
- 重新哈希,重新分配所有元素位置
HashSet、LinkedHashSet、TreeSet区别?
1.底层结构
- HashSet:底层是HashMap(数组+链表/红黑树)
- LinkedHashSet:底层是LinkedHashMap(HashMap+双向链表)
- TreeSet:底层是TreeMap(红黑树)
2.顺序性
- HashSet:无序,不保证插入顺序,也不保证自然顺序
- LinkedHashSet:有序,按照元素插入顺序遍历
- TreeSet:有序,按照元素的自然排序(Comparable)或定制排序(Comparator)遍历
3.元素要求
- HashSet/LinkedHashSet:元素只需重写hashCode()+equals()保证去重
- TreeSet:元素必须可比较,要么实现Comparable接口,要么传入Comparator比较器,否则会报ClassCastException
4.是否允许null
- HashSet、LinkedHashSet:允许存入一个null
- TreeSet:不允许null(比较时会抛空指针异常)
5.性能
- HashSet:查询、增删最快,O(1)
- LinkedHashSet:略慢于HashSet,因为要维护双向链表
- TreeSet:性能最低,O(logn),红黑树排序开销大
6.线程安全
三者均线程不安全,多线程用CopyOnWriteArraySet或ConcurrentSkipListSet
7.使用场景
- HashSet:只需要去重、不关心顺序,追求最高性能
- LinkedHashSet:需要去重+保持插入顺序
- TreeSet:需要去重+自动排序(升序/降序)
TreeSet如何排序?
底层原理
TreeSet底层是TreeMap,基于红黑树实现,会自动对所有元素进行排序,保证遍历出来的结果是有序的。
排序方式
TreeSet支持自然排序和定制排序,必须二选一,否则会抛异常。
1.自然排序(默认方式)
要求:元素类必须实现Comparable接口,重写compareTo()方法。
实现步骤
- 自定义类实现Comparable<T>
- 重写compareTo方法指定比较规则
public class User implements Comparable<User> {
private String name;
private int age;
// 按年龄升序
@Override
public int compareTo(User o) {
return this.age - o.age;
}
}
排序规则
- 返回负数:当前对象小,放左边
- 返回正数:当前对象大,放右边
- 返回0:视为重复元素,不存入
2.定制排序(灵活排序)
不需要修改元素类,在创建TreeSet时传入Comparator比较器。
TreeSet<User> set = new TreeSet<>(new Comparator<User>() {
@Override
public int compare(User u1, User u2) {
// 按年龄降序
return u2.getAge() - u1.getAge();
}
});
优先级:Comparator定制排序>Comparable自然排序
TreeSet如何保证有序+唯一
- 不使用equals()、hashCode()
- 完全依靠比较器(compareTo/compare)
- 返回0就判定为重复元素,不会存入
特点总结
- 有序:自动升序/降序排列
- 唯一:根据比较结果去重
- 不允许null:比较会抛空指针
- 底层红黑树,增删改查效率O(logn)
- 必须可比较,否则无法使用
Map
Map接口有什么特点?
1.顶层独立接口,不属于Collection体系,和Collection平级。
2.存储键值对(key-value),一个key对应一个value。
3.key不允许重复,重复key会覆盖旧value;value可以重复。
4.key最多允许一个null,value可以有多个null。
5.无序:大部分实现不保证插入顺序,也不保证排序(LinkedHashMap、TreeMap除外)。
6.没有索引,不能通过下标访问,只能通过key操作数据。
7.存取方式:
- 存:put(key,value)
- 取:get(key)
- 删:remove(key)
8.遍历方式:遍历keySet、遍历values、遍历entrySet。
9.线程不安全:常用实现类(HashMap、LinkedHashMap、TreeMap)均非线程安全,多线程用ConcurrentHashMap。
10.常用实现类:
- HashMap:无序、性能最高
- LinkedHashMap:保持插入顺序
- TreeMap:按键自动排序
- Hashtable:线程安全、性能低
HashMap的实现原理?
一、底层结构
- JDK7:数组+链表
- JDK8:数组+链表+红黑树
- 数组是主体,称为哈希表/桶数组,链表用于解决哈希冲突,红黑树用于提高冲突过多时的查询效率。
二、存储过程(put流程)
1.根据key的hashCode()计算哈希值,再做扰动处理,得到最终哈希。
2.通过(数组长度-1)&hash计算数组下标,定位到对应桶位置。
3.如果该位置为空,直接将节点存入。
4.如果该位置已有元素:
- 先比较key是否相同(==或equals),相同则覆盖value。
- 不同则形成链表,JDK8采用尾插法。
5.链表长度达到8且数组长度≥64时,链表转为红黑树。
6.红黑树节点数减少到6时,会退化为链表。
7.元素数量达到容量×负载因子(0.75)时,触发扩容。
三、核心参数
- 默认初始容量:16
- 负载因子:0.75
- 扩容阈值:容量×0.75
- 扩容倍数:每次扩容为原来的2倍
- 树化阈值:链表长度≥8
- 退化阈值:红黑树节点≤6
四、哈希冲突解决
- 采用链地址法:相同下标位置用链表连接。
- JDK8加入红黑树优化,避免长链表导致查询效率退化到O(n)。
五、扩容机制
- 创建新数组,长度为原数组2倍。
- 重新计算所有元素的下标。
- 元素要么在原位置,要么移动到原下标+原容量的位置。
- JDK8扩容时保持链表相对顺序,避免倒置,减少并发问题。
六、key相关规则
- 允许key为null,固定存在下标0位置。
- 唯一性依靠:hashCode()+equals()。
- 自定义对象作为key必须同时重写这两个方法,否则无法正确存取。
七、线程安全
- HashMap线程不安全。
- 多线程环境下推荐使用ConcurrentHashMap。
八、时间复杂度
- 正常情况:增删改查O(1)
- 哈希冲突严重:链表O(n),红黑树O(logn)
HashMap在JDK1.7和JDK1.8中有哪些不同?
1.底层数据结构不同
- JDK1.7:数组+链表
- JDK1.8:数组+链表+红黑树链表长度≥8且数组长度≥64时转为红黑树,节点≤6时退化为链表。
2.哈希冲突时链表插入方式不同
- JDK1.7:头插法(新元素放在数组位置,旧节点接在后面)高并发扩容下可能形成环形链表,导致死循环。
- JDK1.8:尾插法(新节点加到链表末尾)扩容时保持顺序,不会形成环形链表。
3.哈希扰动算法不同
- JDK1.7:进行4次位移+异或扰动,计算复杂。
- JDK1.8:简化为1次高16位异或低16位hash=key.hashCode()^(key.hashCode()>>>16),效率更高,散列更均匀。
4.扩容后数据存储位置计算
- JDK1.7:重新全部哈希计算索引。
- JDK1.8:按原索引或原索引+旧容量两种位置,只需判断新bit位,不用重新全哈希,效率更高。
5.效率表现
- JDK1.7:冲突严重时链表过长,查询退化为O(n)。
- JDK1.8:使用红黑树,最坏情况O(logn),查询效率大幅提升。
6.线程安全隐患
- JDK1.7:多线程扩容头插法会链表成环,CPU100%。
- JDK1.8:解决了环形链表问题,但仍非线程安全,多线程仍要用ConcurrentHashMap。
能否使用任何类作为Map的key?
理论上可以用任意类作为Map的key,但能不能正常工作,取决于这个类是否遵守了key的设计规范。
一、能不能用?
- 语法上:任何类都能作为Map的key(因为key类型是Object)。
- 功能上:不是所有类都适合,用不好会出现数据找不到、内存泄漏、无法去重等问题。
二、作为Map的key必须满足什么条件?
以最常用的HashMap为例:
1.必须正确重写hashCode()和equals()
存入时:根据hashCode()计算位置
比较重复时:用equals()判断是否是同一个key
如果不重写:
- hashCode()默认用对象地址计算
- equals()默认用==判断→new出来的两个内容相同的对象,会被当成不同key,数据存了找不到。
2.建议类是不可变类(Immutable)
比如String、Integer、Long都是不可变的。
如果对象是可变的:
- 存入后修改属性→hashCode()变了
- 导致该key所在数组下标“漂移”→这个key-value就永远丢失、无法获取也无法删除。
3.不能直接用数组作为key
数组默认没有重写hashCode()和equals(),会直接按对象地址判断,极其容易出问题。
三、哪些类非常适合做key?
String(最常用)
Integer、Long、Double等包装类
自定义不可变类:
- final类
- 成员变量privatefinal
- 只提供getter,不提供setter
- 正确重写hashCode()+equals()
四、总结
- 语法上可以用任何类作为Map的key,但不是所有类都适合。
- 要保证正常存取,必须重写hashCode()和equals(),保证相等对象有相同哈希码。
- 强烈建议使用不可变类,避免key状态改变导致哈希值变化,使键值对“丢失”。
- 实际开发优先使用String、Integer等标准不可变类。
HashMap为什么不直接使用hashCode()处理后的哈希值直接作为table的下标?
1.hashCode返回值范围太大,数组装不下
-
Object.hashCode()返回的是int类型,范围是:-2³¹~2³¹-1,大约40亿的范围。
-
而HashMap底层数组初始容量只有16,后面扩容也是32、64、128……
-
直接用hashCode当下标,绝大多数位置都会越界,根本无法存入数组。
2.直接使用会导致大量下标越界
数组下标必须是[0,length-1]之间的整数。hashCode可能是负数、也可能远大于数组长度,不能直接用。
3.哈希值分布不够均匀,容易造成大量冲突
- 很多对象的hashCode()实现质量一般,高位变化大、低位变化小。
- 如果只简单取余,冲突会非常多,链表变长,效率下降。
所以HashMap做了两步优化:
- 扰动函数:hash=key.hashCode()^(hash>>>16),让高16位也参与运算,让哈希结果更分散。
- 下标计算:index=hash&(table.length-1),等价于取模,但位运算更快,且保证下标在合法范围内。
HashMap的长度为什么是2的幂次方?
1.让下标计算更高效,用&代替%取模
- 计算数组下标公式:index = hash & (length - 1)
- 当且仅当length是2的幂时:hash % length == hash & (length - 1)
- 位运算&比取模运算%快得多,能大幅提升性能。
2.保证下标分布均匀,减少哈希冲突
- 2的幂对应的二进制是100…0,length-1就是011…1
- 做&运算时,hash的低位会被完整保留,分布更均匀
- 如果不是2的幂,length-1二进制末尾会出现0,会导致部分数组位置永远用不到,冲突加剧
3.扩容时重新计算下标更简单高效
- 扩容为原来2倍(仍是2的幂)
- 元素新下标只有两种可能:
- 原下标位置不变
- 新下标=原下标+原容量
- 只需判断hash新增的那个高位是0还是1即可,不用重新全量哈希,提升扩容效率
4.方便扩容与移位操作
- 扩容始终是<<1移位操作,简单高效
- 保证新数组长度依旧是2的幂,维持上述所有优点
HashMap、HashTable、TreeMap、LinkedHashMap特点与区别?
一、底层结构
- HashMap:数组+链表+红黑树(JDK8)
- Hashtable:数组+链表
- TreeMap:红黑树
- LinkedHashMap:HashMap+双向链表(继承HashMap)
二、顺序性
- HashMap:无序,不保证插入顺序
- Hashtable:无序
- TreeMap:按键排序(自然排序,比较器排序)
- LinkedHashMap:保持插入顺序
三、是否允许null
- HashMap:key允许1个null,value允许多个null
- Hashtable:key和value都不允许null,否则NPE
- TreeMap:key不允许null,value可以null
- LinkedHashMap:同HashMap,key最多一个null
四、线程安全
- HashMap:非线程安全
- Hashtable:线程安全(方法上加synchronized)
- TreeMap:非线程安全
- LinkedHashMap:非线程安全
五、性能
- HashMap:高,无锁开销
- Hashtable:低,锁粒度大,并发差
- TreeMap:较低,O(logn),需排序
- LinkedHashMap:略低于HashMap,需维护链表
六、key要求
- HashMap、Hashtable、LinkedHashMap:key重写hashCode()+equals()
- TreeMap:key必须可比较(实现Comparable或传入Comparator)
七、扩容机制
- HashMap:默认16,负载因子0.75,扩容2倍
- Hashtable:默认11,负载因子0.75,扩容2倍+1
- TreeMap:无数组,无需扩容
- LinkedHashMap:同HashMap
八、适用场景
- HashMap:通用,不需要顺序,追求性能
- Hashtable:基本废弃,高并发用ConcurrentHashMap
- TreeMap:需要按键排序、范围查找
- LinkedHashMap:需要去重+保持插入顺序,或实现LRU
对比表
| 特性 | HashMap | Hashtable | TreeMap | LinkedHashMap |
|---|---|---|---|---|
| 结构 | 数组+链表+红黑树 | 数组+链表 | 红黑树 | HashMap+双向链表 |
| 顺序 | 无序 | 无序 | 按键排序 | 插入有序 |
| null允许 | key1个,value多个 | 都不允许 | key不允许 | 同HashMap |
| 线程安全 | 否 | 是(synchronized) | 否 | 否 |
| 性能 | 高 | 低 | 中(O(logn)) | 略低于HashMap |
| 适用场景 | 绝大多数场景 | 已废弃 | 排序、范围查找 | 保序、LRU缓存 |
总结
- 日常开发优先用HashMap
- 要插入有序用LinkedHashMap
- 要自动排序用TreeMap
- Hashtable直接抛弃,高并发用ConcurrentHashMap
HashMap线程不安全表现?
1.数据覆盖(最常见)
- 两个线程同时使用相同下标插入数据。
- 线程A判断位置为空,准备插入;
- 此时线程B也判断为空,也插入;
- 结果:一个数据被另一个覆盖,数据丢失。
2.扩容导致死循环(JDK1.7经典问题)
- JDK1.7使用头插法,多线程扩容会让链表形成环形链。
- 一旦触发环形链表,get()时会无限循环遍历,导致CPU100%,服务卡死。
- JDK1.8改用尾插法修复了死循环,但依旧线程不安全。
3.扩容导致数据丢失
- 多线程同时扩容,会互相覆盖重新哈希后的节点。
- 结果:部分数据直接丢失,无法找回。
4.size统计不准确
- size没有原子性保障。
- 多个线程同时put,size++会出现计数少算的情况。
总结
HashMap线程不安全主要表现为:多线程put出现数据覆盖、数据丢失;JDK1.7扩容会产生环形链表导致死循环;size计数不准确。
解决方案
绝对不要在多线程中使用HashMap!
- 高并发:用ConcurrentHashMap(推荐)
- 低并发:用Collections.synchronizedMap
TreeMap如何排序?
底层原理
TreeMap是按键排序的Map,底层基于红黑树实现,排序规则由Key决定,有两种排序方式:
排序方式
1.自然排序(默认)
要求
Key对应的类必须实现Comparable接口,重写compareTo()方法。
常用类型默认规则
- Integer、Long:升序
- String:按字母字典序排序
- Date:按时间先后
示例
TreeMap<Integer, String> map = new TreeMap<>();
map.put(3, "C");
map.put(1, "A");
map.put(2, "B");
// 输出:1,2,3 升序
2.定制排序(自定义规则)
创建TreeMap时,传入Comparator比较器,Key不需要实现Comparable。
TreeMap<Integer, String> map = new TreeMap<>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
// 降序排列
return o2 - o1;
}
});
排序与去重规则
- TreeMap不使用equals和hashCode
- 完全依靠compare/compareTo判断是否重复
- 返回0→视为同一个Key,覆盖旧值
特点
- 按键有序,不是插入顺序
- Key不能为null(比较会抛NPE)
- Value可以为null
- 增删改查时间复杂度O(logn)
HashSet与HashMap的区别?
核心关系
- HashSet底层就是HashMap,是对HashMap的简单包装。
- HashSet只用到HashMap的key,value统一存一个固定空对象PRESENT。
详细区别
1.实现接口不同
- HashSet:实现Set接口,单列集合
- HashMap:实现Map接口,双列集合
2.存储内容不同
- HashSet:只存单个对象
- HashMap:存key-value键值对
3.存入数据的方法
- HashSet:add(Object o)底层调用map.put(o,PRESENT)
- HashMap:put(key,value)
4.重复判断
- HashSet:通过元素的hashCode()+equals()保证不重复
- HashMap:通过key的hashCode()+equals()保证key不重复
5.效率
- 两者基本一致,HashSet略轻量一点,但几乎无差别。
6.是否允许null
- HashSet:允许一个null
- HashMap:key允许一个null,value允许多个null
7.用途
- HashSet:用于去重
- HashMap:用于快速查找(通过key找value)
总结
- HashMap存键值对,用于映射关系查询;
- HashSet存单个对象,用于去重;
- HashSet底层就是HashMap,只是把元素当作key,value用固定空对象占位。
Queue
Queue接口有什么特点?
1.Queue是Collection的直接子接口,与List、Set平级。
2.遵循先进先出FIFO:先入队的元素优先出队。
3.只操作头尾:元素从队尾添加,从队头取出,不支持随机访问。
4.方法分两类:
- 失败抛异常:add()、remove()、element()
- 失败返回特殊值:offer()、poll()、peek()
5.主要用于任务排队、消息队列、生产者-消费者模型。
6.子接口Deque是双端队列,可做队列也可做栈。
Queue和Deque有什么区别?
1.接口定义不同
- Queue:单端队列接口,只能尾进头出(FIFO)
- Deque:双端队列接口(Deque=DoubleEndedQueue),两端都可以进出
2.操作方式不同
- Queue:只支持
- 队尾添加
- 队头删除/查看
- Deque:支持
- 队头/队尾都能添加
- 队头/队尾都能删除/查看→既可以当队列,也可以当栈使用
3.方法体系不同
- Queue方法:
- offer()/add()
- poll()/remove()
- peek()/element()
- Deque额外支持:
- offerFirst()/offerLast()
- pollFirst()/pollLast()
- peekFirst()/peekLast()
- 栈方法:push()、pop()、peek()
4.用途区别
- Queue:标准先进先出队列
- Deque:
- 可做队列(FIFO)
- 可做栈(LIFO,官方推荐替代Stack)
- 可做滑动窗口、工作窃取等
5.继承关系
- Deque继承自Queue
- 所以Deque拥有Queue的所有功能,且功能更多
总结
- Queue是单端队列,只能一头进一头出
- Deque是双端队列,两端都能进出,功能更强
- 日常开发优先用ArrayDeque实现Deque,性能最好。
Queue中add()、offer()、remove()、poll()、element()、peek()的区别?
操作逻辑完全一样,区别只在于:队列满/空时,一个抛异常,一个返回特殊值(false/null)。
| 操作 | 抛异常的方法 | 返回特殊值的方法 | 说明 |
|---|---|---|---|
| 添加元素 |
add(e) |
offer(e) |
队列满时:add抛异常,offer返回false |
| 删除队首 |
remove() |
poll() |
队列为空时:remove抛异常,poll返回null |
| 查看队首 |
element() |
peek() |
队列为空时:element抛异常,peek返回null |
- add/remove/element:暴力版,出错直接抛异常
- add/remove/element:暴力版,出错直接抛异常
详细说明
1.添加元素
- add(e):队列满了抛出IllegalStateException
- offer(e):队列满了返回false,不抛异常
2.删除并返回队首
- remove():队空抛出NoSuchElementException
- poll():队空返回null
3.获取但不删除队首
- element():队空抛出NoSuchElementException
- peek():队空返回null
使用场景
- 业务允许队列空/满,需要安全处理→用offer/poll/peek
- 队列不应该空/满,出错就是bug,需要快速失败→用add/remove/element
常见Queue实现类有哪些?
一、普通队列(非线程安全)
1.LinkedList
- 实现了Queue和Deque接口
- 底层双向链表
- 允许存null
2.ArrayDeque
- 实现Deque,数组结构
- 效率比LinkedList更高
- 不允许null
3.PriorityQueue
- 优先级队列,底层堆(小顶堆)
- 不按FIFO,按优先级出队
- 不允许null,线程不安全
二、线程安全队列
1.ConcurrentLinkedQueue
- 无界、无锁、线程安全
- 基于CAS实现,高并发性能好
2.ArrayBlockingQueue
- 有界阻塞队列,数组实现
- 满了阻塞put(),空了阻塞take()
3.LinkedBlockingQueue
- 可选有界/无界阻塞队列
- 链表实现,吞吐量较高
4.PriorityBlockingQueue
- 支持优先级的阻塞队列
5.SynchronousQueue
- 不存储元素,直接移交
- 一个插入必须等待一个删除
PriorityQueue实现原理?
1.底层结构
- 底层是动态数组,实现了小顶堆(默认)结构
- 堆是一棵完全二叉树,满足:父节点≤子节点(最小堆)
2.排序规则
- 元素必须可比较,两种方式:
- 元素实现Comparable接口(自然排序)
- 构造时传入Comparator比较器(定制排序)
- 不满足则抛出ClassCastException
3.核心操作原理
- 入队offer()
- 元素添加到数组末尾
- 向上调整(siftUp):与父节点比较,交换位置,维持堆结构
- 出队poll()
- 取出堆顶(下标0)元素
- 末尾元素移到堆顶
- 向下调整(siftDown):与子节点比较,交换位置,维持堆结构
4.特点
- 并非严格FIFO,而是按优先级出队
- 线程不安全
- 不允许null
- 增删复杂度O(logn)
- 默认容量11,满了自动扩容
5.与普通Queue区别
- Queue:先进先出
- PriorityQueue:按优先级出队,与插入顺序无关
为什么不推荐使用Stack?推荐用什么?
1.继承设计不合理
- Stack继承自Vector,而Vector是线程安全的动态数组。
- 栈只需要后进先出(LIFO),却继承了Vector所有public方法,可以通过add(index,e)、remove(index)随意插入、删除中间元素,破坏了栈的封装和行为规范。
2.性能差
-
Vector所有方法都加了synchronized,单线程环境下完全没必要,开销大、效率低。
3.API老旧
-
属于Java早期遗留类,官方早已不推荐,没有现代化优化。
推荐使用Deque接口及其实现类来实现栈。
- 首选:ArrayDeque(数组实现,效率最高)
- 备选:LinkedList(链表实现,也可)
用法对应
- 入栈:push(e)
- 出栈:pop()
- 查看栈顶:peek()
完全满足栈的LIFO行为,且没有多余方法、性能更高、线程不安全但可控。
ArrayDeque与LinkedList区别?
1.底层结构不同
- ArrayDeque:动态循环数组
- LinkedList:双向链表
2.访问与操作效率
- ArrayDeque
- 内存连续,缓存友好,读写效率更高
- 头插、尾插都是O(1),性能稳定
- LinkedList
- 内存分散,节点开销大
- 插入删除O(1),但需要先遍历找到节点,实际更慢
3.是否支持null
- ArrayDeque:不允许存入null
- LinkedList:允许null
4.内存占用
- ArrayDeque:紧凑,内存开销小
- LinkedList:每个元素都要存前后指针,开销大
5.功能定位
- 两者都实现了Queue、Deque,都能当队列、栈用
- LinkedList额外实现了List,支持随机访问(get(index)),但效率很低
6.使用选择
- 单纯做队列、栈→优先用ArrayDeque(更快、更省内存)
- 需要频繁在中间插入删除,或必须存null→用LinkedList
总结
ArrayDeque是数组,性能高、不允许null;LinkedList是链表,支持null、功能多但效率低。队列/栈首选ArrayDeque。
Queue和List的区别?
1.接口定位不同
- Queue:队列接口,遵循FIFO(先进先出)
- List:线性表接口,强调有序可重复、可随机访问
2.操作方式不同
- Queue:只在头尾操作,不支持按索引增删改查
- List:支持任意位置增删改查,有get(index)、add(index,e)等
3.用途场景不同
- Queue:用于任务排队、消息队列、生产者-消费者
- List:用于普通数据存储、遍历、随机查找
4.方法体系不同
- Queue:offer()、poll()、peek()等队列专用方法
- List:get()、set()、indexOf()等索引相关方法
5.部分实现类重叠
-
LinkedList同时实现了List和Queue,所以它既可以当列表也可以当队列。
Queue是队列,头尾操作、FIFO;List是线性表,支持索引、随机访问。
并发集合
为什么要使用并发集合?
使用并发集合,核心是为了在多线程环境下保证线程安全,同时兼顾性能与易用性,替代低效的同步容器。
1.解决线程安全问题
普通集合(如ArrayList、HashMap、HashSet)非线程安全,多线程同时读写会出现:
- 数据丢失、覆盖
- 并发修改异常ConcurrentModificationException
- 数据不一致、业务错乱并发集合内部实现了安全的并发控制,可直接在多线程下使用。
2.比synchronized同步容器性能高很多
早期同步容器(Vector、Hashtable、Collections.synchronizedList):
- 粗粒度锁,整个方法加锁
- 并发高时锁竞争严重,吞吐量低
并发集合(ConcurrentHashMap、CopyOnWriteArrayList、ConcurrentLinkedQueue):
- 采用分段锁、CAS、无锁设计、读写分离
- 并发能力强、锁粒度更细、高并发下性能提升明显
3.提供更适合并发场景的API
- 原子操作:putIfAbsent、removeIf等复合原子操作
- 弱一致性迭代器,遍历过程不会抛并发修改异常
- 支持阻塞、非阻塞队列,方便做生产者消费者模型
4.降低开发成本
不用自己手动加锁、释放锁、处理死锁,减少代码复杂度与出错概率。
ConcurrentHashMap的实现机制?
1.JDK7:分段锁(Segment)机制
核心结构:Segment[]+HashEntry[]+链表。Segment继承ReentrantLock,默认16个Segment,每个Segment是独立哈希表。
并发控制:
- 写操作:根据key哈希定位到对应Segment,只锁当前Segment,其他Segment可并行访问。
- 读操作:无锁,依赖volatile修饰的HashEntry保证内存可见性。
扩容:每个Segment独立扩容,扩容时仅锁当前Segment。
优缺点:并发度高(默认16);但Segment数量固定,跨段操作(如size())需锁全表,性能受限。
2.JDK8+:CAS+桶级synchronized机制(主流)
核心结构:Node[]table(volatile)+链表+红黑树(链表≥8且数组≥64树化,≤6反树化)。Node的val、next用volatile修饰,保证可见性。
并发控制(核心):
- 无锁读:get()全程无锁,通过volatile保证读取最新值。
- 写操作(put/remove):
- 定位桶下标,用CAS尝试无锁插入空桶;
- 桶非空时,只锁当前链表/红黑树的头节点(桶级锁),锁粒度极细;
- 冲突时自旋重试,失败再用synchronized加锁。
- 扩容优化:多线程协同扩容(化整为零),线程发现扩容时会协助迁移数据,用ForwardingNode标记迁移完成的桶。
size计算:无全局锁,通过CAS累加计数,弱一致性但性能极高。
ConcurrentHashMap与HashMap和Hashtable的差异?
| 特性 | ConcurrentHashMap(JDK8+) | HashMap | Hashtable |
|---|---|---|---|
| 线程安全 |
是(CAS+桶级synchronized) |
否(多线程易数据覆盖/丢失/死循环) |
是(方法全加synchronized,全局锁) |
| 锁粒度 | 桶级(链表/红黑树头节点) | 无锁 | 全表锁(整个对象) |
| 底层结构 | 数组+链表+红黑树 | 数组+链表+红黑树(JDK8+) | 数组+链表 |
| null支持 | 键/值均不允许null(抛NPE) | 键允许1个null,值允许多个 | 键/值均不允许null(抛 NPE) |
| 性能 | 高并发最优(无锁读+细粒度写) | 单线程最高(无锁开销) | 极低(全局锁,并发阻塞) |
| 扩容机制 | 多线程协同迁移,效率高 | 单线程重哈希 | 单线程重哈希 |
| 迭代器 |
弱一致性(不抛ConcurrentModificationException) |
快速失败(抛异常) | 快速失败(抛异常) |
| 默认容量 | 16 | 16 | 11 |
| 负载因子 | 0.75 | 0.75 | 0.75 |
| 适用场景 | 高并发多线程环境 | 单线程通用场景 | 基本废弃(低并发可用,不推荐) |
关键区别
1.线程安全机制(核心差异)
- Hashtable:全表synchronized,同一时间仅1个线程操作,并发能力极差。
- HashMap:无任何锁,多线程并发put/扩容会出现数据覆盖、丢失、JDK7死循环。
- ConcurrentHashMap:
- JDK7:分段锁,不同Segment并行;
- JDK8+:无锁读+CAS无锁写+桶级锁,锁冲突概率极低,并发性能接近HashMap。
2.null限制(设计原因)
- ConcurrentHashMap/Hashtable:不允许null键/值,避免get(null)时无法区分“键不存在”还是“值为null”,保证并发场景下的语义清晰。并发场景下两次操作不是原子的,中间可能被其他线程修改,导致判断结果不可靠。
- HashMap:单线程无歧义,允许null提升灵活性。
3.扩容效率
- HashMap/Hashtable:单线程全量重哈希,扩容时阻塞所有操作。
- ConcurrentHashMap:多线程协助迁移,扩容期间新老数组共存,读写不阻塞,大幅提升扩容性能。
总结
- Hashtable:全局锁,性能差,已废弃;
- HashMap:单线程高效,多线程不安全;
- ConcurrentHashMap:JDK8+用CAS+桶级锁,无锁读、细粒度写,高并发下性能最优,是多线程场景的首选。
ConcurrentHashMap是弱一致性的体现?
ConcurrentHashMap是典型的弱一致性体现,主要体现在迭代器和size()等方法上,目的是在高并发下提升性能。
弱一致性体现在哪里?
1.迭代器是弱一致性的
- 迭代器遍历过程中,其他线程仍可以修改Map(put/remove)
- 不会抛出ConcurrentModificationException
- 但迭代器不保证能读到最新数据,只能保证读到创建迭代器时已存在的数据,或部分新数据
2.size()、isEmpty()等方法弱一致
- 高并发下这些方法不做全局加锁,只做统计估算
- 返回的是近似值,调用瞬间可能已被其他线程修改
- 不保证绝对精确
3.get读操作无锁
- 读不加锁,能读到最近一次完成的写入,但不保证立即可见最新值
- 属于弱一致,而非强一致
为什么设计成弱一致性?
- 如果要强一致性,就必须全局加锁
- 会导致并发性能急剧下降,失去高并发优势
- 弱一致性是性能与一致性的折中,满足绝大多数并发业务场景
哪些操作是强一致?
- put、get、remove、replace等单个原子操作是线程安全、强一致的
- 但复合操作(如get+put)不保证原子性,仍需加锁或使用原子方法
什么是CopyOnWriteArrayList?适用场景?
是什么
- 线程安全的List实现,位于java.util.concurrent包下
- 底层采用写时复制(Copy-On-Write)机制
- 读操作无锁,写操作通过复制新数组实现
核心原理
- 内部持有一个volatile数组,只保证可见性,不加锁
- 读:直接读取原数组,不加锁,性能极高
- 写(add/set/remove等)
- 加ReentrantLock锁,保证同一时刻只有一个线程写
- 复制出一个新数组,在新数组上做修改
- 完成后将引用指向新数组
- 旧数组等待GC回收
-
读写分别操作不同数组,不存在竞争,所以读不用加锁
特点
- 读极快,写有开销
- 迭代器遍历的是快照,不支持增删改,不会抛出ConcurrentModificationException
- 弱一致性:写操作完成前,其他线程读到的仍是旧数据
- 内存占用较高:写操作会临时存在新旧两个数组
优点
- 高并发读场景性能远超Vector、Collections.synchronizedList
- 读操作无锁,无锁竞争
- 遍历安全,适合边遍历边修改的并发场景
优点
- 写操作性能差,有数组复制开销
- 内存占用大
- 数据存在短暂不一致,不适合强一致性要求场景
适用场景
- 读多写少的高并发场景(白名单、配置列表、商品分类、缓存列表等)
- 遍历操作远多于增删改操作
- 允许读取到稍旧数据、对数据实时性要求不高
- 需避免并发修改异常的遍历场景
什么是阻塞队列BlockingQueue?
BlockingQueue是java.util.concurrent包下的线程安全队列,在普通队列基础上增加了阻塞等待机制,主要用于生产者-消费者模型。
核心阻塞特点
- 队列满时:调用put()放入元素的线程会阻塞等待,直到队列有空闲位置
- 队列空时:调用take()取出元素的线程会阻塞等待,直到队列有元素
常用方法对比
| 操作 | 抛异常 | 返回特殊值 | 阻塞等待 | 超时等待 |
|---|---|---|---|---|
| 入队 | add() | offer() | put() | offer(e,timeout,unit) |
| 出队 | remove() | poll() | take() | poll(timeout,unit) |
常见实现类
- ArrayBlockingQueue:数组实现,有界,公平/非公平
- LinkedBlockingQueue:链表实现,可选有界/无界
- PriorityBlockingQueue:优先级阻塞队列
- SynchronousQueue:不存储元素,直接移交
- DelayQueue:延迟队列
应用场景
- 生产者-消费者模式
- 线程池任务队列
- 流量削峰、异步解耦
BlockingQueue=线程安全队列+满了put阻塞、空了take阻塞,专门解决多线程生产消费同步问题。
ConcurrentLinkedQueue原理?和BlockingQueue的对比?
1.结构
- 无界、线程安全的单向链表队列
- 实现Queue接口,不是BlockingQueue
2.并发机制
- 全程无锁,基于CAS+volatile实现线程安全
- 入队、出队都使用CAS自旋重试,不阻塞线程
3.特点
- 高并发吞吐量极高,适合大量读写
- 无容量限制,会一直扩容
- 不支持阻塞(没有put()/take())
- 不允许null
| 对比项 | ConcurrentLinkedQueue | BlockingQueue(如ArrayBlockingQueue) |
|---|---|---|
| 实现方式 | CAS+自旋,无锁 | ReentrantLock+条件队列,有锁 |
| 是否阻塞 | 不阻塞 | 满则put阻塞,空则take阻塞 |
| 容量 | 无界 | 大多有界(可控制内存) |
| 适用场景 | 高并发、不希望阻塞 | 生产者-消费者、流量控制、线程池 |
| API | offer/poll/peek | 支持阻塞put/take+超时 |
| 内存风险 | 无界可能OOM | 有界更安全,可限流 |
| 性能 | 极高(无锁) | 较低(锁竞争) |
总结
- ConcurrentLinkedQueue:无锁、无界、不阻塞,高并发性能最强,适合纯异步消费。
- BlockingQueue:有锁、可阻塞、可限流,是生产者-消费者模型的标准方案。
ConcurrentSkipListMap/ConcurrentSkipListSet特点?
核心特点
1.有序基于跳表(SkipList)实现,元素默认按key升序排列,可指定比较器自定义排序。
2.并发安全采用CAS+细粒度锁实现并发控制,无全局锁,高并发下性能优于Collections.synchronizedSortedMap。
3.高并发下比TreeMap更合适
- TreeMap非线程安全
- 加锁的TreeMap并发性能差
- ConcurrentSkipListMap适合高并发+有序场景
4.key不允许为null同ConcurrentHashMap,避免并发场景下的二义性。
5.弱一致性迭代器遍历是快照级别的弱一致性,不会抛ConcurrentModificationException。
6.支持范围查询自带headMap、tailMap、subMap等范围查找方法,是高并发场景下有序+范围查询的首选。
适用场景
- 需要高并发+有序的场景
- 需按key范围查询、排序、分页
- 秒杀、限流、定时任务、有序事件队列等
总结
ConcurrentSkipListMap/Set是高并发下的有序集合,基于跳表实现,线程安全、支持范围查询,并发性能远优于加锁的TreeMap。
多线程遍历集合为什么会抛ConcurrentModificationException?
多线程遍历集合时抛出ConcurrentModificationException,根本原因是:迭代器采用了快速失败(fail-fast)机制,检测到集合结构被并发修改,为避免数据错乱而主动抛异常。
1.底层原理
- 集合内部维护一个modCount(修改次数)
- 迭代器创建时会保存一份expectedModCount=modCount
- 每次迭代(next)都会对比两者是否相等
- 若其他线程增删元素导致modCount改变
- 迭代器检测到不一致,立即抛出异常
2.为什么单线程也可能抛?
单线程一边遍历一边remove/add也会抛,因为同样触发了modCount与expectedModCount不一致。
3.本质目的
不是为了判定线程安全,而是尽早暴露并发修改问题,避免遍历过程中出现数据丢失、重复、越界等不可预期错误。
4.如何避免?
- 使用并发集合(ConcurrentHashMap、CopyOnWriteArrayList),它们是弱一致性,不抛此异常
- 使用迭代器自带的remove()而非集合的remove()
- 遍历前加锁,保证串行执行
总结
因为普通集合迭代器是快速失败机制,遍历过程中一旦检测到集合被其他线程结构性修改(modCount不匹配),就立即抛出ConcurrentModificationException,防止数据异常。
更多推荐



所有评论(0)