文章目录
- 一、Java8-HashMap源码【2倍扩容】
- 1.1 数据结构【数组+链表+红黑树,当put导致桶的数组长度>=64并且链表元素>8 个(插入的是第九个),会将链表转换为红黑树,降低查找的时间复杂度为 O(logN)。当红黑树节点数量<=6时,会转变为链表结构】【Java7: Entry,Java8 使用 Node来代表每个 HashMap 中的数据节点,,基本没有区别,都是 key,value,hash 和 next 这四个属性,不过,Node 只能用于链表的情况,红黑树的情况需要使用 TreeNode。我们根据数组元素中,第一个节点数据类型是 Node 还是 TreeNode 来判断该位置下是链表还是红黑树的】
- 1、 hashmap的成员属性分析
- 2、HashMap 构造器源码分析:HashMap 是“懒加载”,在构造器中值保留了相关保留的值,并没有初始化 table 数组,当我们向 map 中 put 第一个元素的时候,map 才会进行初始化!
- 3、HashMap 内部类——Node 源码分析
- 4、HashMap 中的 hash 函数:HashMap 允许 key 为null,null 的 hash 为 0。非 null 的 key 的 hash 高 16 位和低 16 位分别由:**key 的 hashCode 高 16 位** 和 **hashCode 的高 16 位异或 hashCode 的低16位**组成。主要是为了增强 hash 的随机性,减少 hash & (n - 1) 的随机性,即减小 hash 冲突,提高 HashMap 的性能。
- 5、tableSizeFor 函数源码分析
- 1.2 put【Java7先扩容再插入到链表最前面,Java8先插入再扩容,插入到链表最后面】【key和value可以为null】
- 1.3 HashMap 的 resize 函数源码分析 【重点中的重点】
- 1.4 get
- 二、Java8-ConCurrentHashMap
- 2.1 数据结构【结构上和 Java8 的 HashMap 基本上一样,不过它要保证线程安全性,所以在源码上确实要复杂一些。也引入了红黑树】
- 2.2 初始化【如果你有提供长度initialCapacity,那么数组长度为sizeCtl = 【 (1.5 * initialCapacity + 1),然后向上取最近的 2 的 n 次方】,但一般我们使用的默认构造器,不指定长度,默认数组长度是16】
- 2.3 put【key和value都不能为null】
- 1)如果数组"空",进行数组初始化
- 2)找该 hash 值对应的数组下标【i = (n - 1) & hash)】,得到第一个节点 f
- 3)if: 数组该位置f为空,用一次 CAS 操作将这个新值放入其中即可,如果 CAS 失败,那就是有并发操作,进到下一个循环就好了(1,2,3,4,5步骤都在for循环里,直到我们某一步成功放入,才break)
- 4)else if: 容量不够,扩容,扩容后的数组容量为原来的两倍
- 5)else: 数组该位置f不为空,获取数组该位置的头结点的监视器锁synchronized (f)【对数组的每个位置操作时都会这样加锁头结点】 ,如果是treeNode,则用红黑树的方法插入,如果是链表,判断是否有重复,重复覆盖,不重复则加到链表末尾,如果链表长度大于8,与HashMap不同的是它不一定会转换为红黑树,比如当前数组长度如果小于64,那会选择进行数组扩容,而不是转换为红黑树
- 2.5 初始化数组:initTable
- 2.6 get
一、Java8-HashMap源码【2倍扩容】
1.1 数据结构【数组+链表+红黑树,当put导致桶的数组长度>=64并且链表元素>8 个(插入的是第九个),会将链表转换为红黑树,降低查找的时间复杂度为 O(logN)。当红黑树节点数量<=6时,会转变为链表结构】【Java7: Entry,Java8 使用 Node来代表每个 HashMap 中的数据节点,,基本没有区别,都是 key,value,hash 和 next 这四个属性,不过,Node 只能用于链表的情况,红黑树的情况需要使用 TreeNode。我们根据数组元素中,第一个节点数据类型是 Node 还是 TreeNode 来判断该位置下是链表还是红黑树的】
1、 hashmap的成员属性分析
- DEFAULT_LOAD_FACTOR很大时,桶的利用率较大的时候(注意可以大于1,因为冲突的元素是使用链表或者红黑树连接起来的),此时空间利用率较高,这也意味着一个桶中存储了很多元素,这时 HashMap 的 put、get 等操作代价就相对较大,因为每一个 put 或 get 操作都变成了对链表或者红黑树的操作,代价肯定大于O(1),所以说 DEFAULT_LOAD_FACTOR 是空间和时间的一个平衡点。
- DEFAULT_LOAD_FACTOR较小时,需要的空间较大,但是 put 和 get 的代价较小;
- DEFAULT_LOAD_FACTOR较大时,需要的空间较小,但是 put 和 get 的代价较大)
- threshold 属性分析
-
threshold 也是比较重要的一个属性:创建 HashMap 时,该变量的值是:初始容量(2 的整数次幂),之后 threshold的值是 HashMap 扩容的门限值,即当前 Nodetable 数组的长度 * loadfactor。
- 举个例子而言,如果我们传给 HashMap 构造器的容量大小为 9,那么 threshold 初始值为16,在向 HashMap 中 put 第一个元素后,内部会创建长度为 16 的 Node 数组,并且 threshold 的值更新为 16 * 0.75=12。具体而言,当我们一直往HashMap 中 put 元素的时候,如果put某个元素后,Node数组中元素个数为 13,此时会触发扩容(因为数组中元素个数> threshold了,即13 > threshold = 12),具体扩容操作之后会详细分析,简单理解就是,扩容操作将 Node 数组长度 * 2;并且将原来的所有元素迁移到新的 Node 数组中。
-
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
private static final long serialVersionUID = 362498820763181265L;
// HashMap的初始容量为16,HashMap的容量指的是存储元素的数组大小,即桶的数量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
// HashMap的最大的容量
static final int MAXIMUM_CAPACITY = 1 << 30;
// 下面有详细解析
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 当某一个桶中链表的长度>=8时,链表结构会转换成红黑树结构,
// 其实还要求桶的中元素数量>=64,后面会提到
static final int TREEIFY_THRESHOLD = 8;
// 当红黑树中的节点数量<=6时,红黑树结构会转变为链表结构
static final int UNTREEIFY_THRESHOLD = 6;
// 上面提到的:当Node数组容量>=64的前提下,如果某一个桶中链表长度>=8,
// 则会将链表结构转换成红黑树结构
static final int MIN_TREEIFY_CAPACITY = 64;
// 我们往map中put的(k,v)都被封装在Node中,所有的Node都存放在table数组中
transient Node<K,V>[] table;
// 用于返回keySet和values
transient Set<Map.Entry<K,V>> entrySet;
// 保存map当前有多少个元素
transient int size;
// failFast机制,在讲解ArrayList和LinkedList一文中已经讲过了
transient int modCount;
int threshold; // 下面有详细讲解
// 负载因子,见上面对DEFAULT_LOAD_FACTOR参数的讲解,默认值是0.75
final float loadFactor;
}
2、HashMap 构造器源码分析:HashMap 是“懒加载”,在构造器中值保留了相关保留的值,并没有初始化 table 数组,当我们向 map 中 put 第一个元素的时候,map 才会进行初始化!
// 构造器:指定map的大小,和loadfactor
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException
("Illegal initial capacity: " + initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
// 保存loadfactor
this.loadFactor = loadFactor;
/* 注意,前面有讲tableSizeFor函数,该函数返回值:>= initialCapacity、
返回值是2的整数次幂,并且得是满足上面两个条件的所有数值中最小的那个数 */
this.threshold = tableSizeFor(initialCapacity);
}
// 只指定HashMap容量的构造器,loadfactor使用的是默认的值:0.75
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
// 无参构造器,默认loadfactor:0.75,默认的容量是16
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
// 其他不常用的构造器就不分析了
3、HashMap 内部类——Node 源码分析
// Node是HashMap的内部类
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key; // 保存map中的key
V value; // 保存map中的value
Node<K,V> next; // 单向链表
// 构造器
Node(int hash, K key, V value ,Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
}
4、HashMap 中的 hash 函数:HashMap 允许 key 为null,null 的 hash 为 0。非 null 的 key 的 hash 高 16 位和低 16 位分别由:key 的 hashCode 高 16 位 和 hashCode 的高 16 位异或 hashCode 的低16位组成。主要是为了增强 hash 的随机性,减少 hash & (n - 1) 的随机性,即减小 hash 冲突,提高 HashMap 的性能。
- (因为如果直接使用 hashCode & (n - 1) 来计算 index,此时 hashCode 的高位随机特性完全没有用到,因为 n 相对于hashCode 的值很小,计算 index 的时候只能用到低 16 位。基于这一点,把 hashCode 高 16 位的值通过异或混合到hashCode 的低 16 位,由此来增强 hashCode 低 16 位的随机性。)
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
5、tableSizeFor 函数源码分析
static final int tableSizeFor(int cap) {
// 举例而言:n的第三位是1(从高位开始数),
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
在 new HashMap 的时候,如果我们传入了大小参数,这是 HashMap 会对我们传入的 HashMap 容量进行传到tableSizeFor 函数处理。这个函数主要功能是:返回一个数。这个数是大于等于 cap 并且是 2 的整数次幂的所有数中最小的那个,即返回一个最接近 cap(>= cap),并且是 2 的整数次幂的数。
具体逻辑如下:一个数是 2 的整数次幂,那么这个数减 1 的二进制就是一串掩码,即二进制从某位开始是一 串连续的1。所以只要对相应的掩码 +1 一定是 2 的整数次幂,这也是为什么 n = cap - 1 的原因。
举例而言,假设:n=00010000_00000000_00000000
n = 00010000_00000000_00000000
n |= n >>> 1; //执行完后
// n = 00011000_00000000_00000000
n |= n >>> 2; // 执行完后
// n = 00011110_00000000_00000000
n |= n >>> 4; // 执行完后
// n = 00011111_11100000_00000000
n |= n >>> 8; // 执行完后
// n = 00011111_11111111_11100000
n |= n >>> 16; // 执行完后
// n = 00011111_11111111_11111111
返回 n + 1,(n + 1) >= cap、为2的整数次幂,并且是与 cap 差值最小的那个数。最后的 n+1 一定是 2 的整数次幂,并且一定是 >= cap。
整体的思路就是:如果 n 的二进制的第 k 为1,那么经过上面四个 ‘|’ 运算后 [0 ,k] 位都变成了1,即:一连串连续的二进制‘1’(掩码),最后 n+1 一定是 2 的整数次幂(如果不溢出)。
1.2 put【Java7先扩容再插入到链表最前面,Java8先插入再扩容,插入到链表最后面】【key和value可以为null】
1)第一次 put 值的时候,resize()初始化数组长度从 null 初始化到默认的 16 或自定义的初始容量,默认阈值 0.75
2)找到具体的数组下标(n - 1) & hash,如果此位置没有值,那么直接初始化一下 Node 并放置在这个位置就可以了
3)如果数组该位置有数据,判断是红黑树还是链表【插到链表最后面,重复就覆盖旧值,如果插入后个数是9个,则将链表转为红黑树】
4)如果 HashMap 由于新插入这个值导致 size 已经超过了阈值,需要进行扩容,每次扩容数组和阈值都为原来的 2 倍
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
// 第三个参数 onlyIfAbsent 如果是 true,那么只有在不存在该 key 时才会进行 put 操作
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 1、第一次 put 值的时候,resize()初始化数组长度从 null 初始化到默认的 16 或自定义的初始容量
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 2、找到具体的数组下标(n - 1) & hash,如果此位置没有值,那么直接初始化一下 Node 并放置在这个位置就可以了
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {// 3、数组该位置有数据,判断是红黑树还是链表【插到链表最后面,如果是第9个,链表转为红黑树】
Node<K,V> e; K k;
// 首先,判断该位置的第一个数据和我们要插入的数据,key 是不是"相等",如果是,取出这个节点
/* 为什么get和put先判断p.hash==hash,下面的if条件中去掉hash的比较逻辑也是正确?
因为hash的比较是两个整数的比较,比较的代价相对较小,
key是泛型,对象的比较比整数比较代价大,所以先比较hash,hash相等再比较key
*/
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 如果该节点是代表红黑树的节点,调用红黑树的插值方法
//如果已经存在该key的TreeNode,则返回该TreeNode,否则返回null
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 到这里,说明数组该位置上是一个链表
for (int binCount = 0; ; ++binCount) {
/* 遍历到了链表最后一个元素,接下来执行链表的插入操作,先封装为Node,
再插入p指向的是链表最后一个节点,将待插入的Node置为p.next,就完成了单链表的插入 */
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// TREEIFY_THRESHOLD 为 8,所以,如果新插入的值是链表中的第 9 个
// 会触发下面的 treeifyBin,也就是将链表转换为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 如果在该链表中找到了"相等"的 key(== 或 equals)
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
// 此时 break,那么 e 为链表中[与要插入的新值的 key "相等"]的 node
break;
p = e;
}
}
// e!=null 说明存在旧值的key与要插入的key"相等"
// 对于我们分析的put操作,下面这个 if 其实就是进行 "值覆盖",然后返回旧值
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
/* 这个函数的默认实现是“空”,即这个函数默认什么操作都不执行,那为什么要有它呢?
这其实是个hook/钩子函数,主要要在LinkedHashMap(HashMap子类)中使用,
LinkedHashMap重写了这个函数。以后会有讲解LinkedHashMap的文章。*/
afterNodeAccess(e);
return oldValue;
}
}
// 如果是第一次插入key这个键,就会执行到这里
++modCount;
// 4、如果 HashMap 由于新插入这个值导致 size 已经超过了阈值,需要进行扩容
if (++size > threshold)
resize();
// 这也是一个hook函数,作用和afterNodeAccess一样
afterNodeInsertion(evict);
return null;
}
1.3 HashMap 的 resize 函数源码分析 【重点中的重点】
面试谈到 HashMap 必考 resize 相关知识,整体思路介绍:
有两种情况会调用当前函数:
####### 1、之前说过 HashMap 是懒加载,第一次调用 HashMap 的 put 方法的时候 table 还没初始化,这个时候会执行 resize,进行table 数组的初始化,table 数组的初始容量保存在 threshold 中(如果从构造器中传入的一个初始容量的话),如果创建HashMap 的时候没有指定容量,那么 table 数组的初始容量是默认值:16。即,初始化 table 数组的时候会执行 resize 函数。
####### 2、扩容的时候会执行 resize 函数,插入元素后,当 size 的值 > threshold 的时候会触发扩容,即执行 resize 方法,这时 table 数组的大小会翻倍。
- 注意我们每次扩容之后容量都是翻倍( * 2),所以HashMap的容量一定是2的整数次幂。
HashMap的容量为什么一定得是 2 的整数次幂呢?【面试重点】。
我们 put key 的时候,每一个 key 会对应到一个桶里面,桶的索引是这样计算的: index = hash & (n-1),index 的计算最为直观的想法是:hash % n,即通过取余的方式把当前的 key、value 键值对散列到各个桶中;那么这里为什么不用取余(%)的方式呢?
- 原因是:
####### 1、* CPU对位运算支持较好,即位运算速度很快。另外,当 n 是 2 的整数次幂时:hash & (n - 1) 与 hash % n 是等价的,但是两者效率来讲是不同的,位运算的效率远高于取余 % 运算。****所以,HashMap中使用的是 hash & (n - 1)。
####### 2、这还带来了一个好处,就是将旧数组中的 Node 迁移到扩容后的新数组中的时候有一个很方便的特性:【索引为 i 的节点,rehash后的索引只可能是 i 或者 i+oldcap,也就是我们可以这样处理:把 table[i] 这个桶中的 node 拆分为两个链表 l1 和 l2:如果hash & n == 0,那么当前这个 node 被连接到 l1 链表;否则连接到 l2 链表。这样下来,当遍历完 table[i] 处的所有 node 的时候,我们得到两个链表 l1 和 l2,这时我们令 newtab[i] = l1,newtab[i + n] = l2,这就完成了 table[i] 位置所有 node 的迁移(rehash),这也是 HashMap 中容量一定的是2的整数次幂带来的方便之处。 (因为Java8 是尾插,如果你一个一个的来放置的话,那么每个位置你都要遍历到该位置链表的尾部才能插入,耗时长【自己理解的】)】 - 把旧数组数据移动到新数组,我们在扩充HashMap的时候,因为n变为 2 倍,n-1的mask范围在高位多1bit,所以只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”,(看(e.hash & oldCap) == 0还是不等于 0 )
- 元素在重新计算hash之后,因为n变为2倍,那么n-1的mask范围在高位多1bit(红色),因此新的index就会发生这样的变化:
final Node<K,V>[] resize() {
// 保留扩容前数组引用
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 正常扩容:newCap = oldCap << 1
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
// 容量翻倍,扩容后的threshold自然也是*2
newThr = oldThr << 1;
}
else if (oldThr > 0)
newCap = oldThr;
else {
// table数组初始化的时候会进入到这里
// 默认容量newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap*loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE);
}
threshold = newThr; // 更新threshold
@SuppressWarnings({"rawtypes", "unchecked"})
// 扩容后的新数组
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab; // 执行容量翻倍的新数组
if (oldTab != null) {
// 之后完成oldTab中Node迁移到table中去
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
// j这个桶位置只有一个元素,直接rehash到table数组
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
// 如果是红黑树:也是将红黑树拆分为两个链表,这里主要看链表的拆分,两者逻辑一样
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else {
// 链表的拆分第一个链表l1
Node<K,V> loHead = null, loTail = null;
// 第二个链表l2
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap)== 0) {
// rehash到table[j]位置将当前node连接到l1上
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
// 将当前node连接到l2上
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
// l1放到table[j]位置
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
// l1放到table[j+oldCap]位置
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
1.4 get
-
get 函数实质就是进行链表或者红黑树遍历搜索指定的 key 的节点的过程。
- 另外需要注意到 HashMap 的 get 函数的返回值不能判断一个 key 是否包含在 map 中,get 返回 null 有可能是不包含该 key;也有可能该 key 对应的 value 为 null。因为 HashMap 中允许 key 为 null,也允许 value 为 null。
1) 第一个节点就匹配到了,直接返回,否则进行搜索
2) 判断是否是红黑树,是,就找红黑树
3)说明是链表,遍历链表来找
- getNode的主要逻辑:如果 table[index] 处节点的 key 就是要找的 key 则直接返回该节点; 否则:如果在 table[index] 位置进行搜索,搜索是否存在目标 key 的 Node:这里的搜索又分两种:链表搜索和红黑树搜索,具体红黑树的查找就不展开了,红黑树是一种弱平衡(相对于AVL)BST,红黑树查找、删除、插入等操作都能够保证在O(logn)时间复杂度内完成,红黑树原理不在本文范围内,但是大家要知道红黑树的各种操作是可以实现的,简单点可以把红黑树理解为BST,BST的查找、插入、删除等操作的实现在之前的文章中有BST java实现讲解,红黑树实际上就是一种平衡的BST。
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 判断第一个节点是不是就是需要的
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
// 判断是否是红黑树
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
// 链表遍历
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null; // 没找到
}
####### 6、contains 函数源码分析
public boolean containsKey(Object key) {
// 注意与get函数区分,我们往map中put的所有的<key,value>都被封装在Node中,
// 如果Node都不存在显然一定不包含对应的key
return getNode(hash(key), key) != null;
}
二、Java8-ConCurrentHashMap
2.1 数据结构【结构上和 Java8 的 HashMap 基本上一样,不过它要保证线程安全性,所以在源码上确实要复杂一些。也引入了红黑树】
2.2 初始化【如果你有提供长度initialCapacity,那么数组长度为sizeCtl = 【 (1.5 * initialCapacity + 1),然后向上取最近的 2 的 n 次方】,但一般我们使用的默认构造器,不指定长度,默认数组长度是16】
// 这构造函数里,什么都不干
public ConcurrentHashMap() {
}
//通过提供初始容量,计算了 sizeCtl,sizeCtl = 【 (1.5 * initialCapacity + 1),然后向上取最近的 2 的 n 次方】。如 initialCapacity 为 10,那么得到 sizeCtl 为 16,如果 initialCapacity 为 11,得到 sizeCtl 为 32。
public ConcurrentHashMap(int initialCapacity) {
if (initialCapacity < 0)
throw new IllegalArgumentException();
int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
MAXIMUM_CAPACITY :
tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
this.sizeCtl = cap;
}
2.3 put【key和value都不能为null】
1)如果数组"空",进行数组初始化
2)找该 hash 值对应的数组下标【i = (n - 1) & hash)】,得到第一个节点 f
3)if: 数组该位置f为空,用一次 CAS 操作将这个新值放入其中即可,如果 CAS 失败,那就是有并发操作,进到下一个循环就好了(1,2,3,4,5步骤都在for循环里,直到我们某一步成功放入,才break)
4)else if: 容量不够,扩容,扩容后的数组容量为原来的两倍
5)else: 数组该位置f不为空,获取数组该位置的头结点的监视器锁synchronized (f)【对数组的每个位置操作时都会这样加锁头结点】 ,如果是treeNode,则用红黑树的方法插入,如果是链表,判断是否有重复,重复覆盖,不重复则加到链表末尾,如果链表长度大于8,与HashMap不同的是它不一定会转换为红黑树,比如当前数组长度如果小于64,那会选择进行数组扩容,而不是转换为红黑树
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();
// 得到 hash 值
int hash = spread(key.hashCode());
// 用于记录相应链表的长度
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
// 1、如果数组"空",进行数组初始化
if (tab == null || (n = tab.length) == 0)
tab = initTable();
// 2、找该 hash 值对应的数组下标【i = (n - 1) & hash)】,得到第一个节点 f
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 3、如果数组该位置f为空,用一次 CAS 操作将这个新值放入其中即可,如果 CAS 失败,那就是有并发操作,进到下一个循环就好了
if (casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null)))
break;
}
// 4、【容量不够,扩容】hash 居然可以等于 MOVED,这个需要到后面才能看明白,不过从名字上也能猜到,肯定是因为在扩容
else if ((fh = f.hash) == MOVED)
// 帮助数据迁移,这个等到看完数据迁移部分的介绍后,再理解这个就很简单了
tab = helpTransfer(tab, f);
else { // 5、数组该位置f不为空,获取数组该位置的头结点的监视器锁synchronized (f) ,如果treeNode,则用红黑树的方法插入,如果是链表,判断是否有重复,重复覆盖,不重复则加到链表末尾,如果链表长度大于8,与HashMap不同的是它不一定会转换为红黑树,比如当前数组长度如果小于64,那会选择进行数组扩容,而不是转换为红黑树
V oldVal = null;
// 获取数组该位置的头结点的监视器锁
synchronized (f) {
if (tabAt(tab, i) == f) {
// 头结点的 hash 值大于 0,说明是链表
if (fh >= 0) {
// 用于累加,记录链表的长度
binCount = 1;
// 遍历链表
for (Node<K,V> e = f;; ++binCount) {
K ek;
// 如果发现了"相等"的 key,判断是否要进行值覆盖,然后也就可以 break 了
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, null);
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;
}
}
}
}
// binCount != 0 说明上面在做链表操作
if (binCount != 0) {
// 判断是否要将链表转换为红黑树,临界值和 HashMap 一样,也是 8
if (binCount >= TREEIFY_THRESHOLD)
// 这个方法和 HashMap 中稍微有一点点不同,那就是它不是一定会进行红黑树转换,
// 如果当前数组的长度小于 64,那么会选择进行数组扩容,而不是转换为红黑树
// 具体源码我们就不看了,扩容部分后面说
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
//
addCount(1L, binCount);
return null;
}
2.5 初始化数组:initTable
1) sc = sizeCtl,然后CAS将 sizeCtl 设置为 -1,代表抢到了锁,如果本身就是-1了,说明初始化这件事被其他线程抢去了,我就只需Thread.yield();就好
2)如果由我抢到了锁,初始化数组,长度为 16(默认长度) 或初始化时根据你提供的长度计算出来的长度,负载因子为0.75,table 是 volatile 的
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
// 初始化的"功劳"被其他线程"抢去"了
if ((sc = sizeCtl) < 0)
Thread.yield(); // lost initialization race; just spin
// CAS 一下,将 sizeCtl 设置为 -1,代表抢到了锁
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if ((tab = table) == null || tab.length == 0) {
// DEFAULT_CAPACITY 默认初始容量是 16
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
// 初始化数组,长度为 16 或初始化时提供的长度
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
// 将这个数组赋值给 table,table 是 volatile 的
table = tab = nt;
// 如果 n 为 16 的话,那么这里 sc = 12
// 其实就是 0.75 * n
sc = n - (n >>> 2);
}
} finally {
// 设置 sizeCtl 为 sc,我们就当是 12 吧
sizeCtl = sc;
}
break;
}
}
return tab;
}
2.6 get
1)计算 hash 值
2)根据 hash 值找到数组对应位置: (n – 1) & h
3)根据该位置处结点性质进行相应查找【如果该位置为 null,那么直接返回 null ->如果该位置处的节点刚好就是我们需要的,返回该节点的值即可->如果该位置节点的 hash 值小于 0,说明正在扩容,或者是红黑树,后面我们再介绍 find 方法->如果以上 3 条都不满足,那就是链表,进行遍历比对即可】
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
int h = spread(key.hashCode());
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
// 判断头结点是否就是我们需要的节点
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
// 如果头结点的 hash 小于 0,说明 正在扩容,或者该位置是红黑树
else if (eh < 0)
// 参考 ForwardingNode.find(int h, Object k) 和 TreeBin.find(int h, Object k)
return (p = e.find(h, key)) != null ? p.val : null;
// 遍历链表
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}