ArrayList 源码分析
ArrayList 简介?
- ArrayList 底层是一个 Object[] 数组,它的容量可以动态增长;
- ArrayList 继承于 AbstractList,实现了 List、RandomAccess、Cloneable、Serializable 接口,这表明:
- List 接口表明 ArrayList 是一个列表,支持添加、删除、查找等操作,并且支持通过下标进行访问;
- RandomAccess 接口表明 ArrayList 支持快速随机访问,即我们可以通过元素的序号快速获取到元素的对象;
- Cloneable 接口表明,ArrayList 具有拷贝能力;
- Serializable 接口表明,它可以进行序列化和反序列化操作,即对象可以转为字节流进行持久化存储或网络传输;
ArrayList 参数介绍?
- 默认初始容量大小是 10;
- Object[] 数组类型空静态变量 EMPTY_ELEMENTDATA 共享给所有初始化参数指定为 0 的 ArrayList 对象;
- Object[] 数组类型空静态变量 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 共享给所有没有指定初始化容量的 ArrayList 对象;
- 两个 Object[] 数组类型空静态变量是为了区分这两种空 ArrayList 第一次添加数据时需要初始化数组长度的容量,对于没有指定初始容量的 ArrayList 下次扩容为默认初始容量 10;
- 一个非私有的
transient Object[] elementData数组用来保存数据,非私有是为了简化扩展类对 elementData 的访问;而 transient 是为了让 ArrayList 不序列化整个数组,只序列化具体的元素,当反序列化的时候,需要重新创建数组,这样效率更高;
构造函数简介?
一共有三个构造函数:
- 带初始容量参数的构造函数:
- 如果初始容量大于 0 ,创建指定初始化容量的数组;
- 如果初始化容量等于0 ,将 EMPTY_ELEMENTDATA 赋值给 elementData 数组;
- 其他情况抛出参数异常错误;
- 无参构造函数:
- 将 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 赋值给 elementData 数组;
- 指定一个集合参数的构造函数:
- 将指定的集合转换为数组并赋值给 elementData;
- 如果 elementData 数组的长度不为 0, 判断数组的类型,如果不是 Object 类型,使用 Arrays.copyOf() 将 elementData 数组类型转换为 Object;
- 如果 elementData 数组的长度为 0,将 EMPTY_ELEMENTDATA 赋值给 elementData;
trimToSize() 的作用?
将 ArrayList 的容量大小 resize 为实际大小,从而最小化 ArrayList 的实例的存储;
ArrayList 的 clone()?
ArrayList 的 clone() 是浅克隆,使用 Arrays.copyOf(elementData, size); 复制一个新的数组, 但是数组的元素是共用的;
扩容过程分析?
以无参构造创建了新的 ArrayList 作为起点进行分析。当无参构造 new ArrayList 后,会将 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 赋值给 elementData,当第一次向 ArrayList 添加元素触发扩容。
// 第一步:调用 add 添加元素
public boolean add(E e) {
// 加元素之前,先调用ensureCapacityInternal方法
ensureCapacityInternal(size + 1);
// 这里看到ArrayList添加元素的实质就相当于为数组赋值
elementData[size++] = e;
return true;
}
// 第二步:确保内部容量达到指定的最小容量
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
// 第三步:根据给定的最小容量和当前数组元素来重新计算所需容量
private static int calculateCapacity(Object[] elementData, int minCapacity) {
// 如果当前数组元素为空数组(初始情况),返回默认容量和最小容量中的较大值作为所需容量
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
// 否则直接返回最小容量
return minCapacity;
}
//第四步:根据所需容量判断是否需要扩容
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// 如果所需容量大于当前容量,则需要进行扩容
if (minCapacity - elementData.length > 0)
// 调用grow方法进行扩容
grow(minCapacity);
}
// 第五步:扩容
private void grow(int minCapacity) {
// oldCapacity为旧容量,newCapacity为新容量
int oldCapacity = elementData.length;
// 将oldCapacity 右移一位,其效果相当于oldCapacity /2,
// 我们知道位运算的速度远远快于整除运算,整句运算式的结果就是将新容量更新为旧容量的1.5倍,
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 然后检查新容量是否大于最小需要容量,若还是小于最小需要容量,那么就把最小需要容量当作数组的新容量,
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 如果新容量大于 MAX_ARRAY_SIZE,进入(执行) `hugeCapacity()` 方法来比较 minCapacity 和 MAX_ARRAY_SIZE,
// 如果minCapacity大于最大容量,则新容量则为`Integer.MAX_VALUE`,否则,新容量大小则为 MAX_ARRAY_SIZE 即为 `Integer.MAX_VALUE - 8`。
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
// 处理新的容量大于最大容量的情况
private static int hugeCapacity(int minCapacity) {
// 当 minCapacity < 0 表示发生了溢出,直接报错
if (minCapacity < 0)
throw new OutOfMemoryError();
// 对 minCapacity 和 MAX_ARRAY_SIZE 进行比较:
// 若 minCapacity 大,将 Integer.MAX_VALUE 作为新数组的大小
// 若 MAX_ARRAY_SIZE 大,将 MAX_ARRAY_SIZE 作为新数组的大小
// MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
对于无参构造函数构造的 ArrayList 扩容过程?
- 创建新的 ArrayList 对象,并将 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 赋值给 elementData;
- 调用 add 方法添加元素,此时发现 elementData 是 DEFAULTCAPACITY_EMPTY_ELEMENTDATA,将 elementData 扩容到默认的 10;
- 依次 add 元素,直到第 11 个元素,此时进行扩容为 15,(old + (old >> 1)),然后继续添加;
对于非无参构造函数构造的 ArrayList 库容过程?
每次都会进行扩容判断,根据实际情况进行扩容;
ArrayList 的 ensureCapacity 方法?
这个方法是提供给外部调用的,其目的是在需要大量向 ArrayList 添加元素时,可以调用该函数,让个 ArrayList 提前库容完成,避免添加元素期间多次扩容;
LinkedList 源码分析
LinkedList 简介?
LinkedList 继承了 AbstractSequentialList,而 AbstractSequentialList 又继承自 AbstractList; LinkedList 实现的接口:
- List: 表明它是一个列表,支持添加、删除、查找等操作,并且可以通过下标进行访问。
- Deque: 继承自 Queue 接口,具有双端队列的特性,支持从两端插入和删除元素,方便实现栈和队列等数据结构。
- Cloneable: 表明它具有拷贝能力,可以进行深拷贝或浅拷贝操作。
- Serializable: 表明它可以进行序列化操作,也就是可以将对象转换为字节流进行持久化存储或网络传输。
LinkedList 的 Node 定义?
private static class Node<E> {
E item;// 节点值
Node<E> next; // 后继节点
Node<E> prev; // 前驱结点
// 初始化参数顺序分别是:前驱结点、本身节点值、后继节点
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
LinkedList 的构造函数?
// 无参构造函数, 构造空链表
public LinkedList() {
}
// 接收一个集合类型作为参数,会创建一个与传入集合相同元素的链表对象
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
LinkedList 插入元素?
- add(E e) 插入链表尾部;
- add(int index, E element) 在链表指定位置插入;
- 判断 index 是否为链表尾,如果是采用 linkLast 插入;
- 如果不是,采用 linkLast 插入到 index 指定元素之前;
插入尾部会直接调用 linkLast(E e) 方法:
// 将元素节点插入到链表尾部
void linkLast(E e) {
// 将最后一个元素赋值(引用传递)给节点 l
final Node<E> l = last;
// 创建节点,并指定节点前驱为链表尾节点 last,后继引用为空
final Node<E> newNode = new Node<>(l, e, null);
// 将 last 引用指向新节点
last = newNode;
// 判断尾节点是否为空
// 如果 l 是null 意味着这是第一次添加元素
if (l == null)
// 如果是第一次添加,将first赋值为新节点,此时链表只有一个元素
first = newNode;
else
// 如果不是第一次添加,将新节点赋值给l(添加前的最后一个元素)的next
l.next = newNode;
size++;
modCount++;
}
// 在指定元素之前插入元素
void linkBefore(E e, Node<E> succ) {
// assert succ != null;断言 succ不为 null
// 定义一个节点元素保存 succ 的 prev 引用,也就是它的前一节点信息
final Node<E> pred = succ.prev;
// 初始化节点,并指明前驱和后继节点
final Node<E> newNode = new Node<>(pred, e, succ);
// 将 succ 节点前驱引用 prev 指向新节点
succ.prev = newNode;
// 判断尾节点是否为空,为空表示当前链表还没有节点
if (pred == null)
first = newNode;
else
// succ 节点前驱的后继引用指向新节点
pred.next = newNode;
size++;
modCount++;
}
LinkedList 获取元素?
- getFirst():获取链表的第一个元素。
- getLast():获取链表的最后一个元素。
- get(int index):获取链表指定位置的元素。
对于获取指定位置的的元素核心方法:
// 返回指定下标的非空节点
Node<E> node(int index) {
// 如果index小于size的二分之一 从前开始查找(向后查找) 反之向前查找
if (index < (size >> 1)) {
Node<E> x = first;
// 遍历,循环向后查找,直至 i == index
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
HashMap 源码分析
HashMap 简介?
- HashMap 主要用来存放键值对,它基于哈希表的 Map 接口实现,是常用的 Java 集合之一,是非线程安全的;
- HashMap 可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个;
- JDK1.8 之前 HashMap 由 数组+链表 组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。 JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于等于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。
- HashMap 默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。并且, HashMap 总是使用 2 的幂作为哈希表的大小。
HashMap 的属性介绍?
// 默认的初始容量是16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
// 最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认的负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 当桶上的结点数大于等于这个值时会转成红黑树
static final int TREEIFY_THRESHOLD = 8;
// 当桶上的结点数小于等于这个值时树转链表
static final int UNTREEIFY_THRESHOLD = 6;
// 桶中结构转化为红黑树对应的table的最小容量
static final int MIN_TREEIFY_CAPACITY = 64;
// 存储元素的数组,总是2的幂次倍
transient Node<k,v>[] table;
// 存放具体元素的集
transient Set<map.entry<k,v>> entrySet;
// 存放元素的个数,注意这个不等于数组的长度。
transient int size;
// 每次扩容和更改map结构的计数器
transient int modCount;
// 阈值(容量*负载因子) 当实际大小超过阈值时,会进行扩容
// threshold = capacity * loadFactor,当 Size>threshold的时候,那么就要考虑对数组的扩增了,也就是说,这个的意思就是 衡量数组是否需要扩增的一个标准。
int threshold;
// 负载因子
// loadFactor 负载因子是控制数组存放数据的疏密程度,loadFactor 越趋近于 1,那么 数组中存放的数据(entry)也就越多,也就越密,也就是会让链表的长度增加,loadFactor 越小,也就是趋近于 0,数组中存放的数据(entry)也就越少,也就越稀疏。
//loadFactor 太大导致查找元素效率低,太小导致数组的利用率低,存放的数据会很分散。loadFactor 的默认值为 0.75f 是官方给出的一个比较好的临界值。
// 给定的默认容量为 16,负载因子为 0.75。Map 在使用过程中不断的往里面存放数据,当数量超过了 16 * 0.75 = 12 就需要将当前 16 的容量进行扩容,而扩容这个过程涉及到 rehash、复制数据等操作,所以非常消耗性能。
final float loadFactor;
HashMap Node 节点类:
static class Node<K,V> implements Map.Entry<K,V> {
// 哈希值,存放元素到hashmap中时用来与其他元素hash值比较
final int hash;
//键
final K key;
//值
V value;
// 指向下一个节点
Node<K,V> next;
}
HashMap 树节点:
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
// 父
TreeNode<K,V> parent;
// 左
TreeNode<K,V> left;
// 右
TreeNode<K,V> right;
// needed to unlink next upon deletion
TreeNode<K,V> prev;
// 判断颜色
boolean red;
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
// 返回根节点
final TreeNode<K,V> root() {
for (TreeNode<K,V> r = this, p;;) {
if ((p = r.parent) == null)
return r;
r = p;
}
}
}
}
HashMap 的四个构造方法介绍?
- HashMap() 默认构造函数,装载因子为默认的 0.75f;
- HashMap(Map<? extends K, ? extends V> m) 包含一个 map 的构造函数, 默认装载因子为 0.75f,并且会调用 putMapEntries(m, false); 方法把 map 全部放到新的map中;
- HashMap(int initialCapacity) 指定容量大小的构造函数;
- HashMap(int initialCapacity, float loadFactor) 指定容量大小和装载因子的构造函数;
对于指定了容量大小的构造函数,只是将指定的容量大小通过 tableSizeFor 计算出阈值,在后续扩容的过程中才会真正初始化;
putMapEntries 方法分析?
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
int s = m.size();
if (s > 0) {
// 判断table是否已经初始化
if (table == null) {
/*
* 未初始化:
* 阈值=容量*负载因子,ft指的是要添加s个元素所需的最小的容量;
*/
float ft = ((float)s / loadFactor) + 1.0F;
int t = ((ft < (float)MAXIMUM_CAPACITY) ?(int)ft : MAXIMUM_CAPACITY);
/*
根据构造函数可知,table未初始化,threshold实际上是存放的初始化容量,如果添加s个元素所需的最小容量大于初始化容量,则将最小容量扩容为最接近的2的幂次方大小作为初始化。
如果容量达到最大值 1>>>30, 则返回最大值,否则扩大为原来的二次幂。
*/
if (t > threshold)
threshold = tableSizeFor(t);
// 已初始化,并且m元素个数大于阈值,进行扩容处理
} else if (s > threshold){
resize();
}
// 将m中的所有元素添加至HashMap中,如果table未初始化,putVal中会调用resize初始化或扩容
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false, evict);
}
}
}
put 方法分析?
put 方法的大致步骤:
- 如果定位到数组位置没有元素就直接插入;如果定位到数组位置有元素,就比较 key 是否相同,如果相同就替换值;如果不同,就判断当前节点是否为树节点,如果是树节点,按照树节点插入;如果是不是树节点就插入链表尾部,插入后判断是否满足转树的条件,如果满足就转树,这个插入过程完成后,对 size 进行自增后,判断是否需要扩容,入股需要扩容就进行扩容。
源码详细分析
// put 方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
// putVal 方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// table 未初始化或者长度为 0,进行扩容
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// (n - 1) & hash 确定元素存放在哪个桶中,桶为空,新生成结点放入桶中(此时,这个结点是放在数组中)
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
// 桶中已经存在元素(处理hash冲突)
else {
Node<K,V> e; K k;
// 快速判断第一个节点table[i]的key是否与插入的key一样,若相同就直接使用插入的值p替换掉旧的值e。
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 判断插入的是否是红黑树节点
else if (p instanceof TreeNode)
// 插入树中
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 不是红黑树节点则说明为链表结点
else {
// 在链表最末插入结点
for (int binCount = 0; ; ++binCount) {
// 到达链表的尾部
if ((e = p.next) == null) {
// 在尾部插入新结点
p.next = newNode(hash, key, value, null);
// 结点数量达到阈值(默认为 8 ),执行 treeifyBin 方法
// 这个方法会根据 HashMap 数组来决定是否转换为红黑树。
// 只有当数组长度大于或者等于 64 的情况下,才会执行转换红黑树操作,以减少搜索时间。否则,就是只是对数组扩容。
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
// 跳出循环
break;
}
// 判断链表中结点的key值与插入的元素的key值是否相等
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
// 相等,跳出循环
break;
// 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表
p = e;
}
}
// 表示在桶中找到key值、hash值与插入元素相等的结点
if (e != null) {
// 记录e的value
V oldValue = e.value;
// onlyIfAbsent为false或者旧值为null
if (!onlyIfAbsent || oldValue == null)
//用新值替换旧值
e.value = value;
// 访问后回调
afterNodeAccess(e);
// 返回旧值
return oldValue;
}
}
// 结构性修改
++modCount;
// 实际大小大于阈值则扩容
if (++size > threshold)
resize();
// 插入后回调
afterNodeInsertion(evict);
return null;
}
get 方法源码详细分析
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;
// table 不为 null, 长度不为 0,hash 所在的 hash 桶位置不为 null。
if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
// 检查 hash 所在位置的第一个元素 key 是否相同,相同就返回对应的值
if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))))
return first;
// 第一个 key 不是要找的key,且桶中不止一个节点;
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;
}
resize 方法源码分析
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
// 旧容量大于 0
if (oldCap > 0) {
// 超过最大值就不再扩充了,就只好随你碰撞去吧
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
// 将旧容量翻倍作为新容量,如果没超过最大值,就扩充为原来的2倍
} else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1;
// 旧阈值大于 0,(说明此时 oldCap == 0,说明此时是创建对象后的初始化阶段)
}else if (oldThr > 0)
// 创建对象时初始化容量大小放在threshold中,此时只需要将其作为新的数组容量
newCap = oldThr;
// 默认情况,旧容量和旧阈值都为 0,,此时按照无参构造函数创建的对象在这里计算容量和阈值
else {
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
// 创建时指定了初始化容量或者负载因子,在这里进行阈值初始化,
// 或者扩容前的旧容量小于16,在这里计算新的resize上限
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
// 把每个旧的bucket都移动到新的bucket中
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
// 只有一个节点,直接计算元素新的位置即可
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
/**
* 1. 对于树,会根据树的每个节点的hashcode最高位是 0 或者 1,拆分为 2 棵树;
* 2. 最高位是 0 的树,会在原来的位置,最高位是 1 的,会放在新位置(index + oldCap)
* 3. 将红黑树拆分成2棵子树,如果子树节点数小于等于 UNTREEIFY_THRESHOLD(默认为 6),则将子树转换为链表。
* 4. 如果子树节点数大于 UNTREEIFY_THRESHOLD,则保持子树的树结构。
*
* 举例帮助理解:对于长度为 2 的 table,2 和 4 都会分配到 2 位置,当 table 扩容后,2 应该保持位置不变,但是 4 应该去 4 位置。
*/
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else {
/**
* 对于链表的处理逻辑跟树类似,会把链表拆分为两个子链表;
*/
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
// 原索引
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
// 原索引+oldCap
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
// 原索引放到bucket里
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
// 原索引+oldCap放到bucket里
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
remove 方法源码详解?
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value;
}
final Node<K,V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
// 首先,table 不为空、table 对应的位置桶位置不为空;
if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) {
Node<K,V> node = null, e; K k; V v;
// 如果头结点就是删除的节点,把头节点保存到 node 里;
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
node = p;
else if ((e = p.next) != null) {
// 否则,如果是树节点,遍历树,找到要删除的节点;
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
// 如果是链表,遍历链表,找到要删除的点,此时 node 保存的是要删除的节点的前一个节点,p 保存的是要删除的节点;
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
// 如果要删除的节点存在;
if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) {
// 对于树,按照树删除;
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
else if (node == p)
// 对于头结点就是要删除的节点,要删除的节点的 next 节点放到桶里;
tab[index] = node.next;
else
// 对于链表,将要删除的节点的前置节点的 next 指针指向要删除的节点的 next 节点;
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
ConcurrentHashMap 源码分析
ConcurrentHashMap 1.7
ConcurrentHashMap 1.7 的存储结构?
- 结构:Segment 数组 + HashEntry 数组 + 链表;
- Segment 数组不能扩容,默认长度是 16,由于锁加在每个 Segment 上,所以这意味着默认支持 16 个线程并发;
- HashEntry 数组可以扩容,扩容是基于装载因子(默认 0.75f),并将 HashEntry 扩容为原来的 2 倍;
- Segment 和 HashEntry 的长度都是 2 的次幂;
ConcurrentHashMap 1.7 的构造函数?
- 无参构造函数,默认初始化容量是 16, 装载因子是 0.75f,并发级别是 16;
- 有参构造函数:
- 如果并发级别超过 MAX_SEGMENTS(65536),就取最大值 MAX_SEGMENTS;
- 否则取指定 initialCapacity 最近的 2 次幂数作为 initialCapacity;
- 计算 segmentShift 偏移量,segmentMask 掩码;
- 计算 每个 segment 的容量,公式: 容量/并发级别 = 单个 segment 容量,默认情况下是 16/16 = 1, 由于容量必须是 2 或者 2 的倍数,所以初始容量是 2;
- 初始化 segments[0],默认大小为 2,负载因子 0.75,扩容阀值是 2*0.75=1.5,插入第二个值时才会进行扩容。
ConcurrentHashMap 1.7 的 put 方法详解?
- 通过 hash 值与 segmentShift 偏移量和 segmentMask 的掩码进行计算,获取到当前 key 所在的 segment;
- 如果当前 segment 为 null,调用 ensureSegment 对当前 segment 进行初始化;
- 最后调用 segment 的 put 方法存放元素;
ConcurrentHashMap 1.7 的 segment 初始化方法 ensureSegment 详解?
- 首先判断指定位置的 segment 是否为 null,如果不为 null 直接返回 segment;
- 当 segment 为 null 时,获取 0 号 segment 的容量、装载因子,并计算阈值;
- 根据前面获取的容量、装载因子、阈值初始化 HashEntry 数组;
- 再次判断当前位置的 segment 是否为 null, 如果为 null,采用 cas 自旋来对当前位置 segment 进行赋值,完成初始化;
ConcurrentHashMap 1.7 的 segment 的 put 方法详解?
- Segment 继承了 ReentrantLock,所以 Segment 内部可以很方便的获取锁;
- tryLock() 获取锁,获取失败,则使用 scanAndLockForPut 方法继续获取锁;
- 获取到锁后,根据 hash 和 tab.length 计算出当前 key 所在 HashEntry 数组的位置,并获取当前的 HashEntry;
- 如果当前 HashEnty 不为 null,就遍历链表,如果找到相同的 key 就替换值;
- 如果没有找到相同的 key,判断 node 是否为 null,如果 node 为 null ,就用 key、value 新建一个 HashEntry ,如果 node 不为 null,将 node 指向 头结点,使用头插法插入;
- 然后判断是否需要扩容,进行处理;
- put 结束,释放锁;
ConcurrentHashMap 1.7 的 segment 的 scanAndLockForPut 方法详解?
- 进入 scanAndLockForPut 方法后,先采用 cas 自旋进行 tryLock;
- 达到一定次数后,进入阻塞获取锁状态 lock;
- 获取到锁,方法返回;
ConcurrentHashMap 1.7 的扩容 rehash 方法详解?
- 首先根据 oldTable.length 把容量扩充为 2 倍;
- 计算出新的阈值;
- 创建新的 HashEntry 数组;
- 循环老的 HashEntry 数组,将元素重新分配到新的 HashEntry 数组中;
- 首先获取老
HashEntry[i]的值,如果它的 next 为 null,表示是单值,直接计算在新数组中的位置并放置; - 如果 next 不为 null,首先循环整个链表,找到最后一个不在原位置的元素,并把它放到新的位置;
- 再次循环整个链表,根据每个元素位置,将它们链接到新的链表上;
- 首先获取老
- 计算传入的 node 在新数组中的位置,并放到正确的位置;
- 把新数组赋值给 table;
ConcurrentHashMap 1.7 的 get 方法详解?
get 方法分两步查询即可;
- 通过 hash 以及 segmentShift 和 segmentMask,计算出 segment 的位置;
- 从 segment 中取出 HashEntry,并计算 key 在 HashEntry 中的位置;
- 遍历 HashEntry, 根据 hash 值相等,及 key equals 取出正确的值,如果没有,返回 null;
ConcurrentHashMap 1.8
ConcurrentHashMap 1.8 的存储结构?
不再是 segment 数组 + HashEntry 数组 + 链表,而是 Node 数组 + 链表/红黑树,当链表达到一定长度,就会转换成红黑树;
2. ConcurrentHashMap 1.8 初始化 initTable 详解?
由于 ConcurrentHashMap 构造函数只会先创建一个空的对象,不会立即对 Node 数组进行初始化化,当需要使用是,才会调用 initTable 进行初始化;
初始化的过程如下:
- 进入自旋,自旋条件是
Node<K,V>[] tab是否为 null 或者 长度为 0; - 在自旋中判断 sizeCtl 是否小于 0, 小于 0 表示另外的线程正在对 tab 初始化或者扩容,此时需要调用
Thread.yield();让出 CPU; - 否则使用 unsafe 类的 compareAndSwapInt 将 SIZECTL 位置的 sizeCtl 字段值更新为 -1,如果更新失败,进入下次自旋,如果更新成功,对 tab 进行初始化;
- 根据初始化的容量计算出最新的 sizeCtl 作为下次扩容的阈值;
- 返回 tab;
源码
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
// 开始自旋
while ((tab = table) == null || tab.length == 0) {
// 如果 sizeCtl < 0 ,说明另外的线程执行CAS 成功,正在进行初始化或者扩容;
if ((sc = sizeCtl) < 0)
// 让出 CPU 使用权
Thread.yield();
// 尝试 cas,当 SIZECTL 位置的字段 sizeCtl 值等于 sc 时,更新为 -1;
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if ((tab = table) == null || tab.length == 0) {
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
// 计算新的 sizeCtl 作为下次扩容的阈值,实际值是 n*(3/4);
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
break;
}
}
return tab;
}
sizeCtl 值说明:
- -1 说明正在初始化,其他线程需要自旋等待;
- -N (N - 1) 为正在进行扩容的线程数;
- 0 表示 table 初始化大小, table 没有初始化;
-
0 表示 table 下次扩容的阈值;
2. ConcurrentHashMap 1.8 put 方法详解?
- 判断 key、value 是否为 null ,为 null 抛异常;
- 计算 key 在哈希桶中的位置;
- 自旋 table:
- 如果 table 为 null 进入 initTable 流程,然后继续自旋;
- 判断 table 当前位置是否为 null, 如果是 就 cas 尝试将新节点放入,成功就退出自旋,否则继续自旋;
- 如果 table 当前位置是不为 null, 则判断当前 node 是否处于 MOVED 状态,如果处于移动状态,当前线程会进行帮助转移;
- 如果当前 node 不是处于 MOVED 状态,则使用 synchronized 加锁,将节点插入;
- 对于链表的情况,使用 for 循环,一边判断是否需要覆盖节点,一边判断是否到达链表尾部,如果到达尾部,直接插入;
- 对于红黑树,直接按照红树插入;
public V put(K key, V value) {
return putVal(key, value, false);
}
final V putVal(K key, V value, boolean onlyIfAbsent) {
// key 和 value 为空抛异常
if (key == null || value == null)
throw new NullPointerException();
// 计算 key 在通中的位置
int hash = spread(key.hashCode());
int binCount = 0;
// 进入自旋
for (Node<K,V>[] tab = table;;) {
// f = 目标位置元素
// fh 后面存放目标位置的元素 hash 值
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
// 数组桶为空,初始化数组桶(自旋+CAS)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 桶内为空,CAS 放入,不加锁,成功了就直接 break 跳出
if (casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null)))
break;
} else if ((fh = f.hash) == MOVED)
// 扩容时,正在发生元素迁移,需要进行帮助迁移
tab = helpTransfer(tab, f);
else {
V oldVal = null;
// 使用 synchronized 加锁加入节点
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, 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;
}
}
}
}
if (binCount != 0) {
// 插入成功后,检查是否需要扩容或者转树
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
ConcurrentHashMap 1.8 get 方法详解?
- 根据 hash 值计算位置。
- 查找到指定位置,如果头节点就是要找的,直接返回它的 value.
- 如果头节点 hash 值小于 0 ,说明是红黑树。
- 如果是链表,遍历查找之。
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
// key 所在的 hash 位置
int h = spread(key.hashCode());
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
// 如果指定位置元素存在,头结点hash值相同
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
// key hash 值相等,key值相同,直接返回元素 value
return e.val;
}
else if (eh < 0)
// 头结点hash值小于0,说明正在扩容或者是红黑树,find查找
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;
}