1. Java集合的分类
Java集合框架主要分为以下几类:
(1)Map(映射)
- 特点:存储键值对(Key-Value),Key不可重复,Value可以重复。
- 常见实现类:
HashMap
:基于哈希表实现,线程不安全。Hashtable
:基于哈希表实现,线程安全。ConcurrentHashMap
:基于分段锁实现,线程安全且性能优于Hashtable
。TreeMap
:基于红黑树实现,Key有序。
(2)List(列表)
- 特点:存储有序且可重复的元素。
- 常见实现类:
ArrayList
:基于动态数组实现,查询快,增删慢。LinkedList
:基于双向链表实现,增删快,查询慢。Vector
:基于动态数组实现,线程安全。
(3)Set(集合)
- 特点:存储无序且不可重复的元素。
- 常见实现类:
HashSet
:基于哈希表实现,元素无序。LinkedHashSet
:基于哈希表和链表实现,元素按插入顺序排序。TreeSet
:基于红黑树实现,元素有序。
(4)Queue(队列)
- 特点:先进先出(FIFO)的数据结构。
- 常见实现类:
LinkedList
:可以用作队列。PriorityQueue
:基于堆实现,元素按优先级排序。ArrayDeque
:基于数组实现的双端队列。
2. 线程安全与线程不安全的集合
- 线程安全的集合:
Vector
:线程安全的List
。Hashtable
:线程安全的Map
。ConcurrentHashMap
:线程安全且高效的Map
。CopyOnWriteArrayList
:线程安全的List
。CopyOnWriteArraySet
:线程安全的Set
。
- 线程不安全的集合:
ArrayList
、LinkedList
、HashSet
、HashMap
等。
3. ArrayList与LinkedList的异同
相同点
- 都实现了
List
接口,存储有序且可重复的元素。
不同点
特性 | ArrayList | LinkedList |
---|---|---|
底层实现 | 动态数组 | 双向链表 |
查询性能 | 快(通过索引直接访问) | 慢(需要从头遍历) |
增删性能 | 慢(需要移动元素) | 快(只需修改指针) |
空间占用 | 预留空间,可能浪费 | 每个节点有前后指针,占用更多空间 |
随机访问 | 支持(实现RandomAccess 接口) | 不支持 |
适用场景 | 频繁查询,少量增删 | 频繁增删,少量查询 |
4. ArrayList与Vector的区别
相同点
- 底层都是动态数组,存储有序且可重复的元素。
不同点
特性 | ArrayList | Vector |
---|---|---|
线程安全 | 线程不安全 | 线程安全 |
扩容机制 | 扩容为原来的1.5倍 | 扩容为原来的2倍 |
性能 | 性能较高 | 性能较低(同步开销) |
空间占用 | 相对节省空间 | 相对浪费空间 |
5. ArrayList的扩容机制
- 初始容量:默认初始容量为10。
- 扩容规则:当元素数量超过当前容量时,触发扩容。新容量为旧容量的1.5倍。
- 扩容过程:
- 创建一个新数组,容量为旧数组的1.5倍。
- 将旧数组中的元素复制到新数组中。
- 将新数组赋值给
ArrayList
的内部数组引用。
6. Array与ArrayList的区别
相同点
- 都可以存储多个元素。
不同点
特性 | Array | ArrayList |
---|---|---|
数据类型 | 支持基本数据类型和引用数据类型 | 只支持引用数据类型(需使用包装类) |
容量 | 固定长度 | 动态扩容 |
功能 | 功能简单 | 提供丰富的操作方法(如增删改查) |
性能 | 性能较高 | 性能较低(动态扩容和封装开销) |
7. 扩展:集合的最佳实践
- 选择合适的集合:根据需求选择
List
、Set
、Map
或Queue
。 - 线程安全:在多线程环境下使用线程安全的集合,如
ConcurrentHashMap
、CopyOnWriteArrayList
。 - 性能优化:避免频繁扩容,初始化时指定合适的容量。
- 遍历集合:使用迭代器或增强for循环遍历集合,避免直接操作底层数据结构。
8. 示例代码
ArrayList与LinkedList
List<Integer> arrayList = new ArrayList<>();
arrayList.add(1);
arrayList.add(2);
System.out.println(arrayList.get(0)); // 输出 1
List<Integer> linkedList = new LinkedList<>();
linkedList.add(1);
linkedList.add(2);
System.out.println(linkedList.get(0)); // 输出 1
ArrayList扩容
List<Integer> list = new ArrayList<>(5); // 初始容量为5
for (int i = 0; i < 10; i++) {
list.add(i);
}
System.out.println(list.size()); // 输出 10
Array与ArrayList
int[] array = new int[10]; // 支持基本数据类型
array[0] = 1;
List<Integer> arrayList = new ArrayList<>(); // 只支持引用数据类型
arrayList.add(1);
9. HashMap的底层数据结构
- JDK 1.7及之前:数组 + 链表。
- JDK 1.8及之后:数组 + 链表 + 红黑树。
- 当链表长度超过8且数组长度大于64时,链表会转换为红黑树。
- 当红黑树节点数小于6时,红黑树会退化为链表。
为什么选择红黑树?
- 红黑树的查询时间复杂度为O(log n),而链表的查询时间复杂度为O(n)。
- 红黑树的插入、删除和查找操作相对平衡,适合处理大量数据。
为什么一开始不直接用红黑树?
- 红黑树的维护成本较高(如左旋、右旋、变色等操作),在数据量较小时,链表的性能已经足够。
10. 解决哈希冲突的方法
- 链地址法:将哈希冲突的元素存储在链表中(如HashMap)。
- 开放寻址法:当发生冲突时,寻找下一个空闲位置存储(如
ThreadLocalMap
)。 - 再哈希法:使用多个哈希函数,直到找到空闲位置。
- 建立公共溢出区:将冲突的元素存储在一个单独的溢出区。
11. HashMap的加载因子
- 默认加载因子:0.75。
- 为什么是0.75?
- 加载因子是空间和时间的平衡值。
- 加载因子过大(如1.0):减少扩容次数,但哈希冲突概率增加,查询性能下降。
- 加载因子过小(如0.5):减少哈希冲突,但浪费空间,扩容频繁。
12. HashMap的存储索引计算
- 步骤:
- 计算
key
的hashCode
。 - 对
hashCode
进行扰动计算((h = key.hashCode()) ^ (h >>> 16)
),以减少哈希冲突。 - 将扰动后的值与数组长度取模(
index = (n - 1) & hash
),得到存储索引。
- 计算
13. HashMap的put流程
- 计算
key
的哈希值,确定存储索引。 - 如果该位置为空,直接插入。
- 如果该位置不为空:
- 判断
key
是否相等(equals
方法):- 相等:更新
value
。 - 不相等:遍历链表或红黑树,插入新节点。
- 相等:更新
- 判断
- 判断是否需要扩容(元素数量超过阈值)。
- 判断是否需要将链表转换为红黑树(链表长度超过8且数组长度大于64)。
14. HashMap的key选择
- 常用key类型:
String
、Integer
等。 - 原因:
- 这些类重写了
hashCode
和equals
方法,保证了哈希值的唯一性和一致性。 - 这些类是不可变的(Immutable),避免了
key
被修改导致哈希值变化。
- 这些类重写了
15. HashMap的线程安全问题
- JDK 1.7及之前:
- 使用头插法,多线程环境下可能导致循环链表,造成死循环。
- JDK 1.8及之后:
- 改为尾插法,解决了循环链表问题。
- 但仍然存在数据覆盖问题(多个线程同时插入相同
key
时,可能导致数据丢失)。
解决方案:
- 使用
Collections.synchronizedMap(new HashMap<>())
。 - 使用
ConcurrentHashMap
(推荐)。
16. 扩展:ConcurrentHashMap
- 特点:线程安全且高效,采用分段锁(JDK 1.7)或CAS + synchronized(JDK 1.8)。
- 适用场景:高并发环境下的
Map
操作。
17. 示例代码
HashMap的put操作
Map<String, Integer> map = new HashMap<>();
map.put("a", 1);
map.put("b", 2);
System.out.println(map.get("a")); // 输出 1
ConcurrentHashMap的使用
Map<String, Integer> concurrentMap = new ConcurrentHashMap<>();
concurrentMap.put("a", 1);
concurrentMap.put("b", 2);
System.out.println(concurrentMap.get("a")); // 输出 1
18. HashMap总结
- 数据结构:JDK 1.8之后,
HashMap
采用数组 + 链表 + 红黑树的结构。 - 哈希冲突:常用链地址法解决。
- 加载因子:默认0.75,平衡空间和时间。
- 线程安全:
HashMap
非线程安全,ConcurrentHashMap
是线程安全的替代方案。 - 最佳实践:选择合适的
key
类型,避免线程安全问题,合理设置初始容量和加载因子。
ConcurrentHashMap
是 Java 并发编程中非常重要的一个类,常用于高并发场景下的键值对存储。以下是关于 ConcurrentHashMap
的常见面试题及其详细解答,帮助你更好地理解和掌握这一知识点。
19. ConcurrentHashMap 和 HashMap 的区别是什么?
- 线程安全性:
HashMap
是非线程安全的,多线程环境下可能导致数据不一致或死循环。ConcurrentHashMap
是线程安全的,支持高并发操作。
- 实现方式:
HashMap
使用数组 + 链表 + 红黑树(JDK 1.8)。ConcurrentHashMap
在 JDK 1.7 中使用分段锁(Segment
),在 JDK 1.8 中使用CAS
+synchronized
。
- 性能:
HashMap
在单线程下性能更高。ConcurrentHashMap
在多线程下性能更好,避免了全局锁的竞争。
20. ConcurrentHashMap 在 JDK 1.7 和 JDK 1.8 中的实现有什么不同?
- JDK 1.7:
- 使用分段锁(
Segment
),将数据分成多个段,每个段独立加锁。 - 优点:减少了锁的竞争,提高了并发性能。
- 缺点:实现复杂,内存占用较高。
- 使用分段锁(
- JDK 1.8:
- 取消了分段锁,采用数组 + 链表 + 红黑树的结构。
- 使用
CAS
和synchronized
实现线程安全。 - 优点:简化了实现,提高了并发性能。
21. ConcurrentHashMap 的 get 操作为什么不需要加锁?
ConcurrentHashMap
的get
操作是无锁的,因为:Node
的value
和next
字段都是volatile
修饰的,保证了可见性。volatile
确保线程能够读取到最新的值,无需加锁。
22. ConcurrentHashMap 的 put 操作是如何保证线程安全的?
- JDK 1.8:
- 使用
CAS
和synchronized
实现线程安全。 - 插入新节点时,使用
CAS
竞争锁。 - 更新节点时,使用
synchronized
锁定链表或红黑树的头节点。
- 使用
- 扩容机制:
- 当元素数量超过阈值时,触发扩容。
- 扩容时,使用
CAS
和synchronized
确保线程安全。
23. ConcurrentHashMap 的扩容机制是什么?
- 触发条件:
- 当元素数量超过阈值(
sizeCtl
)时,触发扩容。
- 当元素数量超过阈值(
- 扩容过程:
- 创建一个新数组,容量为旧数组的 2 倍。
- 将旧数组中的元素复制到新数组中。
- 使用
CAS
和synchronized
确保线程安全。
24. ConcurrentHashMap 如何解决哈希冲突?
- 链地址法:
- 将哈希冲突的元素存储在链表中。
- 当链表长度超过 8 且数组长度大于 64 时,链表转换为红黑树。
- 红黑树:
- 红黑树的查询时间复杂度为 O(log n),适合处理大量数据。
25. ConcurrentHashMap 的 size 方法是如何实现的?
- JDK 1.7:
- 遍历所有段,累加每个段的元素数量。
- 由于分段锁的存在,
size
方法可能不准确。
- JDK 1.8:
- 使用
CounterCell
统计元素数量。 - 通过
CAS
更新CounterCell
,确保线程安全。
- 使用
26. ConcurrentHashMap 的 key 和 value 是否可以为 null?
- key 和 value 都不能为 null:
ConcurrentHashMap
的设计目标是高并发场景,null
值会导致歧义。- 如果
key
为null
,无法确定是key
不存在还是key
为null
。 - 如果
value
为null
,无法确定是value
不存在还是value
为null
。
在 HashMap
中,key 和 value 都可以为 null
。这是 HashMap
与 ConcurrentHashMap
的一个重要区别。下面我会详细解释这一点,并分析其背后的原因。
- 示例:
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>(); map.put(null, 1); // 抛出 NullPointerException map.put("a", null); // 抛出 NullPointerException
26.1 HashMap 的 key 和 value 可以为 null
key 为 null
HashMap
允许key
为null
。- 当
key
为null
时,HashMap
会将其哈希值计算为0
,并将其存储在数组的第一个位置(索引为0
)。 - 示例:
HashMap<String, Integer> map = new HashMap<>(); map.put(null, 1); System.out.println(map.get(null)); // 输出 1
value 为 null
HashMap
也允许value
为null
。- 示例:
HashMap<String, Integer> map = new HashMap<>(); map.put("a", null); System.out.println(map.get("a")); // 输出 null
26.2. 为什么 HashMap 允许 key 和 value 为 null?
- 设计灵活性:
HashMap
的设计目标是提供一种通用的键值对存储结构,允许null
值可以满足更多场景的需求。
- 历史原因:
HashMap
是 Java 早期引入的类,为了兼容性和易用性,允许null
值。
- 与 Hashtable 的区别:
Hashtable
不允许key
或value
为null
,而HashMap
允许,这是两者之间的一个重要区别。
26.3. HashMap 中 key 为 null 的实现细节
- 当
key
为null
时,HashMap
会将其哈希值计算为0
,并将其存储在数组的第一个位置(索引为0
)。 - 源码解析:
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
26.4. 使用 HashMap 时需要注意的问题
- key 为 null:
- 如果
key
为null
,只能有一个null
键,因为HashMap
的键是唯一的。
- 如果
- value 为 null:
- 如果
value
为null
,可以通过containsKey
方法判断key
是否存在。
- 如果
26.5. 示例代码
HashMap<String, Integer> map = new HashMap<>();
map.put(null, 1); // key 为 null
map.put("a", null); // value 为 null
System.out.println(map.get(null)); // 输出 1
System.out.println(map.get("a")); // 输出 null
System.out.println(map.containsKey("a")); // 输出 true
27. ConcurrentHashMap 的迭代器是强一致性还是弱一致性?
- 弱一致性:
ConcurrentHashMap
的迭代器不会抛出ConcurrentModificationException
。- 迭代器反映的是创建迭代器时的状态,后续的修改可能不会反映到迭代器中。
这里涉及到fail-fast和fail-safe的知识点
28. ConcurrentHashMap 的适用场景是什么?
- 高并发场景:
- 如缓存、计数器、并发任务调度等。
- 需要线程安全的
Map
:- 如多线程环境下的数据共享。
29. ConcurrentHashMap 的源码分析(JDK 1.8)
put 方法源码解析
public V put(K key, V value) {
return putVal(key, value, false);
}
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
break;
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key, value);
break;
}
}
}
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
ConcurrentHashMap
是 Java 并发编程中非常重要的工具,通过分段锁(JDK 1.7)或 CAS
+ synchronized
(JDK 1.8)实现线程安全。