/**
* 空参构造默认初始化容量16(1 << 4是16),必须是2的幂
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**
*带参初始化map时,如果传入的参数大于2的30次方,则赋值map的容量为2的30次方
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* 初始化map时没有指定负载因子,默认的负载因子值
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* Entry类型的数组,HashMap用这个来维护内部的数据结构,长度必须是2的n次方,默认为空
*/
static final Entry<?,?>[] EMPTY_TABLE = {};
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
//下一个节点的引用
Entry<K,V> next;
//hash计算的值
int hash;
/**
* 此map中所有键值对的数量
*/
transient int size;
/**
* 加载因子,默认0.75f
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* 下次扩容的临界值,大小>=threshold(容量和 * 加载因子),就会扩容
* HashMap的极限容量
*/
int threshold;
/**
* 空参构造会调用HashMap(int initialCapacity, float loadFactor)构造方法
*/
public HashMap() {
//DEFAULT_INITIAL_CAPACITY这个是16,DEFAULT_LOAD_FACTOR这个是0.75
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
//initialCapacity是hashmap中数组的长度,loadFactor是加载因子
public HashMap(int initialCapacity, float loadFactor) {
//初始容量<0,抛IllegalArgumentException异常
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//初始化容量>最大容量,默认使用最大容量 2的30次方
//static final int MAXIMUM_CAPACITY = 1 << 30
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//加载因子<=0或者为空,抛出异常
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
//加载因子赋值
this.loadFactor = loadFactor;
//初始化容量赋值给阈值,所以jdk1.7的阈值构造时就是数组长度,put方法中会把阈值改成长度*加载因子;
//一上来直接就把长度*加载因子赋值给阈值是jdk1.6,这里面试被问到过,别记错了.
threshold = initialCapacity;
//此方法为空,LinkedHashMap会重写它,这里略过
init();
public V put(K key, V value) {
//判断hashmap数组是否为空
if (table == EMPTY_TABLE) {
//如果为空就初始化entry[]
inflateTable(threshold);
}
//判断k时候为null
if (key == null)
//如果为null,放入数组0号索引
return putForNullKey(value);
//计算hash值
int hash = hash(key);
//计算索引值
int i = indexFor(hash, table.length);
//循环entry[i]位置上的链表
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
//hash和key是否相同
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
//如果(key)相同,新值(value)覆盖上去
e.value = value;
e.recordAccess(this);
//返回旧值(value)
return oldValue;
}
}
//这个hashmap改变的次数,迭代器中用来判断
modCount++;
//k-v对添加到entry[]中
addEntry(hash, key, value, i);
return null;
}
/**
* map刚构造好,第一次put会调用这个方法初始化entry[]
*/
private void inflateTable(int toSize) {
//toSize是数组长度,
//这个方法的作用是找到大于或等于toSize的最小的2的幂(2的n次方)
int capacity = roundUpToPowerOf2(toSize);
//赋值阈值=数组长度*负载因子;如果阈值超过最大值(2的30次方)那么阈值就是最大值
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
//创建新的entry数组
table = new Entry[capacity];
//如果觉得源码的hash计算不够好可以自己配置虚拟机中"jdk.map.althashing.threshold"改变hash种子来改进hash算法
initHashSeedAsNeeded(capacity);
}
private static int roundUpToPowerOf2(int number) {
//判断数组长度是否大于最大值,则赋值为最大值
return number >= MAXIMUM_CAPACITY
? MAXIMUM_CAPACITY
//判断数组长度是否大于1,如果小于1则赋值为1
: (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}
public static int highestOneBit(int i) {
// HD, Figure 3-1
i |= (i >> 1);
i |= (i >> 2);
i |= (i >> 4);
i |= (i >> 8);
i |= (i >> 16);
return i - (i >>> 1);
}
//假设我们传入17
17:
0001 0001
>>1
0000 1000
|
0001 1001
>>2
0000 0110
|
0001 1111
>>4
0000 0001
|
0001 1111
>>8
0000 0000
|
0001 1111
>>16
0000 0000
|
0001 1111
>>>1
0000 1111
-
0001 0000---16
//最终的结果是16,所以Integer.highestOneBit(int)方法返回的是小于等于传入int值的最大2的幂
//再看我们调用时Integer.highestOneBit((number - 1) << 1)
//(number - 1) << 1是我们传入的参数,number是数组初始长度,-1再左移一位
//就好比数组初始化容量指定17,那么17减去1再乘2是32,调用Integer.highestOneBit(32),返回32
//所以Integer.highestOneBit((number - 1) << 1)返回的是大于等于当前数组长度的最小的2的幂作为entry[]的初始长度
private void inflateTable(int toSize) {
int capacity = roundUpToPowerOf2(toSize);
//省略无关代码
//entry[]的初始长度在这里被赋值成大于等于当前数组长度的最小的2的幂
table = new Entry[capacity];
}
//key如果为null,放入数组0号索引
private V putForNullKey(V value) {
//遍历entry[0]的链表
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
//找到k为null的节点
if (e.key == null) {
//覆盖新v
V oldValue = e.value;
e.value = value;
//linkedhashmap重写,略过
e.recordAccess(this);
//返回旧v
return oldValue;
}
}
modCount++;
//添加entry节点
addEntry(0, null, value, 0);
return null;
}
//计算hash值,尽可能能的让h的高位参与运算,减少hash冲突,使hash值的位值在高低位上尽量分布均匀
final int hash(Object k) {
//哈希种子,默认为0
//我们可以通过配置虚拟机"jdk.map.althashing.threshold"属性来改变哈希种子,优化hash算法
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
//k的hashcode和哈希种子(默认0)做异或运算
h ^= k.hashCode();
//尽可能能的让h的高位参与运算
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
static int indexFor(int h, int length) {
//传入计算好的hash值,和entry[]长度-1做与运算
//结果是entry[]数组的索引下标
return h & (length-1);
}
//这里可以看出entry[]长度是2的幂的好处
//当HashMap的容量是16时,它的二进制是10000,(n-1)的二进制是01111,与随机hash值得计算结果如下:
0 1 1 1 1 0 1 1 1 1
0 1 0 0 1 0 1 1 0 1
& &
0 1 0 0 1 0 1 1 0 1
0 1 1 1 1 0 1 1 1 1
0 1 1 0 0 0 1 1 1 1
& &
0 1 1 0 0 0 1 1 1 1
//上面四种情况我们可以看出,不同的hash值,和(n-1)进行位运算后,能够得出不同的值,使得添加的元素能够均匀分布在集合中不同的位置上,避免hash碰撞。
//下面就来看一下HashMap的容量不是2的n次幂的情况,当容量为10时,二进制为01010,(n-1)的二进制是01001,向里面添加同样的元素,结果为:
0 1 0 0 1 0 1 0 0 1
0 1 0 0 1 0 1 1 0 1
& &
0 1 0 0 1 0 1 0 0 1
0 1 0 0 1 0 1 0 0 1
0 1 1 0 0 0 1 1 1 1
& &
0 1 0 0 0 0 1 0 0 1
//可以看出,有三个不同的元素进过&运算得出了同样的结果,严重的hash碰撞了。
//HashMap计算添加元素的位置时,使用的位运算,这是特别高效的运算;另外,HashMap的初始容量是2的n次幂,扩容也是2倍的形式进行扩容,是因为容量是2的n次幂,可以使得添加的元素均匀分布在HashMap中的数组上,减少hash碰撞,避免形成链表的结构,使得查询效率降低!
void addEntry(int hash, K key, V value, int bucketIndex) {
//jdk1.7的扩容条件是size(哈希表所以k-v对的数量)>阈值,并且entry[bucketIndex]为空才会扩容
//jdk1.8修改为只有size>阈值这一个条件,面试被问过
if ((size >= threshold) && (null != table[bucketIndex])) { //entry[]扩容成2倍并转移entry到新的数组上
resize(2 * table.length);
//k如果为0,则hash为0
hash = (null != key) ? hash(key) : 0;
//重新计算索引下标
bucketIndex = indexFor(hash, table.length);
}
//头插法添加节点
createEntry(hash, key, value, bucketIndex);
}
void resize(int newCapacity) {
//旧的entry[]
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
//如果以经是最大值(2的30次方)了,就不扩容了
threshold = Integer.MAX_VALUE;
return;
}
//创建新entry[]数组
Entry[] newTable = new Entry[newCapacity];
//转移entry到新的entry[]
transfer(newTable, initHashSeedAsNeeded(newCapacity));
//保存新数组
table = newTable;
//计算新阈值
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
//外层循环遍历的是entry[]
for (Entry<K,V> e : table) {
//内层循环遍历的是entry[]在这个位置上的链表
while(null != e) {
Entry<K,V> next = e.next;
//默认false不会执行,我们可以配置虚拟机中"jdk.map.althashing.threshold"让rehash为true
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
//计算索引,并使用头插法添加
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
//假设1和2两个线程都一起执行,线程1正常执行,
//线程2执行完这里被卡住;线程1执行完了线程2再执行
}
}
}
/**
* 执行代码
*/
int i = indexFor(e.hash, newCapacity);
//A的下一个节点指向entry[i]位置,这里要注意,因为entry[]是局部变量
//每个线程中都会存储一份,所以顺序是cba的entry[]是线程1的,线程2的entry[]现在还是空的
e.next = newTable[i];
//把A移动到newTable[i]位置
newTable[i] = e;
e = next;
while(null != e) {
//next指向A
Entry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity);
//A已经在那个位置了,不用动
e.next = newTable[i];
//把B移动到newTable[i]位置
newTable[i] = e;
//把next的值赋值给e
e = next;
while(null != e) {
//next指向null
Entry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity);
//把newTable[i](已经是B)的赋值给A.next;就是把A的下一个指向B
//出问题了哈,成环了
e.next = newTable[i];
//把A移动到newTable[i]位置
newTable[i] = e;
//把next的值赋值给e
e = next;
比put简单多了
public V get(Object key) {
if (key == null)
//key==null,遍历entry[0]位置上的链表
return getForNullKey();
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : hash(key);
//老样子循环entry[i]的链表
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
//如果hash和key都相等则返回这个entry
return e;
}
return null;
}
ConcurrentHashMap是Segment[]数组 + HashEntry[]数组结构,HashEntry存储键值对,Segment(继承与Reentrantlock)和HashEntry是ConcurrentHashMap的内部类.
ConcurrentHashMap使用分段锁技术,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
/**
* 空参构造时,用来计算出segment[]长度和entry[]长度
*/
static final int DEFAULT_INITIAL_CAPACITY = 16;
/**
* 空参构造,默认加载因子
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* 空参构造,默认并发级别,用来计算出segment[]长度和entry[]长度
*/
static final int DEFAULT_CONCURRENCY_LEVEL = 16;
/**
CAPACITY最大值,2的30次方
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* 每个segment中hashentry[]的最小长度
*/
static final int MIN_SEGMENT_TABLE_CAPACITY = 2;
/**
* 每个segment中hashentry[]的最大长度
*/
static final int MAX_SEGMENTS = 1 << 16;
/**
* 默认自旋次数,超过这个次数直接加锁,防止size方法中无限的进行自旋影响性能
*/
static final int RETRIES_BEFORE_LOCK = 2;
/**
* segment[]数组
*/
final Segment<K,V>[] segments;
/**
* 继承与ReentrantLock
*/
static final class Segment<K,V> extends ReentrantLock implements Serializable {
private static final long serialVersionUID = 2249069246763182397L;
/**
* 和CPU核心数有关,CPU核心数大于1是64,单核CPU是1
*/
static final int MAX_SCAN_RETRIES =
Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;
/**
* 内部存储HashEntry类型的数组
*/
transient volatile HashEntry<K,V>[] table;
/**
* segment[i]这个位置上hashentry[]存放k-v对的总数
* 没有加volatile修饰,所以只能在加锁或者确保可见性(如Unsafe.getObjectVolatile)
*/
transient int count;
/**
* 总的修改(put,remove)次数
*/
transient int modCount;
/**
* segment[i]这个位置上hashentry[]的阈值
* 超过了阈值(capacity *loadFactor)segment[]是不会扩容的,扩容的是segment[i]这个位置上的hashentry[]
*/
transient int threshold;
/**
* 加载因子
*/
final float loadFactor;
Segment(float lf, int threshold, HashEntry<K,V>[] tab) {
this.loadFactor = lf;
this.threshold = threshold;
this.table = tab;
}
static final class HashEntry<K,V> {
final int hash;
final K key;
volatile V value;
volatile HashEntry<K,V> next;
HashEntry(int hash, K key, V value, HashEntry<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
/**
* 空参构造
* 三个参数依次是16,0.75f,16
* 然后调用全参构造方法
*/
public ConcurrentHashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);
}
public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel) {
//判断加载因子<=0或 initialCapacity <0 或 concurrencyLevel <=0 抛异常
if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
//并发级别最大值MAX_SEGMENTS(2的16次方)
if (concurrencyLevel > MAX_SEGMENTS)
concurrencyLevel = MAX_SEGMENTS;
int sshift = 0;
int ssize = 1;
//下面这些代码是通过initialCapacity和concurrencyLevel两个属性找到segment和hashentry的长度
while (ssize < concurrencyLevel) {
//2的次方数,比如concurrencyLevel是16,那么sshift是4
++sshift;
//ssize是segment[]的长度,永远是2的幂次方
//比如concurrencyLevel是16,那么ssize是4
ssize <<= 1;
}
//这两个值再put方法种用来计算segment[]的索引下标
this.segmentShift = 32 - sshift;
this.segmentMask = ssize - 1;
//initialCapacity 最大值是2的30次方
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//这里的c取整数
int c = initialCapacity / ssize;
//c相当于initialCapacity / ssize向上取整
if (c * ssize < initialCapacity)
++c;
//每个segment中hashentry[]的最小长度是2
int cap = MIN_SEGMENT_TABLE_CAPACITY;
while (cap < c)
//segment中hashentry[]的长度,永远是2的幂次方
cap <<= 1;
// 生成一个segment s0作为模板
// 之后put方法中不需要重新计算segment[]长度和hashtry[]长度,直接从这个模板对象中取
Segment<K,V> s0 =
new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
(HashEntry<K,V>[])new HashEntry[cap]);
Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
//把s0放在ss的0号位置
UNSAFE.putOrderedObject(ss, SBASE, s0);
this.segments = ss;
}
//和1.7hashmap的k,v可以为null不同,1.7的concurrenthashmap的kv不能为null
public V put(@NotNull K key,@NotNull V value) {
Segment<K,V> s;
//v为空,空指针异常
if (value == null)
throw new NullPointerException();
//计算k的HASH值
int hash = hash(key);
//this.segmentShift = 32 - sshift; sshift是2的次方数
//this.segmentMask = ssize - 1; ssize是segment[]的长度;
//经过右移之后保留hash值的高sshift 位,让原来hash值的高位和segmentMask进行与操作.
int j = (hash >>> segmentShift) & segmentMask;
//Class sc = Segment[].class;
//int ss = UNSAFE.arrayIndexScale(sc);
//SSHIFT = 31 - Integer.numberOfLeadingZeros(ss); SSHIFT是s31 - (ss转换成2进制前面有几个0)
//SBASE = UNSAFE.arrayBaseOffset(sc); SBASE是segement数组
//代码的意思是获取segment[],j位置上的segment,并判断是否为空
if ((s = (Segment<K,V>)UNSAFE.getObject
(segments, (j << SSHIFT) + SBASE)) == null)
//如果为空,则初始化j位置上的segment
s = ensureSegment(j);
//调用segment的put方法
return s.put(key, hash, value, false);
}
private Segment<K,V> ensureSegment(int k) {
//赋值segment[]
final Segment<K,V>[] ss = this.segments;
//获取k所在segment在内存中的偏移量(类似地址值)
long u = (k << SSHIFT) + SBASE;
Segment<K,V> seg;
//第一次判断segment[k]位置是否为空
if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
//用segment[0]的元素作为模板
//segment[0]在初始化concurrenthashmap时已经初始化
Segment<K,V> proto = ss[0];
int cap = proto.table.length;
float lf = proto.loadFactor;
int threshold = (int)(cap * lf);
//初始化一个HashEntry数组,大小和Segments[0]中的HashEntry一样
HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
//第二次判断segment[k]位置是否为空,防止第一次判断和第二次判断中间这段时间其他线程初始化
//类似于懒汉单例模式
if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
== null) { // recheck
//如果两次检车segment为空
Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
//通过UNSAFE操作CAS把segment[k]位置上期望为null,设置为初始化好的seg
while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
== null) {
if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
break;
}
}
}
return seg;
}
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
//这里是分段锁思想的实现,要想操作segment先加锁
//reentrantlock的trylock()方法,尝试获取锁,获取锁失败不会阻塞,返回加锁是否成功
//tryLock()返回false就调用scanAndLockForPut()自旋加锁知道最大加锁次数(64),并在自旋的间隙初始化要插入的hashentry
HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value);
V oldValue;
try {
HashEntry<K,V>[] tab = table;
//使用hash值计算一次在hashentry[]数组中的位置
int index = (tab.length - 1) & hash;
//获取hashentry[index]位置的头节点
HashEntry<K,V> first = entryAt(tab, index);
//从头结点遍历链表
for (HashEntry<K,V> e = first;;) {
//从头节点开始判断hashentry链表上节点是不是null
if (e != null) {
K k;
//节点不是null,判断是否hash冲突
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
//如果冲突了新值覆盖旧值,返回旧值
oldValue = e.value;
//默认false
if (!onlyIfAbsent) {
//新值覆盖旧值
e.value = value;
++modCount;
}
break;
}
//链表的下一个元素
e = e.next;
}
//代码有两种可能执行到这里
//1.hashentry[index]位置上的链表是空的,即还没有元素插入过
//2.遍历完链表没有找到hash冲突的节点
else {
//如果scanAndLockForPut()方法中初始化了node
if (node != null)
//头插法把hashentry节点插入hashentry[]
node.setNext(first);
else
//反之初始化一个hashentry
node = new HashEntry<K,V>(hash, key, value, first);
//当前segment中的k-v对总数+1
int c = count + 1;
//如果总数>阈值并且当前segment中hashentry的长度小于最大值(2的30次方)
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
//触发扩容
rehash(node);
else
//头插法插入hashentry
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
//finally中释放锁,和reentrantlock用法一样
unlock();
}
return oldValue;
}
rivate HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
//获取当前segment中hashentry[]hash位置的头结点
HashEntry<K,V> first = entryForHash(this, hash);
HashEntry<K,V> e = first;
HashEntry<K,V> node = null;
int retries = -1;
//尝试获取k所在segment的锁。成功就直接返回、失败进入while循环进行自旋并初始化hashentry
//trylock
while (!tryLock()) {
//尝试获取k所在segment的锁。成功就直接返回、失败进入while循环进行自旋尝试获取锁
//trylock失败了间隙初始化hashentry,如果一直while(true)循环下去很占用cpu资源的
HashEntry<K,V> f;
if (retries < 0) {
//如果first头节点为空
if (e == null) {
//并且node也没有被初始化
if (node == null)
//那么初始化node
node = new HashEntry<K,V>(hash, key, value, null);
//重试次数设置为0
retries = 0;
//如果重试过程中找到了相同的key,那么只需要替换v就行
} else if (key.equals(e.key))
//重试次数设置为0
retries = 0;
else
e = e.next;
//如果重试次数>64(多核cpu为64,单核为1)
} else if (++retries > MAX_SCAN_RETRIES) {
//就使用带有阻塞的加锁了
lock();
break;
//偶数 & 1 == 0
//如果retries 为偶数并且现在的头结点和刚才的头节点不一样(被别的获取到锁的线程修改了)
} else if ((retries & 1) == 0 &&
(f = entryForHash(this, hash)) != first) {
//重新获取头节点
e = first = f;
retries = -1;
}
}
return node;
}
扩容的逻辑很有意思,先别急着看代码,画图演示一下
比如旧的segment[1]位置上的hashentry[]要扩容,元素扩容后再新数组的位置相对于就数组是不变的
然后会找到和头节点在扩容后的数组位置不同的最后一个元素,如果有多个那么取第一个
把他移动到新的数组,旧数组剩下的节点从上向下遍历头插到新数组的1或者3位置上
看代码
private void rehash(HashEntry<K,V> node) {
HashEntry<K,V>[] oldTable = table;
int oldCapacity = oldTable.length;
//新数组的长度是就数组的2倍不解释
int newCapacity = oldCapacity << 1;
//新数组阈值
threshold = (int)(newCapacity * loadFactor);
//初始化新数组
HashEntry<K,V>[] newTable =
(HashEntry<K,V>[]) new HashEntry[newCapacity];
int sizeMask = newCapacity - 1;
for (int i = 0; i < oldCapacity ; i++) {
//旧数组[i]位置的头结点
HashEntry<K,V> e = oldTable[i];
//头节点不为空才会执行后边的代码
if (e != null) {
//头结点的下一个节点
HashEntry<K,V> next = e.next;
//计算头节点在新数组的索引
int idx = e.hash & sizeMask;
//如果头节点的下一个节点为空
if (next == null)
//直接就把头节点移动到新数组了
newTable[idx] = e;
else {
//如果头结点的下一个节点不为空
//lastRun和lastIdx存储的就是和头节点在扩容后的数组位置不同的最后一个元素,如果有多个那么取第一个的节点和其在新数组的索引
HashEntry<K,V> lastRun = e;
int lastIdx = idx;
//循环去找那个节点
for (HashEntry<K,V> last = next;
last != null;
last = last.next) {
int k = last.hash & sizeMask;
if (k != lastIdx) {
lastIdx = k;
lastRun = last;
}
}
//把它转移
newTable[lastIdx] = lastRun;
//旧数组剩下的节点遍历并头插到新数组
for (HashEntry<K,V> p = e; p != lastRun; p = p.next) {
int h = p.hash;
int k = h & sizeMask;
HashEntry<K,V> n = newTable[k];
newTable[k] = new HashEntry<K,V>(h, p.key, p.value, n);
}
}
}
}
//put()中的那个node头插到新数组
int nodeIndex = node.hash & sizeMask;
node.setNext(newTable[nodeIndex]);
newTable[nodeIndex] = node;
//segment中保存新数组
table = newTable;
}
public V get(Object key) {
Segment<K,V> s;
HashEntry<K,V>[] tab;
//计算k的hash
int h = hash(key);
//计算h值存储所在segments[]中内存偏移量
long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
//通过Unsafe中的getObjectVolatile方法进行volatile语义的读,获取到segments[]在偏移量为u位置的segment,且此segment中的hashentry[]不为空
if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
(tab = s.table) != null) {
//获取h对应的segment[]中的segment中hashentry[]的头结点,然后对链表进行遍历
for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
(tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
e != null; e = e.next) {
K k;
//k相同
if ((k = e.key) == key || (e.hash == h && key.equals(k))) //返回v
return e.value;
}
}
return null;
}
jdk1.8中的hashmap和1.7相比数据结构变为数组+单链表+红黑树/双向链表;线程依然不安全.
/**
* 就是1.7中的entry[],改名字了
*/
transient Node<K,V>[] table;
/**
- 默认初始化Node[]容量16
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**
*node[]最大长度2的30次方
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* 默认加载因子0.75f
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* node[]的某个节点的单链表在超过8(第9个元素到来)put的时候会树化成红黑树
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* 由红黑树转链表的临界值,扩容后的红黑树有可能被分成高低位,树的容量小于6退化成链表
*/
static final int UNTREEIFY_THRESHOLD = 6;
/**
* 桶可能被转化为树形结构的最小容量的临界值
* 解释一下:如果node[]的某个节点是链表结构,当这个链表结构达到8以上时会判断如果node[]的长度小于64会触发扩容即node[]的长度原长度的2倍
* 当链表结构达到8以上时触发树化,再判断node[]长度大于等于64则树化.
*/
static final int MIN_TREEIFY_CAPACITY = 64;
/**
* map中k-v对(node)节点的总数
*/
transient int size;
/**
* 计数器,记录map总的修改次数
*/
transient int modCount;
/**
* 阈值,k-v对(node节点)的总数大于阈值会触发扩容
*/
int threshold;
/**
* 加载因子
*/
final float loadFactor;
/**
* 链表节点, 继承自Entry
*/
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
}
/**
* 红黑树节点
*/
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent;
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev;
boolean red;
}
//空参构造threshold 为0,loadFactor为0.75f
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; fields defaulted
}
//loadFactor默认0.75f
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//initialCapacity 最大值2的30次方
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
// Float的isNaN()方法放回false
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
//获取大于传入初始化长度最小的2的幂,比如传入10返回16最为node[]初始长度
this.threshold = tableSizeFor(initialCapacity);
}
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
//1.8计算hash的方法比1.7简略
static final int hash(Object key) {
int h;
//注意这个三元,key为nullhash是0,put元素计算数组下标是与运算,0和任何数相与都是0,所以key为null也存放在数组的0号位置
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
//第一次put node[]为空,初始化node[],长度为16
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
//node[i]位置如果还没有元素直接放
tab[i] = newNode(hash, key, value, null);
else {
//node[i]位置有元素了
Node<K,V> e; K k;
//判断node[i]位置的元素的hash值和key值是否相等
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//相等的话则用e来进行记录
e = p;
//判断node[i]位置是否是红黑树
else if (p instanceof TreeNode)
//是的话则将元素添加到树节点上
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//如果hashCode一样的两个不同Key就会以链表的形式保存
else {
for (int binCount = 0; ; ++binCount) {
//循环判断该链表尾部指针是不是空的
if ((e = p.next) == null) {
//在链表的尾部创建链表节点
p.next = newNode(hash, key, value, null);
//判断链表的长度是否达到转化红黑树的临界值,即链表已经存在8个元素,put第9个元素是链表树化成红黑树
if (binCount >= TREEIFY_THRESHOLD - 1)
//链表树化
treeifyBin(tab, hash);
break;
}
//判断链表中的节点是否与该节点相等
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//判断当前的key已经存在的情况下,新值覆盖旧值,并返回旧值
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
//linkedhashmap重写,此处忽略
afterNodeAccess(e);
return oldValue;
}
}
//操作次数加1
++modCount;
//map中k-v对的总数超过阈值那么扩容
if (++size > threshold)
resize();
//linkedhashmap重写,此处忽略
afterNodeInsertion(evict);
return null;
}
//第一次put时会初始化node[]
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//这里有两种情况
//空参构造threshold时0
//有参数构造时threshold为大于传入初始化长度最小的2的幂
//初始化时,如果是有参构造的map那么threshold保存的会是初始化数组的长度;初始化完再扩容时,threshold保存的是node[]的阈值
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) { //初始化时为0,这是扩容逻辑
//省略无关代码
}
else if (oldThr > 0)
//使用有参构造时,node[]初始容量为threshold
newCap = oldThr;
else {
//使用空参构造时
//初始化node[]长度为16,阈值为12
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;
//初始化node[]
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
//省略无关代码
}
return newTab;
}
/**
* 红黑树的put操作,红黑树插入会同时维护双向链表(prev,next);
* 链表转成树化时先转成双向链表再转成红黑树
*/
final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
int h, K k, V v) {
Class<?> kc = null;
boolean searched = false;
// 获取树的根节点
TreeNode<K,V> root = (parent != null) ? root() : this;
// 从根节点开始遍历
for (TreeNode<K,V> p = root;;) {
int dir, ph; K pk;
// 如果传入的hash值小于p节点的hash值,将dir赋值为-1,代表向p的左边查找树
if ((ph = p.hash) > h)
dir = -1;
// 如果传入的hash值大于p节点的hash值, 将dir赋值为1,代表向p的右边查找树
else if (ph < h)
dir = 1;
// 如果传入的hash值和key值等于p节点的hash值和key值, 则p节点即为目标节点, 返回p节点
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
//如果k的类没有实现Comparable接口 或者 k和p节点的key相等
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0) {
// 第一次符合条件, 从p节点的左节点和右节点分别调用find方法进行查找, 如果查找到目标节点则返回
if (!searched) {
TreeNode<K,V> q, ch;
searched = true;
if (((ch = p.left) != null &&
(q = ch.find(h, k, kc)) != null) ||
((ch = p.right) != null &&
(q = ch.find(h, k, kc)) != null))
return q;
}
// 否则使用定义的一套规则来比较k和p节点的key的大小, 用来决定向左还是向右查找
dir = tieBreakOrder(k, pk); // dir<0则代表k
}
TreeNode<K,V> xp = p; // xp赋值为x的父节点,中间变量,用于下面给x的父节点赋值
// dir<=0则向p左边查找,否则向p右边查找,如果为null,则代表该位置即为x的目标位置
if ((p = (dir <= 0) ? p.left : p.right) == null) {
// xp的next节点
Node<K,V> xpn = xp.next;
// 创建新的节点, 其中x的next节点为xpn, 即将x节点插入xp与xpn之间
TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
//将x插入到红黑树的叶子节点
// 如果时dir <= 0, 则代表x节点为xp的左孩子节点
if (dir <= 0)
xp.left = x;
else // 如果时dir> 0, 则代表x节点为xp的右孩子节点
xp.right = x;
//维护双向链表
// 将xp的next节点设置为x
xp.next = x;
// 将x的parent和prev节点设置为xp
x.parent = x.prev = xp;
// 如果xpn不为空,则将xpn的prev节点设置为x节点,与上文的x节点的next节点对应
if (xpn != null)
((TreeNode<K,V>)xpn).prev = x;
// 进行红黑树的插入平衡调整
moveRootToFront(tab, balanceInsertion(root, x));
return null;
}
}
/**
* node单链表先转成treenode双向链表再树化成红黑树
*/
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
// 如果node[]为空或者node[]的长度小于64, 调用resize方法进行扩容
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
// 从node[i]位置头节点开始遍历
else if ((e = tab[index = (n - 1) & hash]) != null) {
//hd是双向链表的头结点,tl是双向链表的尾结点
TreeNode<K,V> hd = null, tl = null;
do {
// node转化成treenode
TreeNode<K,V> p = replacementTreeNode(e, null);
// 如果是第一次遍历,将头节点赋值给hd
if (tl == null) // tl为空代表为第一次循环
hd = p;
else {
// 如果不是第一次遍历,构建双向链表
p.prev = tl; // 当前节点的prev属性设为上一个节点
tl.next = p; // 上一个节点的next属性设置为当前节点
}
//将p节点赋值给tl,用于在下一次循环中作为上一个节点进行一些双向链表的关联操作(p.prev = tl 和 tl.next = p)
tl = p;
} while ((e = e.next) != null);
//将node[i]赋值为新转的TreeNode的头节点,如果该节点不为空,则以头节点(hd)为根节点, 构建红黑树
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
/**
* 双向链表转成红黑树
*/
final void treeify(Node<K,V>[] tab) {
TreeNode<K,V> root = null;
// 从双向链表的头结点开始遍历
for (TreeNode<K,V> x = this, next; x != null; x = next) {
next = (TreeNode<K,V>)x.next; // next为x的下一个节点
x.left = x.right = null; // 将x的左右节点设置为空
// 如果没有根节点, 先将双向链表头结点为根节点
if (root == null) {
x.parent = null; // 根节点没有父节点
x.red = false; // 根节点为黑色
root = x; // 将x设置为根节点
}
else {
K k = x.key;
int h = x.hash;
Class<?> kc = null;
// 如果当前节点x不是根节点, 则从根节点开始查找属于该节点的位置
for (TreeNode<K,V> p = root;;) {
int dir, ph;
K pk = p.key;
// 如果x节点的hash值小于p节点的hash值,则将dir赋值为-1, 代表向p的左边查找
if ((ph = p.hash) > h)
dir = -1;
// 如果x节点的hash值大于p节点的hash值,则将dir赋值为1, 代表向p的右边查找
else if (ph < h)
dir = 1;
// 走到这代表x的hash值和p的hash值相等,则比较key值
else if ((kc == null && // 如果k没有实现Comparable接口 或者 x节点的key和p节点的key相等
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
//使用定义的一套规则来比较x节点和p节点的大小,用来决定向左还是向右查找
dir = tieBreakOrder(k, pk);
TreeNode<K,V> xp = p; // xp赋值为x的父节点,中间变量用于下面给x的父节点赋值
// 则向p左边查找,否则向p右边查找,如果为null,则代表该位置即为x的目标位置,并给p赋值
if ((p = (dir <= 0) ? p.left : p.right) == null) {
// x和xp节点的属性设置
x.parent = xp; // x的父节点即为最后一次遍历的p节点
if (dir <= 0) // 如果时dir <= 0, 则代表x节点为父节点的左节点
xp.left = x;
else // 如果时dir > 0, 则代表x节点为父节点的右节点
xp.right = x;
// 进行红黑树的插入平衡(通过左旋、右旋和改变节点颜色来保证当前树符合红黑树的要求)
root = balanceInsertion(root, x);
break;
}
}
}
}
// 如果root节点不在table索引位置的头节点, 则将其调整为头节点
moveRootToFront(tab, root);
}
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
// 如果旧的node[]>0
if (oldCap > 0) {
// 如果原来的node[]容量大于最大值(2的30次方)
//阈值为Integer最大值
//返回旧node[]
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 将新node[]赋值为旧node[]的2倍,如果新node[]小于最大容量并且旧node[]容量大于等于16, 则将新阈值设置为原来的两倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
// 如果旧node[]的容量为0, 旧node[]的阈值大于0, 是因为初始容量被放入阈值,则将新node[]的容量设置为旧node[]的阈值
else if (oldThr > 0)
newCap = oldThr;
else {
// 旧node[]容量和阈值都为0,是初始化逻辑
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 如果新新node[]的阈值为空, 则通过新的容量*负载因子获得阈值
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
// 将当前阈值设置为刚计算出来的新的阈值,定义新node[],容量为刚计算出来的新容量,将table设置为新定义的node[]。
threshold = newThr;
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
// 如果旧node[]不为空
if (oldTab != null) {
//遍历旧node[]
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
//判断旧node[j]位置上不为空,并赋值给e
if ((e = oldTab[j]) != null) {
oldTab[j] = null; //help gc
// 如果e.next为空, 则代表旧node[j]位置只有1个节点,计算新表的索引位置, 直接将该节点放在该位置
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
// 如果是红黑树节点,则进行红黑树的拆分移动(跟链表的hash分布基本相同)
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else {
// 如果是普通的链表节点,则进行普通的重hash分布
Node<K,V> loHead = null, loTail = null; // 存储索引位置为:“原索引位置”的节点
Node<K,V> hiHead = null, hiTail = null; // 存储索引位置为:“原索引位置+oldCap”的节点
Node<K,V> next;
do {
next = e.next;
// 如果e的hash值与旧node[]的容量进行与运算为0,则扩容后在新node[]的索引位置跟旧node[]的索引位置一样
if ((e.hash & oldCap) == 0) {
if (loTail == null) // 如果loTail为空, 代表该节点为第一个节点
loHead = e; // 则将loHead赋值为第一个节点
else
loTail.next = e; // 否则将节点添加在loTail后面
loTail = e; // 并将loTail赋值为新增的节点
}
// 如果e的hash值与旧node[]的容量进行与运算不为0,则扩容后的索引位置为:旧node[]的索引位置+oldCap
else {
if (hiTail == null) // 如果hiTail为空, 代表该节点为第一个节点
hiHead = e; // 则将hiHead赋值为第一个节点
else
hiTail.next = e; // 否则将节点添加在hiTail后面
hiTail = e; // 并将hiTail赋值为新增的节点
}
} while ((e = next) != null);
// 如果loTail不为空(说明旧node[]的数据有分布到新node[]上“原索引位置”的节点),
// 则将最后一个节点的next设为空,并将新node[]上索引位置为“原索引位置”的节点设置为对应的头节点
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
// 如果hiTail不为空(说明旧node[]的数据有分布到新表上“原索引+oldCap位置”的节点),
// 则将最后一个节点的next设为空,并将新node[]上索引位置为“原索引+oldCap”的节点设置为对应的头节点
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
/**
* 扩容后,拆分红黑树,红黑树的hash分布,只可能存在于两个位置:原索引位置、原索引位置+oldCap
* @param Node[] tab是新的node[]
* @param int index : 旧node[]索引位置
* @param int bit : 旧node[]长度
*/
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
TreeNode<K,V> b = this; // 拿到调用此方法的树节点
TreeNode<K,V> loHead = null, loTail = null; // 存储索引位置为:“原索引位置”的节点
TreeNode<K,V> hiHead = null, hiTail = null; // 存储索引位置为:“原索引+oldCap”的节点
int lc = 0, hc = 0;
// 以调用此方法的节点开始,遍历整个红黑树节点
for (TreeNode<K,V> e = b, next; e != null; e = next) { // 从b节点开始遍历
next = (TreeNode<K,V>)e.next; // next赋值为e的下个节点
e.next = null; //help gc
// 如果e的hash值与旧node[]的容量进行与运算为0,则扩容后的索引位置跟旧node[]的索引位置一样
if ((e.hash & bit) == 0) {
if ((e.prev = loTail) == null) // 如果loTail为空, 代表该节点为第一个节点
loHead = e; // 则将loHead赋值为第一个节点
else
loTail.next = e; // 否则将节点添加在loTail后面
loTail = e; // 并将loTail赋值为新增的节点
++lc; // 统计原索引位置的节点个数
}
// 如果e的hash值与老表的容量进行与运算不为0,则扩容后的索引位置为:旧node[]的索引位置+旧node[]的容量
else {
if ((e.prev = hiTail) == null) // 如果hiHead为空, 代表该节点为第一个节点
hiHead = e; // 则将hiHead赋值为第一个节点
else
hiTail.next = e; // 否则将节点添加在hiTail后面
hiTail = e; // 并将hiTail赋值为新增的节点
++hc; // 统计索引位置为原索引+oldCap的节点个数
}
}
// 如果原索引位置的节点不为空
if (loHead != null) { // 原索引位置的节点不为空
//如果节点个数<=6个则将红黑树转为链表结构
if (lc <= UNTREEIFY_THRESHOLD)
tab[index] = loHead.untreeify(map);
else {
// 将原索引位置的节点设置为对应的头节点,转移树结构
tab[index] = loHead;
// 如果hiHead不为空,则代表原来的红黑树
// 已经被改变, 需要重新构建新的红黑树
if (hiHead != null)
// 以loHead为根节点, 构建新的红黑树
loHead.treeify(tab);
}
}
//如果索引位置为原索引+oldCap的节点不为空
if (hiHead != null) { // 索引位置为原索引+oldCap的节点不为空
// 如果节点个数<=6个则将红黑树转为链表结构
if (hc <= UNTREEIFY_THRESHOLD)
tab[index + bit] = hiHead.untreeify(map);
else {
// 将索引位置为原索引+oldCap的节点设置为对应的头节点
tab[index + bit] = hiHead;
// loHead不为空则代表原来的红黑树
// 已经被改变, 需要重新构建新的红黑树
if (loHead != null)
// 以hiHead为根节点, 构建新的红黑树
hiHead.treeify(tab);
}
}
}
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;
//node[]不为空 && node[]长度大于0 &&
//node[]索引位置(使用table.length - 1和hash值进行位与运算)的节点不为空
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 检查node[]索引位置头节点的hash值和key是否和入参的一样,如果一样则first即为目标节点,直接返回first节点
if (first.hash == hash &&
((k = first.key) == key || (key != null && key.equals(k))))
return first;
// 如果first不是目标节点,并且first的next节点不为空则继续遍历
if ((e = first.next) != null) {
if (first instanceof TreeNode)
//如果是红黑树节点,则调用红黑树的查找目标节点方法getTreeNode
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
// 执行链表节点的查找,向下遍历链表, 直至找到节点的key和入参的key相等时,返回该节点
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
// 找不到符合的返回空
return null;
}
final TreeNode<K,V> getTreeNode(int h, Object k) {
// 首先找到红黑树的根节点;使用根节点调用find方法
return ((parent != null) ? root() : this).find(h, k, null);
final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
// 第一次执行this是红黑树的根节点
TreeNode<K,V> p = this;
do {
int ph, dir; K pk;
TreeNode<K,V> pl = p.left, pr = p.right, q;
// 如果传入的hash值小于p节点的hash值,则往p节点的左边遍历
if ((ph = p.hash) > h)
p = pl;
else if (ph < h) // 如果传入的hash值大于p节点的hash值,则往p节点的右边遍历
p = pr;
// 如果传入的hash值和key值等于p节点的hash值和key值,则p节点为目标节点,返回p节点
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
else if (pl == null) // p节点的左节点为空则将向右遍历
p = pr;
else if (pr == null) // p节点的右节点为空则向左遍历
p = pl;
// 将p节点与k进行比较
else if ((kc != null ||
(kc = comparableClassFor(k)) != null) && // kc不为空代表k实现了Comparable
(dir = compareComparables(kc, k, pk)) != 0)// kpk则dir>0
// k
p = (dir < 0) ? pl : pr;
// 代码走到此处, 代表key所属类没有实现Comparable, 直接指定向p的右边遍历
else if ((q = pr.find(h, k, kc)) != null)
return q;
// 代码走到此处递归“pr.find(h, k, kc)”为空, 因此直接向左遍历
else
p = pl;
} while (p != null);
return null;
}
/**
* 移除某个节点
*/
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;
// 如果node[]不为空并且根据hash值计算出来的索引位置不为空, 将该位置的节点赋值给p
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;
// 如果p的hash值和key都与入参的相同, 则p即为目标节点, 赋值给node
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
else if ((e = p.next) != null) {
// 否则将p.next赋值给e,向下遍历节点
//如果p是TreeNode则调用红黑树的方法查找节点
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
// 否则,进行普通链表节点的查找
do {
// 当节点的hash值和key与传入的相同,则该节点即为目标节点
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e; // 赋值给node, 并跳出循环
break;
}
p = e; // p节点赋值为本次结束的e,在下一次循环中,e为p的next节点
} while ((e = e.next) != null); // e指向下一个节点
}
}
// 如果node不为空(即根据传入key和hash值查找到目标节点),则进行移除操作
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
// 如果是TreeNode则调用红黑树的移除方法
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
// 如果node是该索引位置的头节点则直接将该索引位置的值赋值为node的next节点,
// “node == p”只会出现在node是头节点的时候,如果node不是头节点,则node为p的next节点
else if (node == p)
tab[index] = node.next;
// 否则将node的上一个节点的next属性设置为node的next节点,
// 即将node节点移除, 将node的上下节点进行关联(链表的移除)
else
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node); // 供LinkedHashMap使用
// 返回被移除的节点
return node;
}
}
return null;
}
/**
* map中node[]的最大长度
*/
private static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* map的默认初始容量
*/
private static final int DEFAULT_CAPACITY = 16;
/**
* node[]的某个节点的单链表在超过8(第9个元素到来)put的时候会树化成红黑树
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* 由红黑树转链表的临界值,扩容后的红黑树有可能被分成高低位,树的容量小于6退化成链表
*/
static final int UNTREEIFY_THRESHOLD = 6;
/**
* 桶可能被转化为树形结构的最小容量的临界值
* 如果node[]的某个节点是链表结构,当这个链表结构达到8以上时会判断如果node[]的长度小于64会触发扩容即node[]的长度原长度的2倍
* 当链表结构达到8以上时触发树化,再判断node[]长度大于等于64则树化.
*/
static final int MIN_TREEIFY_CAPACITY = 64;
/**
* map中k-v对(node)结点的总数
*/
transient int size;
/**
* 表示有线程在进行扩容
*/
static final int MOVED = -1;
/**
* node[i]位置上是树结点
*/
static final int TREEBIN = -2;
/**
* JDK8的ConcurrentHashMap比HashMap多了一个TreeBin类
* 用来表示node[i]这个位置是红黑树,
* 存储红黑树的根节点和头节点,不存储hash值,key和value.
*/
static final class TreeBin<K,V> extends Node<K,V> {
TreeNode<K,V> root;
volatile TreeNode<K,V> first;
volatile Thread waiter;
volatile int lockState;
static final int WRITER = 1; // set while holding write lock
static final int WAITER = 2; // set when waiting for write lock
static final int READER = 4; // increment value for setting read lock
}
//空参构造
public ConcurrentHashMap() {
}
//有参构造
public ConcurrentHashMap(int initialCapacity) {
//如果传进来的参数小于0抛异常
if (initialCapacity < 0)
throw new IllegalArgumentException();
//计算sizeCtl =(1.5 * initialCapacity + 1),然后向上取最近的 2 的 n 次方 ,sizeCtl作为node[]初始容量
//如 initialCapacity 为 10,那么得到 sizeCtl 为 16,
//如果 initialCapacity 为 11,得到 sizeCtl 为 32。
int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
MAXIMUM_CAPACITY :
tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
this.sizeCtl = cap;
}
/**
* 假设initialCapacity 为13,那么c为20
* 13+6(13>>>1 = 6)+1 = 20
* int n = c - 1; 0001 0011
* ---
* n |= n >>> 1; 0000 1001
* 0001 1011
* ---
* n |= n >>> 2; 0000 0110
* 0001 1111
* ---
* n |= n >>> 4; 0000 0001
* 0001 1111
* n |= n >>> 8;
* n |= n >>> 16; //得出的结果是31
*/
private static final int tableSizeFor(int c) {
int n = c - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
//判断后31+1=32
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
/*
* 当添加一对键值对的时候,首先会去判断保存这些键值对的数组是不是初始化了,
* 如果没有的话就初始化数组
* 然后通过计算hash值来确定放在数组的哪个位置
* 如果这个位置为空则直接添加
* 如果取出来的节点的hash值是MOVED(-1)的话,则表示当前有其他线程正在对这个数组进行扩容,转移到新的数组,则当前线程也去帮助扩容
* 最后一种情况就是,如果这个节点,不为空,也不在扩容,则通过synchronized来加锁,进行添加操作
* 然后判断当前取出的节点位置存放的是链表还是树
* 如果是链表的话,则遍历整个链表,直到取出来的节点的key来个要放的key进行比较,如果key相等,并且key的hash值也相等的话,
* 则说明是同一个key,则覆盖掉value,否则的话则添加到链表的末尾
* 如果是树的话,则调用putTreeVal方法把这个元素添加到树中去
* 最后在添加完成之后,会判断在该节点处共有多少个节点(注意是添加前的个数),如果达到8个以上(第9个元素来put的时候)的话,
* 则调用treeifyBin方法来尝试将处的链表转为树,或者扩容数组至数组长度大于等于64
*/
public V put(K key, V value) {
return putVal(key, value, false);
}
final V putVal(K key, V value, boolean onlyIfAbsent) {
//ConcurrentHashMap的key和value不能为空,hashmap则可以为空
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;
// 如果数组"空",进行数组初始化
if (tab == null || (n = tab.length) == 0)
// 初始化数组
tab = initTable();
// 找该 hash 值对应的数组下标,得到第一个节点 f
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
//如果这个位置没有元素的话,则通过cas的方式尝试添加,注意这个时候是没有加锁的,这个 put 操作差不多就结束了,可以拉到最后面了
// 如果 CAS 失败,那就是有并发操作,进到下一个循环就好了
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
//创建一个Node添加到数组中区,null表示的是下一个节点为空
break;
}
//如果检测到某个节点的hash值是MOVED(-1),则表示有线程正在对数组进行扩容
//第一个来扩容线程要扩容完所有位置(类似饿汉模式),其他的线程可以来帮忙扩容.
else if ((fh = f.hash) == MOVED)
// 其他线程帮助正在扩容的线程扩容
tab = helpTransfer(tab, f);
else {
//如果在这个位置有元素的话,就采用synchronized的方式加锁,
//如果是链表的话(hash大于0),就对这个链表的所有元素进行遍历,
//如果找到了key和key的hash值都一样的节点,则把它的值替换到
//如果没找到的话,则添加在链表的最后面
//否则,是树的话,则调用putTreeVal方法添加到树中去
// 在添加完之后,会对该节点上关联的的数目进行判断,
//如果在8个以上的话,则会调用treeifyBin方法,来尝试转化为树,或者(数组长度大于等于64)是扩容
V oldVal = null;
// 锁对象是数组在该位置的头结点
synchronized (f) {
if (tabAt(tab, i) == f) {//再次取出要存储的位置的元素,跟前面取出来的比较
if (fh >= 0) {//取出来的元素的hash值大于0说明是链表,当转换为树(TreeBin)之后,hash值为-2
// 用于累加,记录链表的长度
binCount = 1;
// 遍历链表
for (Node<K,V> e = f;; ++binCount) {
K ek;
//要存的元素的hash,key跟要存储的位置的节点的相同的时候,替换掉该节点的value即可,然后也就可以 break 了
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
//当使用putIfAbsent的时候,只有在这个key没有设置值得时候才设置
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;
//调用putTreeVal方法,将该元素添加到树中去
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(第9个元素put时)
if (binCount >= TREEIFY_THRESHOLD)
//链表树化
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
//每次添加元素后,键值对数量加1,并判断是否达到扩容门槛,达到了则进行扩容或协助扩容。
addCount(1L, binCount);
return null;
}
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();
// CAS将 sizeCtl 设置为 -1,代表抢到了锁
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {//如果CAS成功
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
sizeCtl = sc;
}
break;
}
}
return tab;
}
private final void treeifyBin(Node<K,V>[] tab, int index) {
Node<K,V> b; int n, sc;
if (tab != null) {
// MIN_TREEIFY_CAPACITY 为 64
// 所以,如果数组长度小于 64 的时候会对node[]扩容
if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
//node数组长度扩容成原来2倍
tryPresize(n << 1);
// b 是头结点
else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
// 对头结点加锁
synchronized (b) {
//再次检查b是否改变
if (tabAt(tab, index) == b) {
//和JDK8的hashmap,遍历单链表生产双向链表
//hd:双向链表头结点 ld:双向链表为结点
TreeNode<K,V> hd = null, tl = null;
for (Node<K,V> e = b; e != null; e = e.next) {
TreeNode<K,V> p =
new TreeNode<K,V>(e.hash, e.key, e.val,
null, null);
if ((p.prev = tl) == null)
hd = p;
else
tl.next = p;
tl = p;
}
//把TreeNode的链表放入容器TreeBin中
setTabAt(tab, index, new TreeBin<K,V>(hd));
}
}
}
}
}
/**
* 扩容表为指可以容纳指定个数的大小(总是2的N次方)
* 假设原来的数组长度为16,则在调用tryPresize的时候,size参数的值为16<<1(32),此时sizeCtl的值为12
*/
private final void tryPresize(int size) {
/*
* MAXIMUM_CAPACITY = 1 << 30
* 如果给定的大小大于等于数组容量的一半,则直接使用最大容量,
* 否则使用tableSizeFor计算
* 后面table一直要扩容到sizeCtrl(数组长度的3/4)大于等于这个值才退出扩容
*/
int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
tableSizeFor(size + (size >>> 1) + 1);
int sc;
while ((sc = sizeCtl) >= 0) {
Node<K,V>[] tab = table; int n;
/*
* 如果数组table还没有被初始化,则初始化一个大小为sizeCtrl和刚刚算出来的c中较大的一个大小
* 初始化的时候,设置sizeCtrl为-1,初始化完成之后把sizeCtrl设置为数组长度的3/4
* 为什么要在扩张的地方来初始化数组呢?这是因为如果第一次put的时候不是put单个元素,
* 而是调用putAll方法直接put一个map的话,在putALl方法中没有调用initTable方法去初始化table,
* 而是直接调用了tryPresize方法,所以这里需要做一个是不是需要初始化table的判断
*/
if (tab == null || (n = tab.length) == 0) {
n = (sc > c) ? sc : c;
if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { //初始化tab的时候,把sizeCtl设为-1
try {
if (table == tab) {
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = nt;
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
}
}
/*
* 一直扩容到c(一开始算的数组长度)小于等于sizeCtl或者数组长度大于最大长度的时候,则退出
* 所以在一次扩容之后,不是原来长度的两倍,而是2的n次方倍
*/
else if (c <= sc || n >= MAXIMUM_CAPACITY) {
break; //退出扩张
}
//再次检查
else if (tab == table) {
int rs = resizeStamp(n);
/*
* 如果正在扩容Table的话,则帮助扩容
* 否则的话,开始新的扩容
* 在transfer操作,将第一个参数的table中的元素,移动到第二个元素的table中去,
* 虽然此时第二个参数设置的是null,但是,在transfer方法中,当第二个参数为null的时候,
* 会创建一个两倍大小的table
*/
if (sc < 0) {
Node<K,V>[] nt;
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
/*
* transfer的线程数加一,该线程将进行transfer的帮忙
* 在transfer的时候,sc表示在transfer工作的线程数
*/
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
/*
* 没有在初始化或扩容,则开始扩容
*/
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2)) {
transfer(tab, null);
}
}
}
}
/**
* 把数组中的节点复制到新的数组的相同位置,或者移动到扩张部分的相同位置
* 在这里首先会计算一个步长,表示一个线程处理的数组长度,用来控制对CPU的使用,
* 每个CPU最少处理16个长度的数组元素,也就是说,如果一个数组的长度只有16,那只有一个线程会对其进行扩容的复制移动操作
* 扩容的时候会一直遍历,知道复制完所有节点,没处理一个节点的时候会在链表的头部设置一个fwd节点,这样其他线程就会跳过他,
* 复制后在新数组中的链表不是绝对的反序的
*/
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE) //MIN_TRANSFER_STRIDE是16 用来控制不要占用太多CPU
stride = MIN_TRANSFER_STRIDE;
/*
* 如果复制的目标nextTab为null的话,则初始化一个table两倍长的nextTab
* 此时nextTable被设置值了(在初始情况下是为null的)
* 因为如果有一个线程开始了对数组的扩容,其他线程也会进来帮忙扩张,
* 只有是第一个开始扩容的线程需要初始化新数组
*/
if (nextTab == null) {
try {
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
nextTab = nt;
} catch (Throwable ex) {
sizeCtl = Integer.MAX_VALUE;
return;
}
nextTable = nextTab;
transferIndex = n;
}
int nextn = nextTab.length;
/*
* 创建一个fwd节点,这个是用来控制并发的,当一个节点为空或已经被转移之后,就设置为fwd节点,
* fwd节点的hash是-1,其他线程put的时候发现数组这个位置的hash是-1则表示这个位置正在扩容会调用helpTransfer帮助扩容
* 这是一个空的标志节点
*/
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
boolean advance = true; //是否继续向前查找的标志位
boolean finishing = false; // 在完成之前重新在扫描一遍数组,看看有线程还在扩容中
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
while (advance) {
int nextIndex, nextBound;
if (--i >= bound || finishing) {
advance = false;
}
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
bound = nextBound;
i = nextIndex - 1;
advance = false;
}
}
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
if (finishing) { //已经完成转移
nextTable = null;
table = nextTab;
sizeCtl = (n << 1) - (n >>> 1); //设置sizeCtl为扩容后的0.75
return;
}
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT) {
return;
}
finishing = advance = true;
i = n; // recheck before commit
}
}
else if ((f = tabAt(tab, i)) == null) //数组中把null的元素设置为ForwardingNode节点(hash值为MOVED[-1])
advance = casTabAt(tab, i, null, fwd);
else if ((fh = f.hash) == MOVED)
advance = true; // already processed
else {
synchronized (f) { //加锁操作
if (tabAt(tab, i) == f) {
Node<K,V> ln, hn;
if (fh >= 0) { //该节点的hash值大于等于0,说明是一个Node节点,树结点的hash为-2
//链表的转移逻辑和ConcurrentHashMap1.7是相同的,不记得了返回去看看图
/*
* 因为n的值为数组的长度,且是power(2,x)的,所以,在&操作的结果只可能是0或者n
* 根据这个规则
* 0--> 放在新表的相同位置
* n--> 放在新表的(n+原来位置)
*/
int runBit = fh & n;
Node<K,V> lastRun = f;
/*
* lastRun 表示的是需要复制的最后一个节点
* 每当新节点的hash&n -> b 发生变化的时候,就把runBit设置为这个结果b
* 这样for循环之后,runBit的值就是最后不变的hash&n的值
* 而lastRun的值就是最后一次导致hash&n 发生变化的节点(假设为p节点)
* 为什么要这么做呢?因为p节点后面的节点的hash&n 值跟p节点是一样的,
* 所以在复制到新的table的时候,它肯定还是跟p节点在同一个位置
* 在复制完p节点之后,p节点的next节点还是指向它原来的节点,就不需要进行复制了,自己就被带过去了
* 这也就导致了一个问题就是复制后的链表的顺序并不一定是原来的倒序
*/
for (Node<K,V> p = f.next; p != null; p = p.next) {
int b = p.hash & n; //n的值为扩张前的数组的长度
if (b != runBit) {
runBit = b;
lastRun = p;
}
}
if (runBit == 0) {
ln = lastRun;
hn = null;
}
else {
hn = lastRun;
ln = null;
}
/*
* 构造两个链表,顺序大部分和原来是反的
* 分别放到原来的位置和新增加的长度的相同位置(i/n+i)
* 原数组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)
/*
* 假设runBit的值为0,
* 则第一次进入这个设置的时候相当于把旧的序列的最后一次发生hash变化的节点(该节点后面可能还有hash计算后同为0的节点)设置到旧的table的第一个hash计算后为0的节点下一个节点
* 并且把自己返回,然后在下次进来的时候把它自己设置为后面节点的下一个节点
*/
ln = new Node<K,V>(ph, pk, pv, ln);
else
/*
* 假设runBit的值不为0,
* 则第一次进入这个设置的时候相当于把旧的序列的最后一次发生hash变化的节点(该节点后面可能还有hash计算后同不为0的节点)设置到旧的table的第一个hash计算后不为0的节点下一个节点
* 并且把自己返回,然后在下次进来的时候把它自己设置为后面节点的下一个节点
*/
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) { //否则的话是一个树节点,
//扩容转移数据逻辑和HashMap1.8类似
//1.先分成两个高链和低链,
//2.判断是否小于6需要退化成链表
//3.用双向链表构建新的红黑树
//4.转移到新数组
//3.4 步和hashmap1.8相反
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;
}
}
/*
* 在复制完树节点之后,判断该节点处构成的树还有几个节点,
* 如果≤6个的话,就转回为一个单向链表
*/
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;
}
}
}
}
}
}
//f是node[i]的头结点
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
Node<K,V>[] nextTab; int sc;
// 如果 table不是空且node结点是转移类型,代表有线程正在扩容
// 且 node 结点的 nextTable(新 table) 不是空,
// 帮助扩容
if (tab != null && (f instanceof ForwardingNode) &&
(nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
// 根据 length 得到一个标识符号
int rs = resizeStamp(tab.length);
// 如果 nextTab 没有被并发修改 且 tab 也没有被并发修改
// 且 sizeCtl < 0,还有线程在扩容
while (nextTab == nextTable && table == tab &&
(sc = sizeCtl) < 0) {
//如果sc无符号右移16位不等于标识符(校验异常sizeCtl 变化了)
// 如果 sc == 标识符 + 1 (扩容结束了,不再有线程进行扩容)(默认第一个线程设置 sc ==rs 左移 16 位 + 2,当第一个线程结束扩容了,就会将 sc 减一。这个时候,sc 就等于 rs + 1)
// 或者 sizeCtl == rs + 65535 (如果达到最大帮助线程的数量,即 65535)
// 如果 transferIndex <= 0 (转移状态变化了)
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || transferIndex <= 0)
//结束循环
break;
// 如果以上都不是, 将 sizeCtl + 1, (表示增加了一个线程帮助其扩容)
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
// 进行转移
transfer(tab, nextTab);
// 结束循环
break;
}
}
return nextTab;
}
return table;
}
// 从 putVal 传入的参数是 1, binCount大小是链表的长度(如果不是红黑数结构的话)。
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
// 如果计数数组不是空 或者
// 如果修改 baseCount 失败
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
CounterCell a; long v; int m;
boolean uncontended = true;
// 如果计数数组是空(尚未出现并发)
// 如果随机取余一个数组位置为空 或者
// 修改这个槽位的变量失败(出现并发了)
// 执行 fullAddCount 方法。并结束
if (as == null || (m = as.length - 1) < 0 ||
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
!(uncontended =
U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
fullAddCount(x, uncontended);
return;
}
if (check <= 1)
return;
s = sumCount();
}
// 如果需要检查,检查是否需要扩容,在 putVal 方法调用时,默认就是要检查的。
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
// 如果map.size() 大于 sizeCtl(达到扩容阈值需要扩容) 且
// table 不是空;且 table 的长度小于 1 << 30。(可以扩容)
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
// 根据 length 得到一个标识
int rs = resizeStamp(n);
// 如果正在扩容
if (sc < 0) {
// 如果 sc 的低 16 位不等于 标识符(校验异常 sizeCtl 变化了)
// 如果 sc == 标识符 + 1 (扩容结束了,不再有线程进行扩容)(默认第一个线程设置 sc ==rs 左移 16 位 + 2,当第一个线程结束扩容了,就会将 sc 减一。这个时候,sc 就等于 rs + 1)
// 如果 sc == 标识符 + 65535(帮助线程数已经达到最大)
// 如果 nextTable == null(结束扩容了)
// 如果 transferIndex <= 0 (转移状态变化了)
// 结束循环
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
// 如果可以帮助扩容,那么将 sc 加 1. 表示多了一个线程在帮助扩容
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
// 扩容
transfer(tab, nt);
}
// 如果不在扩容,将 sc 更新:标识符左移 16 位 然后 + 2. 也就是变成一个负数。高 16 位是标识符,低 16 位初始是 2.
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
// 更新 sizeCtl 为负数后,开始扩容。
transfer(tab, null);
s = sumCount();
}
}
}
JDK7: 数组+链表 JDK8: 数组+链表+红黑树(JDK8中既使用了单向链表,也使用了双向链表,双向链表主要是为了链表操作方便,应该在插入,扩容,链表转红黑树,红黑树转链表的过程中都要操作链表)
当元素个数小于一个阈值时,链表整体的插入查询效率要高于红黑树;当元素个数大于此阈值时,链表整体的插入查询效率要低于红黑树.此阈值在HashMap中为8
这个题很容易答错,大部分答案就是:当链表中的元素个数大于8时就会把链表转化为红黑树.但是其实还有另外一个限制:当发现链表中的元素个数大于8之后,还会判断判断一下当前数组的长度,如果数组长度小于64时,此时并不会转化为红黑树,而是进行扩容.只有当链表中的元素个数大于8,并且数组的长度大于等于64时才会将链表转为红黑树.
树化前扩容的原因是,如果数组长度还比较小,就先利用扩容来缩小链表的长度.
1.根据key计算hashcode(位运算+或)
2.判断当前HashMap对象中的数组是否为空,如果为空则初始化该数组
3.根据与运算,算出hashcode基于当前数组对应的数组小标i
4.判断数组的第i个位置的元素(tab[i])是否为空
a.如果为空,则将key,value封装为Node对象赋值给tab[i]
b.如果不为空:
i:如果put方法传入进来的key等于tab[i]上的key,那么证明存在相同的key
ii.如果不等于tab[i]上的key,则:
1.如果tab[i]的类型是TreeNode,则表示数组的第i位置上是一颗红黑树,
那么将key和value插入到红黑树中,
并且在插入之前会判断在红黑树中是否存在相同的key
2.如果tab[i]的类型不是TreeNode则表示数组的第i位置上是一个链表,
那么遍历链表寻找是否存在相同的key,
并且在遍历的过程中会对链表中的结点数进行计数,
当遍历到最后一个结点时,会将key,value封装为Node插入到链表的尾部,
同时判断在插入新结点之前的链表结点个数是不是大于等于8,
如果是,则将链表改为红黑树.
iii.如果上述步骤中发现存在相同的key,则根据onlyIfAbsent标记判断是否需要更新value值,然后返回oldValue
5.modCount++
6.HashMap的元素个数size加1
7.如果size大于扩容的阈值,则进行扩容
1.根据key计算hashcode
2.如果数组为空,则直接返回空
3.如果数组不为空,则利用hashcode和数组长度(-1)通过逻辑与操作算出key所对应的数组下标i
4.如果数组的第i个位置上没有元素,则直接返回空
5.如果数组的第i个位置上的元素的key等于get方法所传进来的key,则返回该元素,并获取该元素的value
6.如果不等于则判断该元素还有没有下一个元素,如果没有,返回空
7.如果有则判断该元素的类型是链表结点还是红黑树结点
8.找到即返回结点,没找到则返回空
1.JDK8中使用了红黑树
2.JDK7中链表的插入使用的头插法
(扩容转移元素的时候也是使用的头插法,多线程扩容的情况下使用头插法会出现循环链表的问题,导致循环引用;
选择头插法不是因为快,而是根据时间局部性原理,最近插入的最有可能被使用,所以使用头插;
如果已经存在key,直接覆盖,不涉及头尾插法的效率问题;如果不存在,头尾插法都要遍历到最后一个节点,效率一致。)
JDK8中链表使用的尾插法
3.JDK7的Hash算法比JDK8复杂,Hash算法越复杂,生成的hashcode则更散列,那么hashmap中的元素则更散列,则hashmap性能更好,
jdk7中没有红黑树,所以只能优化Hash算法使得元素更散列,而JDK8中增加了红黑树,查询性能得到了保障,所以可以简化Hash算法,毕竟Hash算法越复杂CPU消耗越大
4.扩容的过程中JDK7中有可能会重新对key进行哈希(重新Hash跟哈希种子有关系),而JDK8中没有这部分逻辑
5.JDK8中扩容的条件和JDK7中不一样,除开判断size是否大于阈值之外,JDK7中还判断了tab[i]是否为空,不为空的时候才扩容,而JDK8中则没有该条件
6.JDK7和JDK8扩容过程中转移元素的逻辑不一样,JDK7是每次转移一个元素,JDK8是先算出来当前位置上哪些元素在新数组的低位上(i),哪些在新数组的高位上(i+n),然后在一次性转移
主要利用Unsafe操作+ReentrantLock+分段思想
主要使用了Unsafe操作中的:
1.compareAndSwapObject: 通过cas的方式修改对象的属性
2.putOrderedObject: 并发安全的给数组的某个位置赋值
3.getObjectVolatile: 并发安全的获取数组某个位置的元素
分段思想是为了提高ConcurrentHashMap的并发量,分段数越高则支持的最大并发量越高,我们可以通过concurrencyLevel参数来指定并发量.ConcurrentHashMap的内部类Segment就是用来表示某一个段的.
每个Segment就是一个小型的HashMap,当调用ConcurrentHashMap的put方法时,最终会调用到Segment的put方法,
而Segment类继承了ReentrantLock,所以Segment自带可重入锁,当调用到Segment的put方法时, 会先利用可重入锁加锁,加锁成功后再将待插入的key,value插入到小型的HashMap中,插入完成后解锁.
ConcurrentHashMap底层是由两层嵌套数组来实现的:
1.ConcurrentHashMap对象中有一个属性segments,类型为Segment[]
2.Segment对象中有一个属性table,类型为HashEntry[]
当调用ConcurrentHashMap的put方法时,先根据key计算出对应的Segment[]的数组下标j,确定好当前key,value应该插入到哪个Segment对象中,如果segments[j]为空,则利用自旋锁的方式在j位置生成一个Segment对象.
然后调用Segment对象的put方法
Segment对象的put方法会先加锁,然后也根据key计算出对应的HashEntry[]的数组下标i,然后将key,value封装为HashEntry对象放入该位置,此过程和JDK7的HashMap的put方法一样,然后解锁.
在加锁的过程中逻辑比较复杂,先通过自旋加锁,如果超过一定次数(多核CPU为64,单核为1)就会直接阻塞等加锁.
主要利用Unsafe操作+synchronized关键字.
Unsafe操作的使用仍然和JDK7中的类似,主要负责并发安全的修改对象的属性或数组某个位置的值.
synchronized主要负责在需要操作某个位置时进行加锁(该位置不为空,锁对象是该位置头结点),比如像某个位置的链表进行插入结点,向某个位置的红黑树插入结点.
JDK8中其实仍然有分段锁的思想,只不过JDK7中段数是可以控制的,而JDK8中是数组的每一个位置都有一把锁.
当向ConcurrentHashMap中put一个key,value时
1.首先根据key计算(n-1 & hash值)对应的数组下标i,如果该位置没有元素,则通过自旋的方法去向该位置赋值.
2.如果该位置有元素,则synchronized会加锁.
3.加锁成功后,再判断该元素类型是红黑树还是链表
4.添加成功后,判断是否需要进行树化
5.addCount,这个方法的意思是ConcurrentHashMap的元素个数加1,但是这个操作也是需要并发安全的,并且元素个数加1成功后,会继续判断是否要进行扩容,如果需要,则会进行扩容,所以这个方法很重要.
6.同时一个线程在put时如果发现当前ConcurrentHashMap正在进行扩容则会去帮助扩容.
1.JDK8中没有分段锁了,使用synchronized来进行控制
2.扩容机制不同,都支持多线程扩容,JDK7中的扩容是针对每个Segment的(类似懒汉模式),JDK8的扩容是第一个来扩容的线程要扩容完所有位置(类似饿汉模式),其他的线程可以来帮忙扩容.
3.JDK8中的元素个数统计的实现不一样了,JDK8中增加了CounterCell来帮助计数,而JDK7中没有,JDK7中是put的时候每个Segment内部计数,统计的时候是遍历每个Segment对象加锁统计