Update Java 容器.md

重新绘制配图,补充 Arrays.asList 的注意事项、fail-fast 示例和 JDK 1.8 中 HashMap 的变化,增加实测附录。
This commit is contained in:
CyC2018
2026-10-28 03:18:45 +08:00
parent 941103fd81
commit 058fa26737
9 changed files with 163 additions and 56 deletions
+163 -56
View File
@@ -16,6 +16,9 @@
* [ConcurrentHashMap](#concurrenthashmap)
* [LinkedHashMap](#linkedhashmap)
* [WeakHashMap](#weakhashmap)
* [附录:实测](#附录实测)
* [A1 ArrayList 的扩容与常见陷阱](#a1-arraylist-的扩容与常见陷阱)
* [A2 HashMap 的并发写入](#a2-hashmap-的并发写入)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
@@ -26,40 +29,32 @@
### Collection
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/image-20191208220948084.png"/> </div><br>
<div align="center"> <img src="pics/jcol-collection.png" width="760px"/> </div><br>
#### 1. Set
- TreeSet:基于红黑树实现,支持有序性操作,例如根据一个范围查找元素的操作。但是查找效率不如 HashSet,HashSet 查找的时间复杂度为 O(1),TreeSet 则为 O(logN)。
- HashSet:基于哈希表实现,支持快速查找,但不支持有序性操作。并且失去了元素的插入顺序信息,也就是说使用 Iterator 遍历 HashSet 得到的结果是不确定的。
- TreeSet:基于红黑树实现,支持有序性操作,例如根据一个范围查找元素的操作。但是查找效率不如 HashSet,HashSet 查找的时间复杂度为 O(1),TreeSet 则为 O(logN);
- HashSet:基于哈希表实现,支持快速查找,但不支持有序性操作。并且失去了元素的插入顺序信息,也就是说使用 Iterator 遍历 HashSet 得到的结果是不确定的;
- LinkedHashSet:具有 HashSet 的查找效率,并且内部使用双向链表维护元素的插入顺序。
#### 2. List
- ArrayList:基于动态数组实现,支持随机访问。
- Vector:和 ArrayList 类似,但它是线程安全的。
- ArrayList:基于动态数组实现,支持随机访问;
- Vector:和 ArrayList 类似,但它是线程安全的;
- LinkedList:基于双向链表实现,只能顺序访问,但是可以快速地在链表中间插入和删除元素。不仅如此,LinkedList 还可以用作栈、队列和双向队列。
#### 3. Queue
- LinkedList:可以用它来实现双向队列。
- LinkedList:可以用它来实现双向队列;
- PriorityQueue:基于堆结构实现,可以用它来实现优先队列。
### Map
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/image-20201101234335837.png"/> </div><br>
- TreeMap:基于红黑树实现。
- HashMap:基于哈希表实现。
- Hashtable:和 HashMap 类似,但它是线程安全的,这意味着同一时刻多个线程同时写入 Hashtable 不会导致数据不一致。它是遗留类,不应该去使用它,而是使用 ConcurrentHashMap 来支持线程安全,ConcurrentHashMap 的效率会更高,因为 ConcurrentHashMap 引入了分段锁。
<div align="center"> <img src="pics/jcol-map.png" width="760px"/> </div><br>
- TreeMap:基于红黑树实现;
- HashMap:基于哈希表实现;
- Hashtable:和 HashMap 类似,但它是线程安全的,这意味着同一时刻多个线程同时写入 Hashtable 不会导致数据不一致。它是遗留类,不应该去使用它,而是使用 ConcurrentHashMap 来支持线程安全。ConcurrentHashMap 的效率更高,因为它不会锁住整个表:JDK 1.7 使用分段锁,JDK 1.8 只锁住一个桶;
- LinkedHashMap:使用双向链表来维护元素的顺序,顺序为插入顺序或者最近最少使用(LRU)顺序。
@@ -67,7 +62,7 @@
### 迭代器模式
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/image-20191208225301973.png"/> </div><br>
<div align="center"> <img src="pics/jcol-iterator.png" width="760px"/> </div><br>
Collection 继承了 Iterable 接口,其中的 iterator() 方法能够产生一个 Iterator 对象,通过这个对象就可以迭代遍历 Collection 中的元素。
@@ -104,11 +99,17 @@ List list = Arrays.asList(arr);
List list = Arrays.asList(1, 2, 3);
```
asList() 返回的是数组的一个视图,而不是一个新的 ArrayList,使用时需要注意(JDK 21 实测,见[附录 A1](#a1-arraylist-的扩容与常见陷阱)):
- 长度固定:可以 set(),但调用 add()、remove() 会抛出 UnsupportedOperationException;
- 与数组共用存储:修改原数组后,List 中的元素也跟着改变;
- 传入 int[] 这样的基本类型数组时,整个数组被当成一个元素,得到的 List 长度为 1。
需要一个可以修改的列表时,用 new ArrayList<>(Arrays.asList(arr)) 复制一份。
## 三、源码分析
如果没有特别说明,以下源码分析基于 JDK 1.8。
在 IDEA 中 double shift 调出 Search EveryWhere,查找源码文件,找到之后就可以阅读源码。
如果没有特别说明,以下源码分析基于 JDK 1.8。JDK 21 中这些类的主要逻辑没有变化,有差异的地方单独说明。
### ArrayList
@@ -122,17 +123,17 @@ public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
```
数组的默认大小为 10。
数组的默认大小为 10。用无参构造函数创建时,elementData 先指向一个长度为 0 的共享空数组,第一次添加元素时才分配长度为 10 的数组。
```java
private static final int DEFAULT_CAPACITY = 10;
```
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/image-20191208232221265.png"/> </div><br>
<div align="center"> <img src="pics/jcol-arraylist.png" width="760px"/> </div><br>
#### 2. 扩容
添加元素时使用 ensureCapacityInternal() 方法来保证容量足够,如果不够时,需要使用 grow() 方法进行扩容,新容量的大小为 `oldCapacity + (oldCapacity >> 1)`,即 oldCapacity+oldCapacity/2。其中 oldCapacity >> 1 需要取整,所以新容量大约是旧容量的 1.5 倍左右。(oldCapacity 为偶数就是 1.5 倍,为奇数就是 1.5 倍-0.5)
添加元素时使用 ensureCapacityInternal() 方法来保证容量足够,如果不够时,需要使用 grow() 方法进行扩容,新容量的大小为 `oldCapacity + (oldCapacity >> 1)`,即 oldCapacity+oldCapacity/2。其中 oldCapacity >> 1 需要取整,所以新容量大约是旧容量的 1.5 倍(oldCapacity 为偶数就是 1.5 倍,为奇数就是 1.5 倍减 0.5)。例如依次添加元素时,容量的变化为 0、10、15、22、33(JDK 21 实测,见[附录 A1](#a1-arraylist-的扩容与常见陷阱))。JDK 21 的 grow() 改为调用 ArraysSupport.newLength() 计算新容量,增长的规则相同。
扩容操作需要调用 `Arrays.copyOf()` 把原数组整个复制到新数组中,这个操作代价很高,因此最好在创建 ArrayList 对象时就指定大概的容量大小,减少扩容操作的次数。
@@ -258,6 +259,21 @@ modCount 用来记录 ArrayList 结构发生变化的次数。结构发生变化
在进行序列化或者迭代等操作时,需要比较操作前后 modCount 是否改变,如果改变了需要抛出 ConcurrentModificationException。代码参考上节序列化中的 writeObject() 方法。
最常见的情况是在 foreach 循环中删除元素。foreach 编译后使用迭代器,list.remove() 改变了 modCount,迭代器下一次调用 next() 时就会抛出异常。应该使用迭代器自己的 remove(),或者 removeIf():
```java
for (String s : list) {
if (s.equals("a")) list.remove(s); // ConcurrentModificationException
}
Iterator<String> it = list.iterator();
while (it.hasNext()) {
if (it.next().equals("a")) it.remove();
}
list.removeIf(s -> s.equals("a"));
```
### Vector
@@ -350,7 +366,7 @@ List<String> list = new CopyOnWriteArrayList<>();
写操作需要加锁,防止并发写入时导致写入数据丢失。
写操作结束之后需要把原始数组指向新的复制数组。
写操作结束之后需要把原始数组指向新的复制数组。JDK 21 中写操作改为用 synchronized 加锁,过程相同。
```java
public boolean add(E e) {
@@ -412,7 +428,7 @@ transient Node<E> first;
transient Node<E> last;
```
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/image-20191208233940066.png"/> </div><br>
<div align="center"> <img src="pics/jcol-linkedlist.png" width="760px"/> </div><br>
#### 2. 与 ArrayList 的比较
@@ -421,15 +437,22 @@ ArrayList 基于动态数组实现,LinkedList 基于双向链表实现。Array
- 数组支持随机访问,但插入删除的代价很高,需要移动大量元素;
- 链表不支持随机访问,但插入删除只需要改变指针。
需要注意的是,链表在中间插入前要先遍历到插入位置,这一步也是 O(N) 的,而且要逐个访问分散在内存中的节点;数组移动元素用的是 System.arraycopy(),一次复制一整块连续内存。例如在 10 万个元素的中间插入 1 万次,ArrayList 约 50 ms,LinkedList 约 0.8~1 秒;只有在头部插入时 LinkedList 才明显更快(JDK 21 实测,见[附录 A1](#a1-arraylist-的扩容与常见陷阱))。所以大多数场景下应该使用 ArrayList,需要在头尾频繁插入删除时使用 ArrayDeque 或 LinkedList。
### HashMap
为了便于理解,以下源码分析以 JDK 1.7 为主。
为了便于理解,以下源码分析以 JDK 1.7 为主。JDK 1.8 主要有以下变化,JDK 21 与 JDK 1.8 相同:
- 数组元素由 Entry 改为 Node,链表长度达到 8 时转为红黑树,见第 8 节;
- 新的键值对插入链表尾部,而不是头部;
- 计算哈希值时只做一次扰动:(h = key.hashCode()) ^ (h >>> 16),把高 16 位异或到低 16 位,让高位也参与决定桶下标;
- 扩容时不再逐个重新计算桶下标,而是按哈希值新增的那一位把链表拆成两条,分别放在原位置和原位置 + 旧容量,见第 6 节。
#### 1. 存储结构
内部包含了一个 Entry 类型的数组 table。Entry 存储着键值对。它包含了四个字段,从 next 字段我们可以看出 Entry 是一个链表。即数组中的每个位置被当成一个桶,一个桶存放一个链表。HashMap 使用拉链法来解决冲突,同一个链表中存放哈希值和散列桶取模运算结果相同的 Entry。
内部包含了一个 Entry 类型的数组 table。Entry 存储着键值对。它包含了四个字段,从 next 字段可以看出 Entry 是一个链表。即数组中的每个位置被当成一个桶,一个桶存放一个链表。HashMap 使用拉链法来解决冲突,同一个链表中存放哈希值对桶的数量取模结果相同的 Entry。
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/image-20191208234948205.png"/> </div><br>
<div align="center"> <img src="pics/jcol-hashmap.png" width="760px"/> </div><br>
```java
transient Entry[] table;
@@ -498,8 +521,8 @@ map.put("K3", "V3");
```
- 新建一个 HashMap,默认大小为 16;
- 插入 &lt;K1,V1\> 键值对,先计算 K1 的 hashCode,为了便于说明,假设为 115(实际上 "K1".hashCode() 是 2374),使用除留余数法得到所在的桶下标 115%16=3。
- 插入 &lt;K2,V2\> 键值对,假设 K2 的 hashCode 为 118,使用除留余数法得到所在的桶下标 118%16=6。
- 插入 &lt;K1,V1\> 键值对,先计算 K1 的 hashCode,为了便于说明,假设为 115(实际上 "K1".hashCode() 是 2374),使用除留余数法得到所在的桶下标 115%16=3;
- 插入 &lt;K2,V2\> 键值对,假设 K2 的 hashCode 为 118,使用除留余数法得到所在的桶下标 118%16=6;
- 插入 &lt;K3,V3\> 键值对,假设 K3 的 hashCode 也为 118,使用除留余数法得到所在的桶下标 118%16=6,插在 &lt;K2,V2\> 前面。
应该注意到链表的插入是以头插法方式进行的,例如上面的 &lt;K3,V3\> 不是插在 &lt;K2,V2\> 后面,而是插入在链表头部。JDK 1.8 改成了尾插法。
@@ -509,7 +532,7 @@ map.put("K3", "V3");
- 计算键值对所在的桶;
- 在链表上顺序查找,时间复杂度显然和链表的长度成正比。
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/image-20191208235258643.png"/> </div><br>
<div align="center"> <img src="pics/jcol-chaining.png" width="760px"/> </div><br>
#### 3. put 操作
@@ -599,7 +622,7 @@ int hash = hash(key);
int i = indexFor(hash, table.length);
```
**4.1 计算 hash 值**
##### 4.1 计算哈希值
```java
final int hash(Object k) {
@@ -618,13 +641,16 @@ final int hash(Object k) {
}
```
JDK 1.8 简化为一次扰动:
```java
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
```
**4.2 取模**
##### 4.2 取模
令 x = 1\<\<4,即 x 为 2 的 4 次方,它具有以下性质:
@@ -649,9 +675,9 @@ x : 00010000
y%x : 00000010
```
我们知道,位运算的代价比求模运算小的多,因此在进行这种计算时用位运算的话能带来更高的性能。
位运算的代价比求模运算小得多,因此在进行这种计算时用位运算的话能带来更高的性能。
确定桶下标的最后一步是将 key 的 hash 值对桶个数取模:hash%capacity,如果能保证 capacity 为 2 的 n 次方,那么就可以将这个操作转换为位运算。
确定桶下标的最后一步是将 key 的哈希值对桶个数取模:hash%capacity,如果能保证 capacity 为 2 的 n 次方,那么就可以将这个操作转换为位运算。
```java
static int indexFor(int h, int length) {
@@ -671,7 +697,7 @@ static int indexFor(int h, int length) {
| :--: | :-- |
| capacity | table 的容量大小,默认为 16。需要注意的是 capacity 必须保证为 2 的 n 次方。|
| size | 键值对数量。 |
| threshold | size 的临界值,当 size 大于等于 threshold 就必须进行扩容操作。 |
| threshold | size 的临界值。JDK 1.7 中 size 大于等于 threshold、并且要插入的桶不为空时扩容;JDK 1.8 中插入后 size 大于 threshold 时扩容。 |
| loadFactor | 装载因子,table 能够使用的比例,threshold = (int)(capacity* loadFactor)。 |
```java
@@ -744,7 +770,7 @@ void transfer(Entry[] newTable) {
假设原数组长度 capacity 为 16,扩容之后 new capacity 为 32:
```html
```text
capacity : 00010000
new capacity : 00100000
```
@@ -754,6 +780,8 @@ new capacity : 00100000
- 为 0,那么 hash%00010000 = hash%00100000,桶位置和原来一致;
- 为 1,hash%00100000 = hash%00010000 + 16,桶位置是原位置 + 16。
JDK 1.8 的 resize() 就利用了这个性质:遍历旧桶中的链表,按 (hash & oldCap) 是否为 0 把节点分到 lo、hi 两条链表,分别放到新数组的 j 和 j + oldCap 位置,链表中节点的相对顺序保持不变。
<div align="center"> <img src="pics/hashmap-resize.png" width="620px"/> </div><br>
#### 7. 计算数组容量
@@ -795,22 +823,24 @@ static final int tableSizeFor(int cap) {
#### 9. 与 Hashtable 的比较
- Hashtable 使用 synchronized 来进行同步。
- HashMap 可以插入键为 null 的 Entry。
- HashMap 的迭代器是 fail-fast 迭代器。
- Hashtable 使用 synchronized 来进行同步;
- HashMap 可以插入键为 null 的 Entry;
- HashMap 的迭代器是 fail-fast 迭代器;
- HashMap 不能保证随着时间的推移 Map 中的元素次序是不变的。
#### 10. 线程安全
HashMap 不是线程安全的。例如两个线程同时向同一个空桶插入,都判断桶为空,后写入的会覆盖先写入的;size++ 也不是原子操作。在 JDK 21 上用 8 个线程各插入 10 万个不同的键,HashMap 的 size 每次都不同,例如 281854、463269,远少于 80 万;同样的操作 ConcurrentHashMap 每次都是 800000。
HashMap 不是线程安全的。例如两个线程同时向同一个空桶插入,都判断桶为空,后写入的会覆盖先写入的;size++ 也不是原子操作。用 8 个线程各插入 10 万个不同的键,HashMap 的 size 每次都不同,例如 111068、323870,远少于 80 万;同样的操作 ConcurrentHashMap 每次都是 800000(JDK 21 实测,见[附录 A2](#a2-hashmap-的并发写入))。
JDK 1.7 的问题更严重:两个线程同时扩容时,头插法会把链表的顺序反转,可能让链表形成环,之后在这个桶上的 get 会陷入死循环。JDK 1.8 改为尾插法,扩容时保持链表原来的顺序,不会再成环,但并发写入仍然会丢失数据。多线程环境应该使用 ConcurrentHashMap。
JDK 1.7 的问题更严重:两个线程同时扩容时,头插法会把链表的顺序反转,可能让链表形成环,之后在这个桶上的 get 会陷入死循环。JDK 1.8 改为尾插法,扩容时保持链表原来的顺序,不会再因为头插法成环,但并发写入仍然会丢失数据;链表转为红黑树时还可能破坏树的结构,实测中出现过线程抛出 ClassCastException,以及线程在 TreeNode.treeify() 中一直循环不返回的情况(见[附录 A2](#a2-hashmap-的并发写入))。多线程环境应该使用 ConcurrentHashMap。
### ConcurrentHashMap
第 1、2 节是 JDK 1.7 的实现,JDK 1.8 的改动见第 3 节。
#### 1. 存储结构
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/image-20191209001038024.png"/> </div><br>
<div align="center"> <img src="pics/jcol-chm7.png" width="760px"/> </div><br>
```java
static final class HashEntry<K,V> {
@@ -876,7 +906,6 @@ ConcurrentHashMap 在执行 size 操作时先尝试不加锁,如果连续两
如果尝试的次数超过 3 次,就需要对每个 Segment 加锁。
```java
/**
* Number of unsynchronized retries in size and containsValue
* methods before resorting to locking. This is used to avoid
@@ -937,7 +966,7 @@ JDK 1.8 去掉了 Segment,和 HashMap 一样使用 Node 数组加链表(或
1. 用 spread() 计算哈希值,把 hashCode 的高 16 位异或到低 16 位,让高位也参与决定桶下标;
2. 如果桶是空的,用 CAS 把新节点放进去,不加锁。CAS 失败说明有其它线程刚刚放入了节点,重新循环;
3. 如果桶的头节点是 ForwardingNode(哈希值为 MOVED),说明正在扩容,当前线程先帮忙迁移数据;
4. 否则用 synchronized 锁住桶的头节点,在链表或红黑树中插入或更新,链表长度达到 8 时转为红黑树;
4. 否则用 synchronized 锁住桶的头节点,在链表或红黑树中插入或更新,链表长度达到 8 时转为红黑树,数组长度小于 64 时则只扩容;
5. 最后更新元素个数。
get 不加锁:数组元素通过 Unsafe 的 volatile 方式读取,节点的 val 和 next 都是 volatile,能读到其它线程写入的最新值。
@@ -1045,7 +1074,7 @@ protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
以下是使用 LinkedHashMap 实现的一个 LRU 缓存:
- 设定最大缓存空间 MAX_ENTRIES 为 3;
- 设定最大缓存空间 MAX_ENTRIES 为 3;
- 使用 LinkedHashMap 的构造函数将 accessOrder 设置为 true,开启 LRU 顺序;
- 覆盖 removeEldestEntry() 方法实现,在节点多于 MAX_ENTRIES 就会将最近最久未使用的数据移除。
@@ -1075,7 +1104,7 @@ public static void main(String[] args) {
}
```
```html
```text
[3, 1, 4]
```
@@ -1083,7 +1112,7 @@ public static void main(String[] args) {
#### 存储结构
WeakHashMap 的 Entry 继承自 WeakReference,被 WeakReference 关联的对象在下一次垃圾回收时会被回收。
WeakHashMap 的 Entry 继承自 WeakReference,键被 WeakReference 关联,没有其它强引用时,键在下一次垃圾收集时会被回收,对应的 Entry 随后从 Map 中清除。
WeakHashMap 主要用来实现缓存,通过使用 WeakHashMap 来引用缓存对象,由 JVM 对这部分缓存进行回收。
@@ -1098,8 +1127,8 @@ Tomcat 中的 ConcurrentCache 使用了 WeakHashMap 来实现缓存功能。
ConcurrentCache 采取的是分代缓存:
- 经常使用的对象放入 eden 中,eden 使用 ConcurrentHashMap 实现,不用担心会被回收(伊甸园);
- 不常用的对象放入 longterm,longterm 使用 WeakHashMap 实现,这些老对象会被垃圾收集器回收。
- 当调用 get() 方法时,会先从 eden 区获取,如果没有找到的话再到 longterm 获取,当从 longterm 获取到就把对象放入 eden 中,从而保证经常被访问的节点不容易被回收。
- 不常用的对象放入 longterm,longterm 使用 WeakHashMap 实现,这些老对象会被垃圾收集器回收;
- 当调用 get() 方法时,会先从 eden 区获取,如果没有找到的话再到 longterm 获取,当从 longterm 获取到就把对象放入 eden 中,从而保证经常被访问的节点不容易被回收;
- 当调用 put() 方法时,如果 eden 的大小超过了 size,那么就将 eden 中的所有对象都放入 longterm 中,利用虚拟机回收掉一部分不经常使用的对象。
```java
@@ -1138,6 +1167,82 @@ public final class ConcurrentCache<K, V> {
```
## 附录:实测
以下实验的环境为 Apple M1 Pro(10 核)、macOS,JDK 为 Adoptium OpenJDK 21.0.13。
### A1 ArrayList 的扩容与常见陷阱
用反射读取 elementData 的长度,运行时需要加上 --add-opens java.base/java.util=ALL-UNNAMED:
```java
Field f = ArrayList.class.getDeclaredField("elementData");
f.setAccessible(true);
ArrayList<String> list = new ArrayList<>();
for (int i = 1; i <= 23; i++) {
list.add("e" + i);
int cap = ((Object[]) f.get(list)).length; // 容量变化时打印
}
```
```text
new ArrayList<>():容量 0
添加第 1 个元素后:容量 10,size 1
添加第 11 个元素后:容量 15,size 11
添加第 16 个元素后:容量 22,size 16
添加第 23 个元素后:容量 33,size 23
```
正文中 foreach 删除元素和 Arrays.asList() 的几种用法:
```text
foreach 中 remove:ConcurrentModificationException
Iterator.remove:[b, c]
removeIf:[b, c]
asList set:[10, 2, 3]
asList add:UnsupportedOperationException
修改数组后 list:[100, 2, 3]
Arrays.asList(int[]).size():1
```
ArrayList 与 LinkedList 的插入:先放入 10 万个元素,再执行 list.add(list.size() / 2, i) 1 万次;另外从空列表开始执行 list.add(0, i) 10 万次:
```text
round 0: 10 万个元素中间插入 1 万次 ArrayList 51 ms,LinkedList 844 ms;头部插入 10 万次 ArrayList 445 ms,LinkedList 2 ms
round 1: 10 万个元素中间插入 1 万次 ArrayList 47 ms,LinkedList 1029 ms;头部插入 10 万次 ArrayList 441 ms,LinkedList 3 ms
round 2: 10 万个元素中间插入 1 万次 ArrayList 47 ms,LinkedList 831 ms;头部插入 10 万次 ArrayList 441 ms,LinkedList 0 ms
```
### A2 HashMap 的并发写入
8 个线程各插入 10 万个不同的键(线程 t 插入 t × 100000 到 t × 100000 + 99999),结束后查看 size:
```java
for (int t = 0; t < 8; t++) {
int base = t * 100_000;
pool.submit(() -> { for (int i = 0; i < 100_000; i++) map.put(base + i, i); });
}
```
```text
HashMap size=111068 ConcurrentHashMap size=800000
HashMap size=323870 ConcurrentHashMap size=800000
HashMap size=732440 ConcurrentHashMap size=800000
```
改用守护线程,每轮最多等待 10 秒,记录没有结束的线程停在哪里。两次运行的输出(节选):
```text
Exception in thread "putter-6" java.lang.ClassCastException: class java.util.HashMap$Node cannot be cast to class java.util.HashMap$TreeNode
at java.base/java.util.HashMap$TreeNode.moveRootToFront(HashMap.java:1994)
at java.base/java.util.HashMap$TreeNode.treeify(HashMap.java:2110)
at java.base/java.util.HashMap.treeifyBin(HashMap.java:778)
run 0: size=278394 stuckThreads=0
run 0: size=263620 stuckThreads=4
putter-4 at java.base/java.util.HashMap$TreeNode.treeify(HashMap.java:2107) / java.base/java.util.HashMap$TreeNode.split(HashMap.java:2339)
```
## 参考资料
- Eckel B. Java 编程思想 [M]. 机械工业出版社, 2002.
@@ -1149,5 +1254,7 @@ public final class ConcurrentCache<K, V> {
- [探索 ConcurrentHashMap 高并发性的实现机制](https://www.ibm.com/developerworks/cn/java/java-lo-concurrenthashmap/)
- [HashMap 相关面试题及其解答](https://www.jianshu.com/p/75adf47958a7)
- [Java 集合细节(二):asList 的缺陷](http://wiki.jikexueyuan.com/project/java-enhancement/java-thirtysix.html)
- [Java Collection Framework – The LinkedList Class](http://javaconceptoftheday.com/java-collection-framework-linkedlist-class/)
- [Java Collection Framework – The LinkedList Class](https://javaconceptoftheday.com/java-collection-framework-linkedlist-class/)
- [JDK 8 源码 java.util](https://github.com/openjdk/jdk8u/tree/master/jdk/src/share/classes/java/util)
- [Java SE 21 API: Arrays.asList](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/Arrays.html#asList(T...))
- [Java SE 21 API: ArrayDeque](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/ArrayDeque.html)
Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB