map类相关知识点总结
主要关注map相关的源码
HashTable、HashMap、HashSet 、TreeMap…
HashTable
基于位桶+链表实现。
- 继承Dictionary(陈旧)
- 实现Cloneable,可被克隆
- 实现Serializable,可被序列化
hash
1 | int hash = key.hashCode(); |
很容易看出Hashtable
的hash
算法首先使得hash
的值小于等于整型数的最大值,再通过%
运算实现均匀散射。
==由于计算机是底层的运算是基于2进制的,所以HashMap的hash算法使用&运算代替%运算,在运算速度上明显HashMap的hash算法更优。==
属性
构造
1 | public Hashtable(int initialCapacity, float loadFactor) { |
Hashtable
底层数组的长度可以为任意值,这就造成了当底层数组长度为合数的时候,Hashtable
的hash算法散射不均匀,容易产生hash冲突。所以,可以清楚的看到Hashtable
的默认构造函数底层数组长度为11(质数),至于为什么Hashtable
的底层数组用质数较好,请参考博文;
方法
put
- value不能为null
- 线程安全
1 | public synchronized V put(K key, V value) { |
可以看出HashTable
到了jdk1.8了内部结构并没有实质优化,继续使用数组+链表的方式实现。
rehash
1 | protected void rehash() { |
Hashtable
的扩容将先创建一个长度为原长度2
倍的数组,再使用头插法将链表进行反序。
和HashMap的不同之处
HashMap | Hashtable | |
---|---|---|
继承机制 | 继承AbstractMap | 继承Dictionary |
线程安全 | 线程不安全 | put、remove等方法均被synchronized修饰,同步,线程安全 |
参数设置 | key、value均可为null | key、value均不可为null |
结构 | 由位桶+链表+红黑树构成 | 由位桶+链表构成 |
添加entry | 添加新entry时,放置链表尾部 | 添加新entry时,放置链表头部 |
构造上 | 底层数组的长度必须为2^n ,默认长度16 |
底层数组的长度可以为任意值,默认长度11 |
Hash算法上 | hash算法通过非常规的设计,将底层table长度设计为2^n(合数)并使用了&运算来代替%运算以减少性能上面的损耗 | hash算法首先使得hash的值小于等于整型数的最大值,再通过%运算实现均匀散射 |
扩容机制上 | 扩容不需要链表反转,用hashCode新增的bit位查看是否插入旧数组还是插入新数组位置 | 头插法扩容 |
HashMap
HashMap就是典型的拉链法哈希表结构。·
他实际上是一个线性数组。他的静态内部类继承了一个Entry
接口。这里注意,在jdk1.8中,在链表中加入了红黑树(平衡二分查找树)。所以1.8版本的HashMap
是由数组+(链表+红黑树)实现的。
hash
基本概念
Hash,即散列,把任意长度的输入,通过散列算法变成固定长度的输出。由于是不定长到定长的压缩映射,输出值域小于输入值域,所以不同的输入可能有相同的输出,即hash碰撞。
hash算法
1 | static final int hash(Object key) { |
这里Hash算法的本质是取key的hashCode值、高位运算、取模运算 ·
在JDK1.8的实现中,优化了高位运算的算法,通过hashCode()的高16位异或低16位实现的:(h = k.hashCode()) ^ (h >>> 16)
,主要是从速度、功效、质量来考虑的,这么做可以在数组table的length比较小的时候,也能保证考虑到高低Bit都参与到Hash的计算中,同时不会有太大的开销。
属性
1 | public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { |
构造
可以看出HashMap
的底层数组的长度必须为2^n
,这样做的好处是为以后的hash
算法做准备
1 | public HashMap(int initialCapacity, float loadFactor) { |
方法
所有方法集合
1 | void clear() // 移除所有键值 |
get/getNode
1 | public V get(Object key) { |
具体逻辑:
计算桶在桶数组的位置
(first = tab[(n - 1) & hash]) != null
1
a % b == (b-1) & a ,当b是2的指数时,等式成立。
判断
hashcode
是否相等,对象是否相等判断是否是TreeNode类型
put/putVal

1 | public V put(K key, V value) { |
1 | final V putVal(int hash, K key, V value, boolean onlyIfAbsent, |
具体逻辑:
- 初始化桶数组,判断是否需要扩容
- 判断key是否存在,若不存在就新创建一个 节点
- 若存在,首先判断键的值和节点的hash值是否是链表第一个节点,是则插入
- 其次判断是否是树节点,是则执行树的插入
- 如果都不是就遍历链表,插入节点,大于阙值就树化,遇到相等的节点就覆盖(hash相等,键相等)
onlyIfAbsent参数
put
的onlyIfAbesent
的false
,putIfAbsent
的onlyIfAbsent
是true
在放入数据时,如果存在重复的key
,那么putIfAbsent
不会放入值。
如果传入key
对应的value
已经存在,就返回存在的value
,不进行替换。如果不存在,就添加key
和value
,返回null
resize
resize()
只有在两种情况下会被调用:·
- 由于
HashMap
实行了懒加载: 新建HashMap
时不会对table
进行赋值, 而是到第一次插入时, 进行resize
时构建table
; - 当
HashMap
的size
值大于threshold
时, 会进行resize()
; 看一下threshold
在源码中的注解:
1 | final Node<K,V>[] resize() { |
具体逻辑
- 若table为null或容量为0,则使用默认值
16
扩容,临界值为16*0.75
,否则2 - 若容量是否超过设定的最大容量,重置临界值不扩容返回,否则3
- 容器容量加倍,临界值加倍·
- 若容器原来不为空,则需迁移数据
oldTab[i]
链表中数据分别移至newTab[i]
和newTab[i+oldTab.length]
中
经过观测可以发现,我们使用的是2次幂的扩展(指长度扩为原来2倍),所以,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置
jdk1.8对扩容进行优化,使得扩容不再需要进行链表的反转,只需要知道hashcode新增的bit位为0还是1。如果是0就在原索引位置,新增索引是1就在oldIndex+oldCap位置。
推荐看一下这篇重新认识HashMap,对扩容写的很清楚
线程安全性
反例,会造成无限环
主要原因在于 并发下的Rehash
会造成元素之间会形成一个循环链表。不过,jdk 1.8 后解决了这个问题,但是还是不建议在多线程下使用 HashMap
,因为多线程下使用 HashMap
还是会存在其他问题比如数据丢失。并发环境下推荐使用 ConcurrentHashMap
。·
1 | public class HashMapInfiniteLoop { |
简单介绍线程安全的
ConcurrentHashMap
·
jdk1.7
- 底层采用分段的数组+链表实现,线程安全
- 通过把整个
Map
分为N
个Segment
,可以提供相同的线程安全,但是效率提升N
倍,默认提升16
倍。(读操作不加锁,由于HashEntry
的value
变量是volatile
的,也能保证读取到最新的值。) Hashtable
的synchronized
是针对整张Hash
表的,即每次锁住整张表让线程独占,ConcurrentHashMap
允许多个修改操作并发进行,其关键在于使用了锁分离技术- 有些方法需要跨段,比如
size()
和containsValue()
,它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁 - 扩容:段内扩容(段内元素超过该段对应Entry数组长度的75%触发扩容,不会对整个Map进行扩容),插入前检测需不需要扩容,有效避免无效扩容
jdk1.8
底层数据结构: JDK1.7的
ConcurrentHashMap
底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟HashMap1.8
的结构一样,数组+链表/红黑二叉树。Hashtable
和 JDK1.8 之前的HashMap
的底层数据结构类似都是采用 数组+链表 的形式,数组是HashMap
的主体,链表则是主要为了解决哈希冲突而存在的;实现线程安全的方式(重要):
① 在JDK1.7的时候,
ConcurrentHashMap
(分段锁) 对整个桶数组进行了分割分段(Segment
),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。 到了 JDK1.8 的时候已经摒弃了Segment
的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用synchronized
和CAS
来操作。(JDK1.6以后 对synchronized
锁做了很多优化) 整个看起来就像是优化过且线程安全的HashMap
,虽然在JDK1.8中还能看到Segment
的数据结构,但是已经简化了属性,只是为了兼容旧版本;② Hashtable(同一把锁) :使用
synchronized
来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。
WeakHashMap
HashMap和WeakHashMap的相同点
- 它们都是散列表,存储的是“键值对”映射。
- 它们都继承于
AbstractMap
,并且实现Map基础。 - 它们的构造函数都一样。
它们都包括4个构造函数,而且函数的参数都一样。 - 默认的容量大小是16,默认的加载因子是0.75。
- 它们的“键”和“值”都允许为null。
- 它们都是“非同步的”。
HashMap和WeakHashMap的不同点
HashMap实现了Cloneable
和Serializable
接口,而WeakHashMap没有。
HashMap实现Cloneable,意味着它能通过clone()克隆自己。
HashMap实现Serializable,意味着它支持序列化,能通过序列化去传输。
HashMap的“键”是“强引用(StrongReference
)”,而WeakHashMap的键是“弱引用(WeakReference
)”。
WeakReference的“弱键”能实现WeakReference对“键值对”的动态回收。当“弱键”不再被使用到时,GC会回收它,WeakReference也会将“弱键”对应的键值对删除。
这个“弱键”实现的动态回收“键值对”的原理呢?其实,通过WeakReference(弱引用)和ReferenceQueue(引用队列)实现的。 首先,我们需要了解WeakHashMap中:
第一,“键”是WeakReference,即key是弱键。
第二,ReferenceQueue是一个引用队列,它是和WeakHashMap联合使用的。当弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。 WeakHashMap中的ReferenceQueue是queue。
第三,WeakHashMap是通过数组实现的,我们假设这个数组是table。
动态回收步骤
1 | 将WeakHashMap中的key设置null,并执行gc()。系统会回收key |
- 新建WeakHashMap,将“键值对”添加到WeakHashMap中。
将“键值对”添加到WeakHashMap中时,添加的键都是弱键。 实际上,WeakHashMap是通过数组table保存Entry(键值对);每一个Entry实际上是一个单向链表,即Entry是键值对链表。
- 当某“弱键”不再被其它对象引用,并被GC回收时。在GC回收该“弱键”时,这个“弱键”也同时会被添加到queue队列中。
例如,当我们在将“弱键”key添加到WeakHashMap之后;后来将key设为null。这时,便没有外部外部对象再引用该了key。 接着,当Java虚拟机的GC回收内存时,会回收key的相关内存;同时,将key添加到queue队列中。
- 当下一次我们需要操作WeakHashMap时,会先同步table和queue。table中保存了全部的键值对,而queue中保存被GC回收的“弱键”;同步它们,就是删除table中被GC回收的“弱键”对应的键值对。
HashSet
HashSet基于HashMap来实现的,底层是使用HashMap的Key保存元素,Value统一为类的常量Object对象。
继承AbstractCollection,实现Set、Serializable、Cloneable接口。
- Serializable:可被序列化
- Cloneable:可被克隆
特性:
- 无序
- 唯一
- 可为null
- 不同步(非线程安全)
属性
1 | // 使用map的Key保存元素 |
构造
1 | // 使用HashMap的初始容量0和默认加载因子0.75f来初始化(JDK1.8) |
方法
1 | boolean add(E e) // 添加元素 |
add
1 | public boolean add(E e) { |
方法声明只有当元素尚未存在于集合中时才会添加元素。如果成功添加了元素,则该方法返回true,否则返回false。
remove
1 | public boolean remove(Object o) { |
HashSet如何保持唯一性?
当我们将一个对象放入一个HashSet
时,它使用该对象的hashcode
值来确定一个元素是否已经在该集合中。
每个散列码值对应于某个块位置,该块位置可以包含计算出的散列值相同的各种元素。但是具有相同hashCode
的两个对象可能不相等。
因此,将使用equals()
方法比较同一存储桶中的对象。
hashCode()与equals()的相关规定:
- 如果两个对象相等,则hashcode一定也是相同的
- 两个对象相等,对两个equals方法返回true
- 两个对象有相同的hashcode值,它们也不一定是相等的
- 综上,equals方法被覆盖过,则hashCode方法也必须被覆盖
- hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则该class的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)。
性能
HashSet
的性能主要受两个参数影响 - 初始容量和负载因子。
较低的初始容量降低了空间复杂性,但增加了重新散布的频率,这是一个昂贵的过程。
另一方面,高初始容量增加了迭代成本和初始内存消耗。
HashMap 和 HashSet区别
TreeMap
TreeMap是基于红黑树(Red-Black Tree)
TreeMap实现SortedMap接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用Iterator遍历TreeMap时,得到的记录是排过序的。如果使用排序的映射,建议使用TreeMap。在使用TreeMap时,key必须实现Comparable接口或者在构造TreeMap传入自定义的Comparator,否则会在运行时抛出java.lang.ClassCastException类型的异常。

属性
1 | // 比较器 |
构造
1 | public TreeMap() { |
方法
1 | V put(K key, V value) // 添加键值对 |
使用示例
1 | // 遍历treeMap |
put
1 | public V put(K key, V value) { |
rotateLeft
1 | // 左旋 |
rotateRight
1 | // 右旋 |
remove
1 | public V remove(Object key) { |
对equal的几点补充
集合中大量使用equals方法,实际上,该方法如果没有被重写,则就比较两个对象是否完全相等。
java对equals()的要求
1 | 1. 对称性:如果x.equals(y)返回是"true",那么y.equals(x)也应该返回是"true"。 |
equals() 与 == 的区别是什么?
没有被复写的时候等价
hashCode() 的作用
本质作用是确定该对象在哈希表中的索引位置
在散列表中可以判断对象的大小关系
hashCode() 和 equals() 的关系
在会创建“类对应的散列表”的情况下
1)、如果两个对象相等,那么它们的hashCode()值一定相同。
这里的相等是指,通过equals()比较两个对象时返回true。
2)、如果两个对象hashCode()相等,它们并不一定相等。
因为在散列表中,hashCode()相等,即两个键值对的哈希值相等。然而哈希值相等,并不一定能得出键值对相等。补充说一句:“两个不同的键值对,哈希值相等”,这就是哈希冲突。
对conparable和comparator的几点补充
Java 中 Comparable 和 Comparator 比较
Comparable
Comparable 是排序接口。
若一个类实现了Comparable
接口,就意味着“该类支持排序”。 即然实现Comparable
接口的类支持排序,假设现在存在“实现Comparable接口的类的对象的List列表(或数组)”,则该List列表(或数组)可以通过 Collections.sort
(或 Arrays.sort
)进行排序。
1 | package java.lang; |
说明:
假设我们通过 x.compareTo(y) 来“比较x和y的大小”。若返回“负数”,意味着“x比y小”;返回“零”,意味着“x等于y”;返回“正数”,意味着“x大于y”。
Comparator
Comparator 是比较器接口。
我们若需要控制某个类的次序,而该类本身不支持排序(即没有实现Comparable
接口);那么,我们可以建立一个“该类的比较器”来进行排序。这个“比较器”只需要实现Comparator
接口即可。
也就是说,我们可以通过“实现Comparator类来新建一个比较器”,然后通过该比较器对类进行排序。
1 | package java.util; |
说明:
若一个类要实现
Comparator
接口:它一定要实现compareTo(T o1, T o2)
函数,但可以不实现equals(Object obj)
函数。为什么可以不实现
equals(Object obj)
函数呢? 因为任何类,默认都是已经实现了equals(Object obj)
的。 Java中的一切类都是继承于java.lang.Object
,在Object.java
中实现了equals(Object obj)
函数;所以,其它所有的类也相当于都实现了该函数。int compare(T o1, T o2)
是“比较o1和o2的大小”。返回“负数”,意味着“o1比o2小”;返回“零”,意味着“o1等于o2”;返回“正数”,意味着“o1大于o2”。
比较
Comparable
是排序接口;若一个类实现了Comparable
接口,就意味着“该类支持排序”。
而Comparator
是比较器;我们若需要控制某个类的次序,可以建立一个“该类的比较器”来进行排序。
我们不难发现:Comparable
相当于“内部比较器”,而Comparator
相当于“外部比较器”。
1 | 一个类本身实现了Comparable比较器,就意味着它本身支持排序;若它本身没实现Comparable,也可以通过外部比较器Comparator进行排序。 |
ConcurrentHashMap(jdk1.7)
属性
1 | final Segment<K,V>[] segments; |
Segment
继承了ReentrantLock
,所以它就是一种可重入锁(ReentrantLock
)。在ConcurrentHashMap
,一个Segment
就是一个子哈希表,Segment
里维护了一个HashEntry
数组,并发环境下,对于不同Segment的数据进行操作是不用考虑锁竞争的。(就按默认的ConcurrentLevel
为16来讲,理论上就允许16个线程并发执行)
构造
segment
构造方法
1 | Segment(float lf, int threshold, HashEntry<K,V>[] tab) { |
ConcurrentHashMap
构造方法
1 | //初始化方法有三个参数,如果用户不指定则会使用默认值,initialCapacity为16,loadFactor为0.75(负载因子,扩容时需要参考),concurrentLevel为16。 |
初始化完成,我们得到了一个 Segment 数组。我们就当是用 new ConcurrentHashMap() 无参构造函数进行初始化的,那么初始化完成后:
Segment
数组长度为 16,不可以扩容.Segment数组的大小ssize是由concurrentLevel来决定的,但是却不一定等于concurrentLevel,ssize一定是大于或等于concurrentLevel的最小的2的次幂- Segment[i] 的默认大小为 2,负载因子是 0.75,得出初始阈值为 1.5,也就是以后插入第一个元素不会触发扩容,插入第二个会进行第一次扩容
- 这里初始化了 segment[0],其他位置还是 null,至于为什么要初始化 segment[0],后面的代码会介绍
- 当前
segmentShift
的值为 32 - 4 = 28,segmentMask
为 16 - 1 = 15,姑且把它们简单翻译为移位数和掩码,这两个值马上就会用到
方法
put
1 | public V put(K key, V value) { |
从源码看出,put的主要逻辑也就两步:
1. 定位segment并确保定位的Segment已初始化
2. 调用Segment的put方法
关于segmentShift
和segmentMask
·
segmentShift
和segmentMask
这两个全局变量的主要作用是用来定位Segment
,int j =(hash >>> segmentShift) & segmentMask
。
segmentMask:段掩码,假如segments
数组长度为16,则段掩码为16-1=15;segments
长度为32,段掩码为32-1=31。这样得到的所有bit位都为1,可以更好地保证散列的均匀性
segmentShift:2的sshift
次方等于ssize
,segmentShift
=32-sshift。若segments
长度为16,segmentShift
=32-4=28;若segments
长度为32,segmentShift
=32-5=27。而计算得出的hash值最大为32位,无符号右移segmentShift,则意味着只保留高几位(其余位是没用的),然后与段掩码segmentMask位运算来定位Segment。
*concurrentHashMap代理到Segment上的put方法,Segment中的put方法是要加锁的。只不过是锁粒度细了而已: *
1 | final V put(K key, int hash, V value, boolean onlyIfAbsent) { |
初始化槽: ensureSegment
ConcurrentHashMap 初始化的时候会初始化第一个槽 segment[0],对于其他槽来说,在插入第一个值的时候进行初始化。这里需要考虑并发,因为很可能会有多个线程同时进来初始化同一个槽 segment[k],不过只要有一个成功了就可以。
1 | private Segment<K,V> ensureSegment(int k) { |
总的来说,ensureSegment(int k)
比较简单,对于并发操作使用 CAS 进行控制。如果当前线程 CAS 失败,这里的 while 循环是为了将 seg 赋值返回。
获取写入锁: scanAndLockForPut
往某个 segment 中 put 的时候,首先会调用 node = tryLock() ? null : scanAndLockForPut(key, hash, value)
,也就是说先进行一次 tryLock() 快速获取该 segment 的独占锁,如果失败,那么进入到 scanAndLockForPut 这个方法来获取锁。
1 | private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) { |
这个方法有两个出口,一个是 tryLock() 成功了,循环终止,另一个就是重试次数超过了 MAX_SCAN_RETRIES,进到 lock() 方法,此方法会阻塞等待,直到成功拿到独占锁。
这个方法就是看似复杂,但是其实就是做了一件事,那就是获取该segment的独占锁,如果需要的话顺便实例化了一下 node。
rehash
segment 数组不能扩容,扩容是 segment 数组某个位置内部的数组 HashEntry[] 进行扩容,扩容后,容量为原来的 2 倍。
首先,我们要回顾一下触发扩容的地方,put 的时候,如果判断该值的插入会导致该 segment 的元素个数超过阈值,那么先进行扩容,再插值
该方法不需要考虑并发,因为到这里的时候,是持有该 segment 的独占锁的。
1 | // 方法参数上的 node 是这次扩容后,需要添加到新的数组中的数据。 |
这里的扩容比之前的 HashMap 要复杂一些,代码难懂一点。上面有两个挨着的 for 循环,第一个 for 有什么用呢?如果没有第一个 for 循环,也是可以工作的,但是,这个 for 循环下来,如果 lastRun 的后面还有比较多的节点,那么这次就是值得的。因为我们只需要克隆 lastRun 前面的节点,后面的一串节点跟着 lastRun 走就是了,不需要做任何操作。我觉得 Doug Lea 的这个想法也是挺有意思的,不过比较坏的情况就是每次 lastRun 都是链表的最后一个元素或者很靠后的元素,那么这次遍历就有点浪费了。不过 Doug Lea 也说了,根据统计,如果使用默认的阈值,大约只有 1/6 的节点需要克隆。
get
1 | public V get(Object key) { |
get方法无需加锁,由于其中涉及到的共享变量都使用volatile修饰,volatile可以保证内存可见性,所以不会读取到过期数据。
- 计算 hash 值,找到 segment 数组中的具体位置,或我们前面用的“槽”
- 槽中也是一个数组,根据 hash 找到数组中具体的位置
- 到这里是链表了,顺着链表进行查找即可
并发分析
put 操作的线程安全性
- 初始化槽,这个我们之前就说过了,使用了 CAS 来初始化 Segment 中的数组。
- 添加节点到链表的操作是插入到表头的,所以,如果这个时候 get 操作在链表遍历的过程已经到了中间,是不会影响的。当然,另一个并发问题就是 get 操作在 put 之后,需要保证刚刚插入表头的节点被读取,这个依赖于 setEntryAt 方法中使用的 UNSAFE.putOrderedObject。
- 扩容。扩容是新创建了数组,然后进行迁移数据,最后面将 newTable 设置给属性 table。所以,如果 get 操作此时也在进行,那么也没关系,如果 get 先行,那么就是在旧的 table 上做查询操作;而 put 先行,那么 put 操作的可见性保证就是 table 使用了 volatile 关键字。
remove 操作的线程安全性
get 操作需要遍历链表,但是 remove 操作会”破坏”链表。如果 remove 破坏的节点 get 操作已经过去了,那么这里不存在任何问题。如果 remove 先破坏了一个节点,分两种情况考虑。 1、如果此节点是头结点,那么需要将头结点的 next 设置为数组该位置的元素,table 虽然使用了 volatile 修饰,但是 volatile 并不能提供数组内部操作的可见性保证,所以源码中使用了 UNSAFE 来操作数组,请看方法 setEntryAt。2、如果要删除的节点不是头结点,它会将要删除节点的后继节点接到前驱节点中,这里的并发保证就是 next 属性是 volatile 的。
ConcurrentHashMap(1.8)
构造
1 | // 这构造函数里,什么都不干 |
方法
put
1 | public V put(K key, V value) { |
初始化数组 initTable()
1 | private final Node<K,V>[] initTable() { |
链表转红黑树: treeifyBin
前面我们在 put 源码分析也说过,treeifyBin 不一定就会进行红黑树转换,也可能是仅仅做数组扩容。我们还是进行源码分析吧。
1 | private final void treeifyBin(Node<K,V>[] tab, int index) { |
扩容:tryPresize
如果说 Java8 ConcurrentHashMap 的源码不简单,那么说的就是扩容操作和迁移操作。这个方法要完完全全看懂还需要看之后的 transfer 方法,读者应该提前知道这点。这里的扩容也是做翻倍扩容的,扩容后数组容量为原来的 2 倍。
1 | // 首先要说明的是,方法参数 size 传进来的时候就已经翻了倍了 |
个方法的核心在于 sizeCtl 值的操作,首先将其设置为一个负数,然后执行 transfer(tab, null),再下一个循环将 sizeCtl 加 1,并执行 transfer(tab, nt),之后可能是继续 sizeCtl 加 1,并执行 transfer(tab, nt)。所以,可能的操作就是执行 1 次 transfer(tab, null) + 多次 transfer(tab, nt),这里怎么结束循环的需要看完 transfer 源码才清楚