Map篇
从Map开始说起
按照惯例,我们还是看一下HashMap的类名,可以看到HashMap实现了Map接口和继承了AbstractMap抽象类。
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
三个视图
而抽象类AbstractMap也同样实现了Map接口,所以我们先来看一下Map接口包含了哪些内容。接口的前半部分根据注释可以分为三个部分:查询、修改以及视图(这里为了节约空间,删去了大量注释)。
这边需要着重注意一下视图,从视图可以看出一个map可以从三个角度来分析Map:KeySet、Values、Entry。
KeySet是一个set的集合,这意味这不能重复,Values是一个普通的集合,Entry是一个内部接口,同样通过Set来存放。使用集合和Set说明了另外一个问题,如果不做判断的话,key和value都是可以为null的。
public interface Map<K,V> {
// Query Operations
int size();
boolean isEmpty();
boolean containsKey(Object key);
boolean containsValue(Object value);
V get(Object key);
// Modification Operations
V put(K key, V value);
V remove(Object key);
// Views
Set<K> keySet();
Collection<V> values();
Set<Map.Entry<K, V>> entrySet();
...//省略
}
由于Map提供了三种视图,所以可以通过这三种方式来进行遍历Map:
Set set = map.keySet();
for (Object key : set) {
System.out.println(map.get(key));
}
Collection values = map.values();
Iterator iterator = values.iterator();
while (iterator.hasNext()){
System.out.println("value " + iterator.next());
}
Set entrySet = map.entrySet();
for (Object o : entrySet) {
Map.Entry entry = (Map.Entry) o;
System.out.println(entry); //key=value
System.out.println(entry.getKey() + " / " + entry.getValue());
}
Entry
最后再来看一下Entry内部接口,Entry包含了key的get方法、value的get和set方法以及自己的equals和hashCode方法。
在最后还包含了两个默认的和重载的键值比较器。
interface Entry<K,V> {
K getKey();
V getValue();
V setValue(V value);
boolean equals(Object o);
int hashCode();
...//省略了key和value的比较器
}
抽象类AbstractMap
抽象类AbstractMap实现了Map接口,也是HashMap、TreeMap、ConcurrentHashMap等类的父类。
AbstractMap实现了大部分对Map的查询和修改操作,从下面的remove操作可以看出,AbstractMap对键值对的操作都是基于其集合内部的迭代器的。
public V remove(Object key) {
Iterator<Entry<K,V>> i = entrySet().iterator();
Entry<K,V> correctEntry = null;
if (key==null) {
while (correctEntry==null && i.hasNext()) {
Entry<K,V> e = i.next();
if (e.getKey()==null)
correctEntry = e;
}
} else {
while (correctEntry==null && i.hasNext()) {
Entry<K,V> e = i.next();
if (key.equals(e.getKey()))
correctEntry = e;
}
}
V oldValue = null;
if (correctEntry !=null) {
oldValue = correctEntry.getValue();
i.remove();
}
return oldValue;
}
除了唯一的抽象方法和一个不支持的put方法。
如果要实现一个不可变的Map类,我们可以继承这个类,然后实现entrySet()
方法,这个Set一般不支持修改方法,对应的迭代器也不能remove。
public abstract Set<Entry<K,V>> entrySet();
但是想要实现一个可变的Map,就需要再重写put方法以及entrySet的迭代器,因为AbstractMap在删除的时候会调用entrySet的迭代器。
public V put(K key,V value){
throw new UnsupportedOperationException();
}
成员变量
AbstractMap有两个成员变量ketSet、values集合,这两个值都是使用volatile关键字和transient关键字修饰的,说明了这两个变量是有多线程的有序性、可见性以及他们是不可序列化的。
volatile修饰不代表map就是线程安全的了,因为这边没有实现原子性,所以HashMap在并发的时候依旧出现数据丢失的问题。
transient volatile Set<K> keySet;
transient volatile Collection<V> values;
内部类
刚才说到Map内部存在一个Entry接口,AbstractMap通过内部类SimpleImmutableEntry、SimpleEntry实现了这个接口,前者是不可变的键值对,后者是可变的。
可以看到在SimpleImmutableEntry中的putValue方法是不被支持的。
public static class SimpleEntry<K,V>
implements Entry<K,V>, java.io.Serializable
{
private static final long serialVersionUID = -8499721149061103585L;
private final K key;
private V value;
public SimpleEntry(K key, V value) {
this.key = key;
this.value = value;
}
public SimpleEntry(Entry<? extends K, ? extends V> entry) {
this.key = entry.getKey();
this.value = entry.getValue();
}
public K getKey() {
return key;
}
public V getValue() {
return value;
}
public V setValue(V value) {
V oldValue = this.value;
this.value = value;
return oldValue;
}
...//省略了equals、hashCode、toString方法
}
public static class SimpleImmutableEntry<K,V>
implements Entry<K,V>, java.io.Serializable
{
...//省略了和SimpleEntry相同的方法
public V setValue(V value) {
throw new UnsupportedOperationException();
}
...//省略了equals、hashCode、toString方法
}
HashMap
包含了13个成员变量、长达2393行的代码和注释、拥有复杂的红黑树结构,他就是面试官的最爱、面试者的噩梦——Hash·超强·Map。
成员变量
HashMap的成员可以分为两个部分:被final修饰的默认值和可以修改的普通变量。
//默认容量16,HashMap的容量必须是2的倍数
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//最大容量2^30
static final int MAXIMUM_CAPACITY = 1 << 30;
//负载因子,超过这个值之后需要进行扩容
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//树装阈值,当链表长度超过8的时候需要变为红黑树
static final int TREEIFY_THRESHOLD = 8;
//非树形阈值,当红黑树的容量小于6的时候,退化为链表
static final int UNTREEIFY_THRESHOLD = 6;
//树形最小容量,指当哈希表的容量超过64时允许转化为红黑树,不然直接扩容
static final int MIN_TREEIFY_CAPACITY = 64;
普通成员:
//元素个数
transient int size;
//链表数组
transient Node<K,V>[] table;
//缓存的键值对集合,没有keyset和values的原因是在抽象类中已经实现了。
transient Set<Map.Entry<K,V>> entrySet;
//负载因子
final float loadFactor;
//负载容量
int threshold;
//修改次数,用户fast-fail机制
transient int modCount;
构造方法
HashMap除了无参构造方法之外还可以指定容量、负载因子、Map的值。
//无参构造方法
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
//指定容量
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//指定容量和负载因子
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);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
//指定内容,使用默认的负载因子,复制传入Map的键值对
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
在之前我们说到,HashMap的容量始终为2的倍数,那么如果传入一个参数5会发生什么事呢?
我们可以看到在第三个方法中调用了tableSizeFor(initialCapacity)
方法,我们来看一下这个方法的内部实现,可以看到这个方法通过多次的右移和求异操作会返回最接近传入参数的容量(一定是大于)。
static final int tableSizeFor(int cap) {
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;
}
节点
HashMap中不使用Map接口的Entry<>,而实现了Entry<>接口的静态类Node和TreeNode。(1.8之后)
在链表节点Node中除了key和value之外还增加了指向下一个节点的next指针。
Node中还重写了equals方法,比较键值对的key和value是否相等而非地址。
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
...//
}
在TreeNode中,除了一般的树形结构都有的指针外,还增加节点的颜色。由于TreeNode继承了LinkedHashMap.Entry<K,V>,而LinkedHashMap.Entry<K,V>又继承了Node,所以将Node转换为TreeNode之后并不会对之前的操作产生任何影响。
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
...//
}
插入
HashMap的底层其实就是一个Node[]数组,每个数组的桶都代表着一条链表或者一颗红黑树。
我们来继续深入了解一下插入的过程。
通过下面的代码我们可以了解到HashMap的插入过程其实还是比较简单的,与我们在数据结构中学习到的拉链法其实没有什么区别。
但是当数组的元素超过负载因子的时候需要进行扩容,当数组的长度超过64且单个链表长度超过8的时候会转化为红黑树。
//添加指定的键值对到 Map 中,如果已经存在,就替换
public V put(K key, V value) {
//先调用 hash() 方法计算位置
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//如果当前 哈希表内容为空,新建,n 指向最后一个桶的位置,tab 为哈希表另一个引用
//resize() 后续介绍
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//如果要插入的位置没有元素,新建个节点并放进去
//注意这里,hash%2^n相当于和(n-1)与,速度非常快
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
//如果要插入的桶已经有元素,替换
// e 指向被替换的元素
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//p 指向要插入的桶第一个 元素的位置,如果 p 的哈希值、键、值和要添加的一样,就停止找,e 指向 p
e = p;
else if (p instanceof TreeNode)
//如果不一样,而且当前采用的还是 JDK 8 以后的树形节点,调用 putTreeVal 插入
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,就要树形化
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
//在treeifyBin方法中,判断数组长度
//如果小于64则直接扩容
//大于64才会进行树化
treeifyBin(tab, hash);
break;
}
//如果找到要替换的节点,就停止,此时 e 已经指向要被替换的节点
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//存在要替换的节点
if (e != null) {
V oldValue = e.value;
//替换,返回
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//如果超出阈值,就得扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
哈希
HashMap中的哈希函数并不是直接调用元素的hashCode
方法,而是先对其进行无符号右移16位,然后再和右移前的元素进行异或。
这样做的理由是什么呢?
根据这个函数上的注释说明,大概就是由于哈希数组的容量是2的N次方,很多时候高位的hashCode是相同的,这样最后的位置只取决于低位的hashCode,所以进行右移再和自身取异或的操作,就能以一个比较简单的方式让数据分布地更加均匀一些。
static final int hash(Object key) {
int h;
//右移16位然后和右移前进行异或
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
扩容
扩容的代码虽然比较长,但是整体上和之前的ArrayList的扩容机制是类似的。
扩容的时候都会首先判断是否达到了容量上限,如果达到了就直接把负载容量扩充到Integer的最大值,这样就不会导致负载容量的溢出。
如果没有达到的话就直接变为原来的2倍,这里用的是左移操作。
同样的,如果数组的长度是0的话就就要进行判断,如果负载容量不为0,说明之前创建了哈希表,所以这里会把数组膨胀到负载容量。
但是如果负载容量也没有初始化,说明这个数组是无参构造函数创建的,并且还未添加过任何元素,所以直接膨胀到默认大小。
之前的ArrayList是创建一个新数组然后将元素复制过去,HashMap也是类似,只不过需要先进行Hash,所以不一定是在原来的位置上。
经杰哥和师姐指出,这里还有一个重要的点,就是扩容之后,元素的位置要么出现在原来的桶中,要么出现在桶+原来数组长度的桶中。
以16为例,扩容之后的数组长度为32,二进制数由1111
变成了0001 1111
,所以最后4位与还是相同的,只要看元素的第五位是0还是1,0的话就放在原来的桶中,1的话则放入到桶+原来数组长度的桶中。
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;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
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;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
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)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
//这里进行判断,这里与的不是oldCap-1,
//而是oldCap,所以也就是判断扩容后
//与操作的第一位是否为0
//如果是0的话就放入原来的桶中
//不是就扩容后的新桶
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
获取
获取的方式比较简单, 首先判断一下是否是null值,如果是的话返回null,不是的话就返回value,然后会根据桶中的数据结构是链表还是树采用不同的策略,如果是链表的就遍历一遍,是树的话采用红黑树的查找方式进行查找。
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;
//tab 指向哈希表,n 为哈希表的长度,first 为 (n - 1) & hash 位置处的桶中的头一个节点
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//如果桶里第一个元素就相等,直接返回
if (first.hash == hash &&
((k = first.key) == key || (key != null && key.equals(k))))
return first;
//否则就得慢慢遍历找
if ((e = first.next) != null) {
if (first instanceof TreeNode)
//如果是树形节点,就调用树形节点的 get 方法
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
//do-while 遍历链表的所有节点
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
默认数据
最后一个小tips,树化阈值设置为8?
根据源码上面的注释,作者进过大数据分析之后,哈希冲突为8的概率非常小,只有不到十万分之一,所以选择8作为树化的阈值。
ConcurrentHashMap
ConcurrentHashMap是一个线程安全的HashMap,由于其出色的性能和效率基本已经完全取代了HashTable的地位,那么它是如何实现线程安全的呢?
按1.8的代码来说,ConcurrentHashMap采用了CAS和synchronized关键字取代了1.7中的分段锁Segment。
而在插入的时候采用CAS来解决不频繁的冲突,在扩容时候则采用synchronized锁住整个Map达到原子性的目的。
继承与接口
和HashMap相同,ConcurrentHashMap也继承于AbstractMap类,并且实现了
ConcurrentMap接口,同时是可以被序列化的。但是他没有实现cloneable接口,那说明多半是不能够使用.clone()
方法的。
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
implements ConcurrentMap<K,V>, Serializable {
ConcurrentMap
按照惯例,我们先来看ConcurrentMap接口。
这个接口继承于Map接口,可以看到里面的方法并不多,只重写了Map中的getOrDefault、replace等方法。
我们以getOrDefault为例,简单的看一下二者有什么区别,为了方便观察,我把两个接口中的方法放到了一起。
可以看到ConcurrentHashMap使用get方法获取到null之后是直接返回的,这是由于ConcurrentHashMap中不允许存在键值为null的元素,而Map中是允许存在键值为null的元素的。
而且更重要的一点是,由于ConcurrentHashMap是在并发场景下使用的,如果先使用get
再判断一下是否contanisKey
,那么如果一个线程在get
操作之后插入了一个key为null
的元素会得出错误的答案。
之后的重载也主要是由于一些并发上的考虑对default方法进行了修改。
//HashMap中的方法
default V getOrDefault(Object key, V defaultValue) {
V v;
return (((v = get(key)) != null) || containsKey(key))
? v
: defaultValue;
}
//ConcurrentHashMap中的方法
@Override
default V getOrDefault(Object key, V defaultValue) {
V v;
return ((v = get(key)) != null) ? v : defaultValue;
}
成员变量
ConcurrentHashMap除了之前的几个成员变量之外,还新增加了不少。
一些之前HashMap就存在的成员变量我这里就不列出来了。
可以看到很多成员变量都是使用volatile关键字修饰的。
为了提高和保证扩容时候的安全性,提供了很多变量用于辅助。
// 默认并发级别 jdk1.7 之前遗留的 1.8只用于初始化
private static final int DEFAULT_CONCURRENCY_LEVEL = 16;
//在扩容的时候使用,在ConcurrentHashMap进行扩容的时候。
//如果线程进来发现map正在扩容,会帮助进行扩容。
//扩容时候每个线程的最少搬运量
private static final int MIN_TRANSFER_STRIDE = 16;
//位的个数来用来生成戳,用在sizeCtrl里面;
private static int RESIZE_STAMP_BITS = 16;
//允许扩容时候参与的最大线程数
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
//用来记录在sizeCtrl中size戳的位偏移数。
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
//判断节点类型,-1为移动过的节点,-2为树节点,-3为临时节点
static final int MOVED = -1; // hash for forwarding nodes
static final int TREEBIN = -2; // hash for roots of trees
static final int RESERVED = -3; // hash for transient reservations
//31个1,用于将一个负数变成一个正数 但是不是取绝对值
static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash
//运行时的CPU数
static final int NCPU = Runtime.getRuntime().availableProcessors();
//扩容用的临时列表
private transient volatile Node<K,V>[] nextTable;
// LongAdder的baseCount
private transient volatile long baseCount;
/**
sizeCtl <0
1. -1 的时候 表示table正在初始化(有线程正在初始化 , 当前线程应该自旋等待)
2. 其他情况 表示当前map正在进行扩容 高16位表示 扩容的标识戳 , 低16位表示 扩容线程数量
sizeCtl = 0
表示创建数组 使用默认容量 16
sizeCtl >0
1. 如果table 未初始化 表示 初始化大小
2. 如果table 已经初始化 表示下次扩容的阈值
*/
private transient volatile int sizeCtl;
//扩容过程中,记录当前进度,所有线程都需要从transferIndex中分配区间任务,去执行自己的任务
private transient volatile int transferIndex;
// 0 表示 无锁 1 表示加锁
private transient volatile int cellsBusy;
// LongAdder 中的cells 数组 当baseCount发生竞争后 会创建cells 数组
// 线程会通过计算hash值 取到自己的cell中
private transient volatile CounterCell[] counterCells;
构造方法
相比较之前的构造方法,ConcurrentHashMap多出了一个构造方法。
可以看到这里还能指定并发级别。
public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
if (initialCapacity < concurrencyLevel) // Use at least as many bins
initialCapacity = concurrencyLevel; // as estimated threads
long size = (long)(1.0 + (long)initialCapacity / loadFactor);
int cap = (size >= (long)MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY : tableSizeFor((int)size);
this.sizeCtl = cap;
}
get()
还是从最简单的get
方法开始。
可以看到最上来的取哈希操作变为了spread
,那么spread
方法中有什么呢?
可以看到除了之前的取余操作之外还和31个1进行了与操作,这样做的原因是为了消除最高位上的负符号 hash的负在ConcurrentHashMap中有特殊意义表示在扩容或者是树节点。
我们可以看到get方法中判断了eh是否小于0,是的话说明正在扩容或者是节点,需要采用别的方法get。
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;
}
else if (eh < 0)
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;
}
//取哈希函数
static final int spread(int h) {
return (h ^ (h >>> 16)) & HASH_BITS;
}
put()
首先可以看到put操作需要分四种情况讨论。
首先如果这个Map是空的,则需要先进行初始化。
如果发现这个Map不为null,并且插入的位置元素为空,则采用CAS进行更新。
如果发现这个Map正在扩容,则进行帮助。
如果发现插入的地方存在元素,则需要先锁住头结点,然后再进行插入。
/** Implementation for put and putIfAbsent */
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, null)))
break; // no lock when adding to empty bin
}
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, 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;
}
}
}
}
//当前链表长度不是0
if (binCount != 0) {
//判断是否需要树化
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
//如果存在旧值就返回旧值
if (oldVal != null)
return oldVal;
break;
}
}
}
//计数并判断是否扩容
addCount(1L, binCount);
//返回null
return null;
}
计数
计数其实是扩容的前置操作,类似一个缓冲区的地方,如果是put调用的计数,则先判断一下是否需要扩容,是否在扩容,是否能参与扩容
private final void addCount(long x, int check) {
//cs 是cells 单元
//b 是未发生竞争的base
// s 是元素数量
CounterCell[] cs; long b, s;
// LongAdder 操作
if ((cs = counterCells) != null ||
!U.compareAndSetLong(this, BASECOUNT, b = baseCount, s = b + x)) {
CounterCell c; long v; int m;
boolean uncontended = true;
if (cs == null || (m = cs.length - 1) < 0 ||
(c = cs[ThreadLocalRandom.getProbe() & m]) == null ||
!(uncontended =
U.compareAndSetLong(c, CELLVALUE, v = c.value, v + x))) {
fullAddCount(x, uncontended);
return;
}
if (check <= 1)
return;
s = sumCount();
}
// 如果check >=0 说明一定是put 调用的 addCount
if (check >= 0) {
// tab 表示 map.table
// nt 是nexttable
//n 是数组长度
//sc 是sizeCtl的长度
Node<K,V>[] tab, nt; int n, sc;
//自旋
//条件1 s >= (long)(sc = sizeCtl)
// ture 1: 当前sizeCtl 是一个负数 表示正在扩容中。
// 2 : 当前sizeCtl 是一个正数 表示扩容阈值
// false 表示当前table 尚未达到扩容条件
// 条件3 : 当前table长度小于最大值限制 可以扩容
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
// 获取到唯一标识戳
int rs = resizeStamp(n);
//条件1
// ture 说明当前线程获取的扩容唯一标识撮 不是本次扩容
// false 说明当前线程获取到的扩容唯一标识撮 是本次扩容
// 条件2
// ture 表示扩容完毕了 false 表示在扩容过程中
// 条件3
// ture : 触发的帮助扩容的达到最大值 65535-1
// false 可以参与
// 条件4
// ture 扩容结束
// false 扩容正在进行
if (sc < 0) {
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
// 当前线程正在扩容中 当前线程参与
// ture 可以参与
//fasle 进行自旋 大概率还会来到这里
//条件失败 1 当前有很多线程都尝试修改sizeCtl 可能会导致和内存的不一样
// transfer 任务内的线程也修改sizeCtl
if (U.compareAndSetInt(this, SIZECTL, sc, sc + 1))
// 协助扩容线程 支持nextTable参数
transfer(tab, nt);
}
// 条件成立 说明当前线程是触发扩容的第一个线程 在transfer方法需要做一些扩容的准备工作
else if (U.compareAndSetInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
//触发扩容的线程 不持有nextTable
transfer(tab, null);
s = sumCount();
}
}
}
扩容
ConcurrentHashMap的扩容采用的是transfer方法,这个方法会传入当前列表和缓存的下一个列表,如果是当前线程执行扩容下一个列表值为null,如果是协助则不为null。
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
// n 扩容之前table数组的长度 tride 分配给线程任务的步长
int n = tab.length, stride;
// 根据CPU核心数分配扩容的步长
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // subdivide range
// 触发扩容的线程 需要做一些扩容准备工作
if (nextTab == null) {
try {
// 创建了比扩容之前大1倍的table
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
nextTab = nt;
} catch (Throwable ex) { // try to cope with OOME
sizeCtl = Integer.MAX_VALUE;
return;
}
nextTable = nextTab; // 方便协助线程 拿到新表
transferIndex = n;// 记录迁移数据整体位置的一个标记 从1开始
}
// 新表长度
int nextn = nextTab.length;
// 新表的引用 当某个桶位处理完毕 将设置为fwd 结点
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
boolean advance = true;// 推进标记
boolean finishing = false; // 完成标记
// i 表示分配给当前线程任务,执行到的桶位
// bound 表示分配给当前线程的下界限制
int i = 0, bound = 0;
for (;;) {// 自旋
// f 桶位的头结点 fh 头结点的hash
Node<K,V> f; int fh;
/**
* 1 给当前线程分配区间
* 2 维护当前线程任务进度(i 当前处理的桶位
* 3 维护map对象全局范围内的进度
*/
while (advance) {
// 分配任务区间的变量 nextIndex分配开始的下标 nextBound 结束下标
int nextIndex, nextBound;
// --i>=bound 当前线程任务尚未完成, 还有相应的区间桶位要处理 --i 表示下一个处理的桶位
// 不成立 表示已经完成 或者未分配
if (--i >= bound || finishing)
advance = false;
// 前置条件 任务已经完成 或者还没有分配任务
// 条件成立 表示对象全局范围内的桶位都分配完毕了 没有区间分配了
// 设置全局i变量为-1 执行退出迁移相关的程序
// 不成立 全局范围内的桶位尚未分配还有区间可分配
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
// 前置条件 当前线程需要分配任务区间 2 全局范围内的桶位尚未分配还有区间可分配
// 条件成功 说明给当前任务分配成功
// 失败 就是 说明分配给当前线程失败,可能发生竞争
else if (U.compareAndSetInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
// 区间复制给bound
bound = nextBound;
i = nextIndex - 1;
advance = false; // 结束
}
}
// i<0 的情况 表示当前线程未分配到任务
if (i < 0 || i >= n || i + n >= nextn) {
int sc;// sizeCtl 的临时变量
if (finishing) {
nextTable = null;
table = nextTab;
sizeCtl = (n << 1) - (n >>> 1);
return;
}
// 条件成立 说明 设置sizeCtl 低16位-1成功 当前线程可以退出
if (U.compareAndSetInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
//条件成立 说明当前线程不是最后一个退出transfer任务线程
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
// 退出
return;
finishing = advance = true;
i = n; // recheck before commit
}
}
//前置条件
//casr1:说明当前桶未 没有存放数据 只需要将此处设置为fwd结点
// 如果不成立 说明桶未有数据
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
//casr2:条件成立 说明当前桶位已经迁移过了,当前线程不用再处理
// 直接在此更新当前线程任务索引,在处理下一个桶位。
else if ((fh = f.hash) == MOVED)
advance = true; // already processed
// 前置条件 当前桶位有数据 而且node结点不是fwd结点 说明这些数据需要迁移
else {
// sync 加锁当前桶位的头结点
synchronized (f) {
// 防止 在你加锁投之前 头对象被其他线程修改过 导致你加锁对象错误
if (tabAt(tab, i) == f) {
// ln 低位链表 hn 高位链表
Node<K,V> ln, hn;
if (fh >= 0) { // 条件成立表示当前桶位是链表
//lastRun
// 可以获取当前链表 末尾连续高位不变的 node
int runBit = fh & n;
Node<K,V> lastRun = f;
for (Node<K,V> p = f.next; p != null; p = p.next) {
int b = p.hash & n;
if (b != runBit) {
runBit = b;
lastRun = p;
}
}
// 说明 lastRun链表 为低位链表 让ln 引用lastRun
if (runBit == 0) {
ln = lastRun;
hn = null;
}
else {
hn = lastRun;
ln = null;
}
//迭代链表 当循环结点 不等于lastRun
for (Node<K,V> p = f; p != lastRun; p = p.next) {
int ph = p.hash; K pk = p.key; V pv = p.val;
if ((ph & n) == 0)
ln = new Node<K,V>(ph, pk, pv, ln);
else
hn = new Node<K,V>(ph, pk, pv, hn);
}
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
setTabAt(tab, i, fwd);
advance = true;
}
else if (f instanceof TreeBin) {
// 转换头结点为treeBin 引用
TreeBin<K,V> t = (TreeBin<K,V>)f;
// 低位双向链表+
TreeNode<K,V> lo = null, loTail = null;
TreeNode<K,V> hi = null, hiTail = null;
int lc = 0, hc = 0;
for (Node<K,V> e = t.first; e != null; e = e.next) {
int h = e.hash;
TreeNode<K,V> p = new TreeNode<K,V>
(h, e.key, e.val, null, null);
if ((h & n) == 0) {
if ((p.prev = loTail) == null)
lo = p;
else
loTail.next = p;
loTail = p;
++lc;
}
else {
if ((p.prev = hiTail) == null)
hi = p;
else
hiTail.next = p;
hiTail = p;
++hc;
}
}
ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
(hc != 0) ? new TreeBin<K,V>(lo) : t;
hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
(lc != 0) ? new TreeBin<K,V>(hi) : t;
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
setTabAt(tab, i, fwd);
advance = true;
}
}
}
}
}
}
协助扩容
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
// nextTable 就是fwd.nextTable
Node<K,V>[] nextTab; int sc;
//条件三:
// 恒成立
if (tab != null && (f instanceof ForwardingNode) &&
(nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
// 拿当前表的长度 获取扩容标识戳
int rs = resizeStamp(tab.length);
// 条件1 :
// 成立 标识当前扩容正在执行
//不成立 1. nextTable被设置为null 已经扩容完毕了
// 2。 再次出发扩容了 nextTable 已经过期了
//条件2 成立 说明扩容正在进行
// 不成立 扩容结束了 扩容结束之后最后退出的线程会把nextTable设置为table
// 条件3: 扩容正在进行中
// 不成立 sizeCtl 是当前扩容阈值 扩容已经结束了
while (nextTab == nextTable && table == tab &&
(sc = sizeCtl) < 0) {
//当前线程没事干了 直接不进入
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || transferIndex <= 0)
break;
// 尝试增加一个线程 帮助扩容
if (U.compareAndSetInt(this, SIZECTL, sc, sc + 1)) {
transfer(tab, nextTab);
break;
}
}
return nextTab;
}
return table;
}
后记
说实话ConcurrentHashMap相比较HashMap复杂了很多,很多地方的源码我其实并没有怎么看懂,由于水平有限,所以暂时先写到这里,后面如果发现更好的讲解ConcurrentHashMap的文章再更新。
感觉最近学习的效率不是很高,自己定了10号参加字节的投递,网易的校招也提前到了13号,时间不太多了。
后面应该花点时间总结一下线程和线程池的内容,总结一下ReentantLock的源码。
然后把字节喜欢面试的算法题再查漏补缺一下。