jdk 1.7
HashMap
内部属性
public class HashMap<K,V>
extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
{
// 模拟初始容量,必须是2的幂
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//最大容量 2的30次方 1073741824
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认的常量负载因子,扩容时需要
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//空数组
static final Entry<?,?>[] EMPTY_TABLE = {};
//存储数据的表,根据需要会调整大小,长度必须是2的幂
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
//包含表中所有键值的数量
transient int size;
//表扩容的阈值
//如果刚开始表是空的,那这个为初始容量
int threshold;
//负载因子
final float loadFactor;
//表被修改的次数,用于fail-fast
transient int modCount;
// 一个默认阈值。高于该值,且以String作为Key时,使用备用哈希hash函数(sun.misc.Hashing.stringHash32),这个备用hash函数能减少String的hash冲突。
// 此值是最大int型,表明hash表再也不能扩容了,继续put下去,hash冲突会越来越多。
// 至于为什么是String型才有这种特殊待遇,因为String是最常见的Key的类型。Integer虽然也很常见,但是Integer有范围限制,并且它的hashCode设计得就非常好(就是自身的数值)。
// 可以通过定义系统属性jdk.map.althashing.threshold来覆盖这个值。
// 值为1时,总是会对String类型的key使用备用的hash函数;值为-1,则一定不使用备用hash函数。
static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;
}
构造方法
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;
threshold = initialCapacity;
init();
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
public HashMap(Map<? extends K, ? extends V> m) {
this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
inflateTable(threshold);
putAllForCreate(m);
}
put方法
public V put(K key, V value) {
//懒惰加载,判断数组是否为空
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
//key如果为空则单独处理,放到数组的第一个桶上
if (key == null)
return putForNullKey(value);
int hash = hash(key);
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
inflateTable
private void inflateTable(int toSize) {
// Find a power of 2 >= toSize
int capacity = roundUpToPowerOf2(toSize);
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
table = new Entry[capacity];
initHashSeedAsNeeded(capacity);
}
roundUpToPowerOf2 : 找到大于等于传进来数值的二次方幂
- 是否有超过最大值 ?
- 有 : 使用最大值
- 没有 : 是否为正数 ?
- 是 : 通过 highestOneBit () 方法巧妙的计算 , 参数为 : 传参 - 1后乘以2, 这个地方很巧妙,其实有两个问题,为什么要乘以2,以及为什么要减1?
- 其实乘以2是因为highestOneBit ()方法的作用是找到比入参小的2次方幂,比如我传入10,返回一个8,但是roundUpToPowerOf2 这个方法是找到比入参大的,我传10的话,如何根据highestOneBit ()找到16呢,那就肯定得让10变的大一点,但是得大多少,这个不太清楚,太大了也不好,所以2倍其实是最好的,比如10,乘以2变成20,highestOneBit ()返回16
- 减一其实是因为如果我给roundUpToPowerOf2 ()方法传入16的话,最后乘2传入给highestOneBit () 方法的是32,因为roundUpToPowerOf2 ()的作用是返回大于等于入参的二次方幂,传16得返回16才行,所以减1变成15,传入highestOneBit ()的话是30,找到比30小的2次方幂是16,符合预期
- 否 : 默认使用1
- 是 : 通过 highestOneBit () 方法巧妙的计算 , 参数为 : 传参 - 1后乘以2, 这个地方很巧妙,其实有两个问题,为什么要乘以2,以及为什么要减1?
private static int roundUpToPowerOf2(int number) {
// assert number >= 0 : "number must be non-negative";
return number >= MAXIMUM_CAPACITY
? MAXIMUM_CAPACITY
: (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}
highestOneBit : 找到小于等于传进来数值的二次方幂
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);
}
例如传入的i为10,二进制为 : 0000 1010
0000 1010
(i >> 1): 0000 0101
| : 0000 1111
(i >> 2): 0000 0011
| : 0000 1111
(i >> 4): 0000 0000
| : 0000 1111
...
(i >>> 1): 0000 0111
- : 0000 1000
举个例子 :
十进制25的二进制
0000 0000 0000 0000 0000 0000 0001 1001
十进制16的二进制
0000 0000 0000 0000 0000 0000 0001 0000
我们可以看下这两者之间有什么规律没有 ?
是不是除了第一个高位1开始的后面所有数都变成了0,这就是规律 !!!
但是我们怎样才能做到将第一个高位的1保留,之后的所有都变成0呢 ?
我们可以将第一个1后面的所有数先变成1,然后再右移一位,接着再让之前的数值减去右移之后的就可以了
比如上面的25这个例子,第一位之后的位数全变成1如下 :
0000 0000 0000 0000 0000 0000 0001 1111
然后右位移一位 :
0000 0000 0000 0000 0000 0000 0000 1111
接下来相减 :
0000 0000 0000 0000 0000 0000 0001 0000
有没有发现这和16的二进制一模一样 !!! !!!
十进制16的二进制
0000 0000 0000 0000 0000 0000 0001 0000
putForNullKey
private V putForNullKey(V value) {
//遍历数组,起始位置为第一个桶
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
//如果找到了key为null的,则直接更新
if (e.key == null) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
//操作变量加一
modCount++;
//链表上使用头插法添加元素
addEntry(0, null, value, 0);
return null;
}
hash
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
之所以右移这么多位是因为如果不右移的话,基本在通过hash值计算下标的时候,只有hashCode的低位在进行运算,高位根本没有参与运算当中,会导致有很多的key是不相同的,HashCode是不相同的,最终也都到了同一个链表中来, 所以右移这么多位并且异或是希望高位也参数进来运算,之后计算索引时冲突可以降低
indexFor
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
return h & (length-1);
}
这里不使用取余是因为&是基于内存的二进制直接运算,比转换成10进制的取余快的多,又因为 X % 2 ^ n = X & (2^n - 1),可以把%运算转换为&运算,所以HashMap的capacity一定要是2 ^ n,这样HashMap计算下标才会更快
addEntry
void addEntry(int hash, K key, V value, int bucketIndex) {
//键值对的数量是否大于阈值 (数组长度 * 负载因子) && 数组当前下标的元素不能为空
if ((size >= threshold) && (null != table[bucketIndex])) {
//扩容
resize(2 * table.length);
//计算当前要插入元素的Hash值,这里不用之前传入的Hash值应该是认为扩容后的长度变了,可能Hash种子也变了,此时重新计算Hash值
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
//创建entry节点,使用头插法
createEntry(hash, key, value, bucketIndex);
}
这里添加元素前还会判断一下是否需要扩容
1.7是必须表中键值对数量达到阈值 (数组长度 * 负载因子) 并且当前数组元素是不为空的情况下才会扩容
扩容后的数组长度为 : 原数组的两倍
resize
扩容
void resize(int newCapacity) {
//记录之前的表和容量
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
//边界处理
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
//创建一个原数组2倍容量的数组
Entry[] newTable = new Entry[newCapacity];
//开始转移元素
transfer(newTable, initHashSeedAsNeeded(newCapacity));
//转移完成重新赋值
table = newTable;
//扩容后重新计算阈值
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
initHashSeedAsNeeded
初始化Hash种子
final boolean initHashSeedAsNeeded(int capacity) {
//刚开始如果不设置这个种子,默认就是0
//所以这个currentAltHashing 为 false
boolean currentAltHashing = hashSeed != 0;
// sun.misc.VM.isBooted() : 虚拟机是否正在启动
//Holder.ALTERNATIVE_HASHING_THRESHOLD : 如果没有通过启动参数设置的话,那就是int类型的最大值 (2^31 - 1)
//所以这里正常情况下也是false
boolean useAltHashing = sun.misc.VM.isBooted() &&
(capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
//上面两个条件都是false,^ : 相同为0,不同为1,所以switching为false,如果改了启动参数的话导致useAltHashing为true,那么种子就会不为0,hash算法会更加散列
boolean switching = currentAltHashing ^ useAltHashing;
if (switching) {
hashSeed = useAltHashing
? sun.misc.Hashing.randomHashSeed(this)
: 0;
}
return switching;
}
HashMap内部Holder类 :
private static class Holder {
/**
* Table capacity above which to switch to use alternative hashing.
*/
static final int ALTERNATIVE_HASHING_THRESHOLD;
static {
//获取指定启动参数
String altThreshold = java.security.AccessController.doPrivileged(
new sun.security.action.GetPropertyAction(
"jdk.map.althashing.threshold"));
int threshold;
try {
//如果指定了用你指定的,否则使用int的最大长度(2^31 - 1)
threshold = (null != altThreshold)
? Integer.parseInt(altThreshold)
: ALTERNATIVE_HASHING_THRESHOLD_DEFAULT;
// disable alternative hashing if -1
//边界处理
if (threshold == -1) {
threshold = Integer.MAX_VALUE;
}
if (threshold < 0) {
throw new IllegalArgumentException("value must be positive integer.");
}
} catch(IllegalArgumentException failed) {
throw new Error("Illegal value for 'jdk.map.althashing.threshold'", failed);
}
ALTERNATIVE_HASHING_THRESHOLD = threshold;
}
}
transfer
转移
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;
//判断是否需要重新Hash,一般情况下都不需要
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;
}
}
}
因为jdk1.7中扩容头插法导致的死循环问题 :
主要是因为在多线程情况下,两个线程都进行了扩容数组,一个线程完毕之后,已经通过头插法改变了e节点和next的位置,另一个线程不清楚这个情况,然后经历了三次循环后导致转移后的链表出现了环形的,这样之后如果get或者put的话,都会产生死循环的问题,导致cpu彪高!!!
假如有一个HashMap如下 :
此时有两个线程都来进行扩容,都创建了容量翻倍的数组,假设B线程刚运行到transfer方法的while循环内部的第一行 Entry<K,V> next = e.next; A线程已经把元素都给转移完毕了,此时情况如下图 :
线程B开始执行转移方法while循环中的语句 :
核心代码如下图 :
...
while(null != e) {
//当前节点下一个元素
Entry<K,V> next = e.next;
//判断是否需要重新Hash,一般情况下都不需要
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;
}
第一次循环 :
e = k1,v1
next = k2,v2
紧接着,第二次循环 :
e = k2,v2
next = k1,v1
紧接着第三次循环 :
e = k1,v1
next = null
我们就会发现此时形成了环形链表!!!
createEntry
头插法
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
总结
- 首先懒惰加载,看一下table是否为空,如果为空则进行初始化,初始化时利用位运算,巧妙的计算出了大于等于指定参数2的幂次方数来用以表的容量
- 之后会判断key是否为null,如果为null则遍历数组上第0位的链表,如果此链表上有对应的null值key的话则直接覆盖值,否则就信插入一个entry
- 计算hash值,通过无符号右移和异或的操作来使hashCode值的高位也参与到下标的计算中,如果不右移的话就只有低位在参与下标的计算,这样可以使元素分布更加的散列
- 之后会通过计算好的hash值来计算存储数组中的下标,采用的方法不是取余,而是比取余效率更高的与运算,因为 : X % 2 ^ n = X & (2 ^ n - 1),所以表的长度必须是2^n,这样计算下标的时候可以效率更快
- 上一步找到了下标之后,就会遍历该位置上的链表,并判断key和hash值是否一样,如果一样则直接替换value并且返回之前的value,如果没有找到的话就首先modCount加1,记录表的修改次数,然后再使用头插法插入到链表上
- 插入之前会首先判断是否需要扩容,条件是HashMap中的键值对数量需要大于等于阈值 (数组长度 * 负载因子)并且当前要插入的桶节点不能为null
- 扩容之后的数组长度为原来的2倍,扩容完以后会进行元素的转移,以及重新计算阈值
get方法
public V get(Object key) {
//如果key为null的情况
if (key == null)
return getForNullKey();
Entry<K,V> entry = getEntry(key);
//entry如果为null返回null否则返回entry对应的value
return null == entry ? null : entry.getValue();
}
getForNullKey
private V getForNullKey() {
//map中键值对如果为0直接返回null
if (size == 0) {
return null;
}
//否则直接遍历数组中第0位的链表,找到key为null的entry直接返回对应value
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null)
return e.value;
}
return null;
}
getEntry
final Entry<K,V> getEntry(Object key) {
//map中键值对如果为0直接返回null
if (size == 0) {
return null;
}
//计算hash值
int hash = (key == null) ? 0 : hash(key);
//通过hash值找到对应的桶,然后遍历其链表
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
//循环判断找到相应的key就返回指定的entry
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}
get方法总结 :
- 首先针对key为null的情况,如果map中键值对数量为0则直接返回null,不用遍历了,否则对数组中第0位的桶进行遍历,因为put的时候针对key为null的entry放置的位置就是数组第0位,找到直接返回entry的value
- 之后key不为null则先计算hash值,然后通过hash计算出桶下标,之后遍历对应的桶上的链表,依次通过 == 和equals来比较key是否相同,如果相同返回value
modCount
快速失败 : fail-fast
当 HashMap 在迭代过程中,结构发生变化时,会抛出 ConcurrentModificationException 错误,称为快速失败机制。
对 HashMap 进行迭代:Iterator iterator = hashMap.keySet().iterator();
public Set<K> keySet() {
Set<K> ks = keySet;
return (ks != null ? ks : (keySet = new KeySet()));
}
private final class KeySet extends AbstractSet<K> {
public Iterator<K> iterator() {
return newKeyIterator();
}
...
}
Iterator<K> newKeyIterator() {
return new KeyIterator();
}
private final class KeyIterator extends HashIterator<K> {
public K next() {
return nextEntry().getKey();
}
}
因为继承了一个父类,所以会先调用父类的初始化方法
private abstract class HashIterator<E> implements Iterator<E> {
Entry<K,V> next; // next entry to return
int expectedModCount; // For fast-fail
int index; // current slot
Entry<K,V> current; // current entry
HashIterator() {
expectedModCount = modCount;
if (size > 0) { // advance to first entry
Entry[] t = table;
while (index < t.length && (next = t[index++]) == null)
;
}
}
...
}
这里初始化的时候会把modCount给赋值给内部的一个属性异常modCount,我们上面可以看到我们执行了两次put操作,所以这时候modCount和异常modCount都是2
迭代器中常用的主要有两个方法,一个是hasNext方法(),看下节点是否为空,还有一个是next()方法,获取节点
hasNext()方法里面主要是判断是否为null,返回一个布尔值,
主要是next()方法中,
private final class KeyIterator extends HashIterator<K> {
public K next() {
return nextEntry().getKey();
}
}
private abstract class HashIterator<E> implements Iterator<E> {
final Entry<K,V> nextEntry() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
Entry<K,V> e = next;
if (e == null)
throw new NoSuchElementException();
if ((next = e.next) == null) {
Entry[] t = table;
while (index < t.length && (next = t[index++]) == null)
;
}
current = e;
return e;
}
}
next方法中会调用nextEntry方法,这个里面开始就会对modCount来进行一个判断,如果不相等的话就直接抛出异常
这里我们发现如果你在执行这个next()方法之前,对HashMap进行过某些操作,导致内部的modCount增加的话,那么如果再次执行的next()方法的话,这时expectedModCount没有变化,所以两个值不一样,判断false,就会抛出异常,因为你改变了map的结构了
主要原因是因为remove方法,(其实put以及clear等方法也都会导致modCount自增)
public V remove(Object key) {
Entry<K,V> e = removeEntryForKey(key);
return (e == null ? null : e.value);
}
final Entry<K,V> removeEntryForKey(Object key) {
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : hash(key);
int i = indexFor(hash, table.length);
Entry<K,V> prev = table[i];
Entry<K,V> e = prev;
while (e != null) {
Entry<K,V> next = e.next;
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
modCount++;
size--;
if (prev == e)
table[i] = next;
else
prev.next = next;
e.recordRemoval(this);
return e;
}
prev = e;
e = next;
}
return e;
}
我们可以很明显的看到modCount++,size–., size–是因为键值对删除了一个,总数量需要减一,modCount++是因为记录修改次数
如果以后要在迭代的时候修改map集合的话,得用迭代器的remove方法,我们看下它内部是如何实现的
public void remove() {
if (current == null)
throw new IllegalStateException();
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
Object k = current.key;
current = null;
HashMap.this.removeEntryForKey(k);
expectedModCount = modCount;
}
其实上面都不重要,主要的是最后一行代码,会对expectedModCount重新赋值,这就能对上了
ConcurrentHashMap
数据结构
内部属性
public class ConcurrentHashMap<K, V> extends AbstractMap<K, V>
implements ConcurrentMap<K, V>, Serializable {
private static final long serialVersionUID = 7249069246763182397L;
static final int DEFAULT_INITIAL_CAPACITY = 16;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
static final int DEFAULT_CONCURRENCY_LEVEL = 16;
static final int MAXIMUM_CAPACITY = 1 << 30;
static final int MIN_SEGMENT_TABLE_CAPACITY = 2;
static final int MAX_SEGMENTS = 1 << 16; // slightly conservative
static final int RETRIES_BEFORE_LOCK = 2;
private transient final int hashSeed = randomHashSeed(this);
private static int randomHashSeed(ConcurrentHashMap instance) {
if (sun.misc.VM.isBooted() && Holder.ALTERNATIVE_HASHING) {
return sun.misc.Hashing.randomHashSeed(instance);
}
return 0;
}
final int segmentMask;
final int segmentShift;
final Segment<K,V>[] segments;
}
构造方法
//initialCapacity :
public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel) {
//边界验证判断
if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
if (concurrencyLevel > MAX_SEGMENTS)
concurrencyLevel = MAX_SEGMENTS;
// Find power-of-two sizes best matching arguments
//偏移量
int sshift = 0;
//ssize : map中segment数组的长度,传入一个concurrencyLevel,找到比它大的二的幂次方数,用ssize来存储,
// 例如传入的是17,那么最后ssize也就是segment数组的长度就是32
int ssize = 1;
while (ssize < concurrencyLevel) {
++sshift;
ssize <<= 1;
}
//如果上述concurrencyLevel为15,则sshift = 4,ssize = 16
//段偏移量
this.segmentShift = 32 - sshift;
//segmentMask : 计算数组下标时,需要通过 hash & (lenght - 1),此时segmentMask就代表表达式右侧的计算结果
this.segmentMask = ssize - 1;
//传入的内部entry数组长度超过了节点个数按最大值来处理
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//计算每个segment中entry数组的长度需要经历两个步骤
//第一步是先保证每个segment中是均分entry总长度的,比如initialCapacity设置的为33,ssize是16
//那么最后c就是2,但是会少一个元素,这个时候不能丢弃那个元素,就得循环然后对c自增,直到找到彻底均分的这个数值
int c = initialCapacity / ssize;
if (c * ssize < initialCapacity)
++c;
//这个地方cap才是最终每个segment中entry数组的长度,最小只能是2,所以即使你传进来initialCapacity是16,并发粒度是16期 //待每个segment中entry数组长度为1,最后也会变成2,不会是1
int cap = MIN_SEGMENT_TABLE_CAPACITY;
//这里就是每个segment中entry数组长度的第二个步骤,因为不能只保证外部segment数组的长度是2的幂次方,里面entry数组也得保长度得是2的幂次方
while (cap < c)
cap <<= 1;
// 创建segment数组0位置上的元素
Segment<K,V> s0 =
new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
(HashEntry<K,V>[])new HashEntry[cap]);
//创建segment数组
Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
//将创建好的元素放到数组0的位置上,以让其他节点可以复制其内部属性
UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
this.segments = ss;
}
public ConcurrentHashMap(int initialCapacity, float loadFactor) {
this(initialCapacity, loadFactor, DEFAULT_CONCURRENCY_LEVEL);
}
public ConcurrentHashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);
}
public ConcurrentHashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);
}
public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
DEFAULT_INITIAL_CAPACITY),
DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);
putAll(m);
}
如果是无参构造的话,段中哈希表的默认大小为2,负载因子是0.75,那么扩容的阈值就是1.5,插入第一个元素正好不用扩容
这里只单独对segment数组第一个位置上的元素初始化了,其他位置的元素没有初始化,其实是因为,如果不这样的话,每次创建segment都需要计算负载因子,阈值,数组这些参数,如果数组第0位在map初始化时创建好的话,其他节点创建的时候拿s0当模版直接照搬参数就行,这样会快一些
有关UNSAFE的方法
entryForHash
通过某个segment以及哈希值,获取该段中指定的entry节点
static final <K,V> HashEntry<K,V> entryForHash(Segment<K,V> seg, int h) {
HashEntry<K,V>[] tab;
return (seg == null || (tab = seg.table) == null) ? null :
(HashEntry<K,V>) UNSAFE.getObjectVolatile
(tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
}
entryAt
通过指定entry数组和下标,获取指定数组内存中元素值,并保持原子性
static final <K,V> HashEntry<K,V> entryAt(HashEntry<K,V>[] tab, int i) {
return (tab == null) ? null :
(HashEntry<K,V>) UNSAFE.getObjectVolatile
(tab, ((long)i << TSHIFT) + TBASE);
}
setEntryAt
将指定数组位置的元素设置为给定值
static final <K,V> void setEntryAt(HashEntry<K,V>[] tab, int i,
HashEntry<K,V> e) {
UNSAFE.putOrderedObject(tab, ((long)i << TSHIFT) + TBASE, e);
}
put方法
public V put(K key, V value) {
Segment<K,V> s;
if (value == null)
throw new NullPointerException();
//计算hash值
int hash = hash(key);
//计算segment下标时注意这里会对hash函数右移
int j = (hash >>> segmentShift) & segmentMask;
//获取要插入位置的segment,如果为空则去创建,创建好以后调用该对象的put方法
if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck
(segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
s = ensureSegment(j);
return s.put(key, hash, value, false);
}
注意 :
注意计算segment下标时变量j的过程,会对hash函数进行右移segmentShift位,这里咱们先对这个segmentShift给求出来
这个segmentShift是在构造函数中的,segmentShift = 32 - sshift ,sshfit和segment有关,是segment数组长度2的次方数,假设segment长度为16,那么这个时候sshift就是4,那么segmentShift 就是28
hash值右移28位就把高位的四个值保留了下来,拿高位的4个bit位去和数组长度去做&运算,之后在计算entry数组的下标时,hash函数直接和entry数组减一进行&运算,没有进行右移,那就说明是拿低位的4bit去做的&运算,这样可以保证,假如我segment数组的长度和entry数组的长度,这种情况是由于entry数组一直在扩容,它俩长度一样的时候,如果都使用的低4位的话,那么如果有很多的元素要来插入,低四位都一样,那么它们放的位置都会在一个segment的一个entry链表上挂着,导致其他的entry节点都是空的
hash
private int hash(Object k) {
int h = hashSeed;
if ((0 != h) && (k instanceof String)) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
// Spread bits to regularize both segment and index locations,
// using variant of single-word Wang/Jenkins hash.
h += (h << 15) ^ 0xffffcd7d;
h ^= (h >>> 10);
h += (h << 3);
h ^= (h >>> 6);
h += (h << 2) + (h << 14);
return h ^ (h >>> 16);
}
ensureSegment
返回给定索引的段,如果不存在则创建
private Segment<K,V> ensureSegment(int k) {
final Segment<K,V>[] ss = this.segments;
//根据索引计算出段的内存地址
long u = (k << SSHIFT) + SBASE; // raw offset
Segment<K,V> seg;
//通过unsafe保证从内存中获取segment
if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
//为空则首先获取数组中第0位置的元素(模版)
Segment<K,V> proto = ss[0]; // use segment 0 as prototype
//拷贝内部属性,节省计算时间
int cap = proto.table.length;
float lf = proto.loadFactor;
int threshold = (int)(cap * lf);
//创建entry数组
HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
//二次校验,确保到这步之前指定位置创建的segment没有被其他元素给创建
if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
== null) { // recheck
//创建segment
Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
//通过自旋+cas的方式来确保一定能创建segment成功
while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
== null) {
if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
break;
}
}
}
//如果不为空则表示之前创建过了,直接返回
return seg;
}
segment.put
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
//使用tryLock尝试加锁,确保不会一直等待
// 如果拿到了锁则进入try块开始遍历内部的entry数组
// 如果没有拿到锁则在等待锁的时候去尝试先看看要插入的节点是否存在,如果不存在则先创建,不傻傻的啥都不干只等待锁,有 // 则直接返回,之后拿到锁以后就不用创建了
HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value);
//用于存储被覆盖的值
V oldValue;
try {
//entry数组
HashEntry<K,V>[] tab = table;
//通过&运算获取下标
int index = (tab.length - 1) & hash;
//获取指定下标的entry节点
HashEntry<K,V> first = entryAt(tab, index);
//遍历entry数组
for (HashEntry<K,V> e = first;;) {
//头结点不为空,或者还没遍历到了链表尾部了
if (e != null) {
K k;
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
oldValue = e.value;
if (!onlyIfAbsent) {
e.value = value;
++modCount;
}
break;
}
e = e.next;
}
//头结点为空,或者链表已经遍历到尾部了
else {
//看看当前线程线程有没有在之前等待锁的时候就已经创建好了entry
if (node != null)
node.setNext(first);
//之前没有创建entry的话就直接创建
else
node = new HashEntry<K,V>(hash, key, value, first);
int c = count + 1;
//判断是否超过了阈值 并且 数组的长度没有超过最大值就开始扩容
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
rehash(node);
//没有超过阈值就正常设置entry
else
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
unlock();
}
return oldValue;
}
scanAndLockForPut
等待锁时尝试遍历entry数组,如果要插入的元素已经存在则不用创建,否则提前创建节点
private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
//根据segment和entry的hash值来返回指定entry节点
HashEntry<K,V> first = entryForHash(this, hash);
HashEntry<K,V> e = first;
HashEntry<K,V> node = null;
//重试次数
int retries = -1; // negative while locating node
//自旋tryLock,内部如果达到重试最大次数的话就退出循环
while (!tryLock()) {
HashEntry<K,V> f; // to recheck first below
//首次进入循环或者在自旋的等待锁的时候感知到了头节点发生了变化,内部会遍历entry链表,看是否存在,不存在则创建
if (retries < 0) {
//如果头结点直接为null或者我此时遍历到了链表尾部了
if (e == null) {
//判断node之前是否创建过
if (node == null) // speculatively create node
//构建entry节点
node = new HashEntry<K,V>(hash, key, value, null);
//将retries变量置为0,暂时结束了遍历链表,之后如果头结点发生了变化还会回到这里重新遍历链表
retries = 0;
}
//判断key是否相等
else if (key.equals(e.key))
//如果相等结束遍历,继续重试获取锁
retries = 0;
//迭代下一个链表节点
else
e = e.next;
}
//每次循环重试次数加一,超过了最大重试次数才尝试阻塞获取锁 (最大重试次数是根据cpu数量来的,超过1则重试64,否则重试1次)
else if (++retries > MAX_SCAN_RETRIES) {
lock();
break;
}
//如果重试次数为偶数次的话并且此时头节点发生了变化,那么就需要重新遍历链表了
else if ((retries & 1) == 0 &&
(f = entryForHash(this, hash)) != first) {
e = first = f; // re-traverse if entry changed
retries = -1;
}
}
//最后获取到锁了,返回node
return node;
}
rehash
private void rehash(HashEntry<K,V> node) {
/*
* 将每个列表中的节点重新分类到新表中。因为我们使用的是2的幂展开,所以每个容器中的元素要么保持在相同的索引上,要么以2的幂偏移量移动。我们通过捕捉旧节点可以被重用的情况来消除不必要的节点创建,因为它们的next字段不会改变。统计上,在默认阈值下,当表加倍时,只有大约六分之一的表需要克隆。一旦它们不再被任何可能处于并发遍历表中的读取线程引用,它们所替换的节点将是可垃圾收集的。条目访问使用普通数组索引,因为它们后面跟着易失性表写入。
*/
//记录下老数组以及新数组相关的一些变量
HashEntry<K,V>[] oldTable = table;
int oldCapacity = oldTable.length;
int newCapacity = oldCapacity << 1;
threshold = (int)(newCapacity * loadFactor);
//创建一个之前数组两倍容量的数组
HashEntry<K,V>[] newTable =
(HashEntry<K,V>[]) new HashEntry[newCapacity];
//记录要位运算的值,这里是新数组的
int sizeMask = newCapacity - 1;
//遍历原数组,老套路,将原哈希表索引 i 处的链表拆分到新哈希表索引 i 和 i+oldCap 两个位置,高位和低位。
for (int i = 0; i < oldCapacity ; i++) {
//当前节点
HashEntry<K,V> e = oldTable[i];
//必须得是不为空才能转移,为空没有必要转移
if (e != null) {
//记录下当前节点的下一个节点
HashEntry<K,V> next = e.next;
//计算下当前节点在新数组的下标
int idx = e.hash & sizeMask;
//如果当前节点的下一个节点为null则证明此位置只有一个元素,无后继节点,这个时候直接转移就行,没有后顾之忧
if (next == null) // Single node on list
newTable[idx] = e;
//下面对应的是链表情况
//首先说下下面代码的功能情况
//第一个for循环想重用扩容后仍在在同一个槽中的连续序列
else { // Reuse consecutive sequence at same slot
//最好的情况第一个节点就是我要找的元素
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) {
V v = p.value;
int h = p.hash;
int k = h & sizeMask;
HashEntry<K,V> n = newTable[k];
newTable[k] = new HashEntry<K,V>(h, p.key, v, n);
}
}
}
}
//扩容完毕以后添加当前要put的节点
int nodeIndex = node.hash & sizeMask; // add the new node
node.setNext(newTable[nodeIndex]);
newTable[nodeIndex] = node;
table = newTable;
}
针对for循环中内部第一个循环画图分析 :
这个循环中主要目的就是想在同一槽的元素就重用连续序列
先说下两种典型的情况,最好和最不好
以下图为基础
最好的情况 :
我这个链表上有四个元素,这四个元素我遍历计算完其在新数组的位置后发现都是同一个下标,此时走完循环,lastRun就是头节点 (k1,v1),
lastIdx就是头节点在新数组上的位置,假设还是0,扩容后的数组如下图
这样其实下面那个循环都不会执行了,因为p 是 等于 lastRun的
最坏的情况 :
这四个元素都不满足,那最后lastRun就是链表的最后一个节点 (k4,v4),他会先移动到新数组上,之后下面那个循环再从头开始移动元素
这样应该都知道这个循环的意义了
总结
- 首先通过key的hashcode计算出hash值
- 再通过hash值的高位与segment数组长度-1进行运算得到下标
- 拿到下标以后通过调用unsafe方法来获取内存中该位置的segmnet元素,如果为空就去初始化,初始化是通过双重check以及自旋cas的方式来进行的,尽可能的增加效率
- 调用获取到的segment对象的put方法来添加元素
- 首先尝试通过不阻塞方式(tryLock)获取锁,获取不到就尝试在等待获取锁的时候去先创建entry节点,等获取到了锁之后就不用再创建了,如果获取到了锁以后则进入下面的步骤
- 通过hash函数计算出下标,然后再获取到对应的entry节点,此时开始遍历链表,如果当前节点不为空,则去判断当前遍历的节点和要put的元素key是否相等,找到的话通过onyIfAbset变量来判断是否需要对值进行覆盖,false则替换,true不替换
- 当前节点为空的话则证明可能已经遍历到了链表的尾部或者这是一个空链表,那么需要创建entry节点,如果之前在等待锁的时候已经创建过了则不需要,最终插入之前还需要判断下是否需要扩容,不需要扩容再通过头插法来插入
- 最后执行finally块中会去释放锁
jdk1.8
HashMap
参考博客 : https://blog.csdn.net/v123411739/article/details/78996181
继承和实现
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {}
内部属性
// 默认初始容量。容量必须是2的倍数
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
// 最大容量
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;
// treeifyBin方法中判断,只有当table数组长度大于64时,才会触发链表转红黑树
// 小于的时候还是优先扩容
static final int MIN_TREEIFY_CAPACITY = 64;
static final Entry<?,?>[] EMPTY_TABLE = {};
// 存储键值对对应的Node数组
transient Node<K,V>[] table;
// 键值对的数量
transient int size;
// 等于加载因子乘以数组长度,表示一个阙值,当size(实际使用容量)超过了就会扩容
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; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
// ...
}
构造方法
//无参构造
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);
}
put方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
hash
static final int hash(Object key) {
int h;
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)
n = (tab = resize()).length;
//通过hash值计算出数组下标,如果这个位置为空,表示还没插入,此时直接创建节点赋值即可
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
//下面就是计算出的位置上有元素的情况了,一共有三种,
//1.只有一个元素,判断是否和我的key相同即可
//2.是一个红黑树,调用root节点的putTreeVal方法插入一个节点
//3.是一个链表,这种情况就需要遍历
else {
Node<K,V> e; K k;
//判断桶第一个位置的元素是否和传入的key相同,可能桶内只有一个元素,也可能是链表头,也可能是红黑树的root节点
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);
//如果binCount大于等于7则尝试将链表转换为红黑树
//假如现在链表中有8个元素,现在如果是第一次循环,那么e现在指向了链表中的第二个节点,binCount此时为0,
//到第8个元素的时候,binCount为6
//也就是说链表中如果有第9个元素的时候,那么此时执行这个if条件,binCount才正好满足 >= //TREEIFY_THRESHOLD - 1,满足后也不是就肯定能转成红黑树了,内部还会对数组长度进行判断,必须得大于64才行
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//链表元素挨个判断
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//如果e不为null,证明之前map中就维护了这key-value
if (e != null) { // existing mapping for key
V oldValue = e.value;
//是否需要替换
if (!onlyIfAbsent || oldValue == null)
e.value = value;
//hashMap中暂时没有实现逻辑,linkedHashMap中实现了
afterNodeAccess(e);
//返回之前保存的value值
return oldValue;
}
}
//记录操作次数,用以fail-fast机制
++modCount;
//扩容后的长度如果大于阈值 (数组长度 * 负载因子)则扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
resize
初始化以及扩容
扩容有三种情况 :
1.空参构造 : 构造方法里只对loadFactor使用了默认值 (0.75),然后第一次put的时候会去初始化数组,默认长度为16,阈值为12
2.指定了初始容量的构造 : 此时因为没有capacity 属性,所以传入的initialCapacity会暂存在threshold属性中国,后续会使用这个值
3.之前已经参与了扩容了(包括初始化)
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表示table已经扩容过了
if (oldCap > 0) {
//老数组长度大于最大值,此时阈值无法乘2了
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; // double threshold
}
//使用了带有初始化容量的构造,此时容量为初始化得到的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;
//当前桶内没有hash冲突,就一个元素
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
//当前index对应节点为红黑树,
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
//链表情况,将当前index对应的链表分成两个链表,减少扩容导致的迁移量
//因为要迁移的元素其实只会移动到新数组的两个位置上,一个是原数组索引位置,另一个索引计算公式为 : 原数 //组索引位置 + oldCap
//比如有一个数组长度为16, 某个元素hash值假设为 : 1111 1010,
// 1111 1010
// 0000 1111
//& 0000 1010
//此时根据数组长度为16的计算索引结果为 : 10
//假设现在扩容了,数组长度翻倍了变成了 : 32
// 1111 1010
// 0001 1111
//& 0001 1010
//此时根据数组长度为16的计算索引结果为 : 26
//结果和原数组indedx相差(26 - 10) = 16,正好是扩容前数组的长度
//扩容前和扩容后的二进制区别主要在于hash值高位的1,扩容后高位多了一个
// 如果高位为0,那么扩容后的索引高位也不会变成1,结果自然不会变,
// 如果高位为1,那么新的索引位置高位也会变成1,结果是原值 + 扩容的值(原数值长度)
//下面代码的目的其实也是将上述两种结果给以链表的形式存储起来,之后移动只需要移动两个链表的头节点即可
//loHead : 低位置头节点 , loTail : 低位置尾节点
//hiHead : 高位置头节点 , hiTail : 高位置尾节点
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;
}
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;
}
TreeNode.split
扩容时桶内为红黑树数据结构的转移方法
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
//记录当前调用改方法的节点,一般是数组桶中第一个元素
TreeNode<K,V> b = this;
// Relink into lo and hi lists, preserving order
//高低位置头尾节点
TreeNode<K,V> loHead = null, loTail = null;
TreeNode<K,V> hiHead = null, hiTail = null;
//高低位置链表中的元素数量
int lc = 0, hc = 0;
//遍历红黑树
for (TreeNode<K,V> e = b, next; e != null; e = next) {
//next结点赋值为e的下一个节点
next = (TreeNode<K,V>)e.next;
e.next = null; //help gc
//新老数组索引位置相同的情况
if ((e.hash & bit) == 0) {
//如果loTail为空则记录头节点,并将e的前驱节点记录为之前的尾节点
if ((e.prev = loTail) == null)
loHead = e;
else
//否则将节点添加到loTail的尾部
loTail.next = e;
//loTail更新为遍历的新节点
loTail = e;
++lc;
}
//新数组索引 = 老数组索引 + lodCap情况
//情况同上
else {
if ((e.prev = hiTail) == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
++hc;
}
}
//如果低位链表头节点不为空
if (loHead != null) {
//低位链表数量小于等于6则退化为链表
if (lc <= UNTREEIFY_THRESHOLD)
tab[index] = loHead.untreeify(map);
else {
//大于6则移动链表到新数组上
//注意:此时之前的红黑树结构依然是存在的
tab[index] = loHead;
//如果高位链表头节点不为空则证明之前的红黑树分成了两个链表,树形结构已经发生了变化,需要重新构建低位链表的 //红黑树,这里的else情况其实就是发现高位链表不存在,证明这颗红黑树都在新数组的同一个位置,此时红黑树结构不 //用动,还是之前的那样,很巧妙
if (hiHead != null) // (else is already treeified)
loHead.treeify(tab);
}
}
//同上
if (hiHead != null) {
if (hc <= UNTREEIFY_THRESHOLD)
tab[index + bit] = hiHead.untreeify(map);
else {
tab[index + bit] = hiHead;
if (loHead != null)
hiHead.treeify(tab);
}
}
}
TreeNode.untreeify
树形退化为链表,条件为扩容后发现高低位链表个数小于等于6
final Node<K,V> untreeify(HashMap<K,V> map) {
//头节点和尾节点
Node<K,V> hd = null, tl = null;
//遍历红黑树高低位的链表
for (Node<K,V> q = this; q != null; q = q.next) {
//将treeNode节点转化为普通node
Node<K,V> p = map.replacementNode(q, null);
//尾节点为空记录头节点
if (tl == null)
hd = p;
//尾节点不为空则将p添加到尾节点尾部
else
tl.next = p;
//更新尾节点
tl = p;
}
//返回头节点
return hd;
}
TreeNode.putTreeVal
将节点插入到红黑树中
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;
//1.查找根节点,索引位置的头节点并不一定为红黑树的根节点
TreeNode<K,V> root = (parent != null) ? root() : this;
//2.将根节点赋值给p节点开始遍历查找
for (TreeNode<K,V> p = root;;) {
int dir, ph; K pk;
//3.如果传入的hash值小于p节点的hash值,则dir赋值为-1,代表向p的左边查找
if ((ph = p.hash) > h)
dir = -1;
//4.如果传入的hash值大于p节点的hash值,则dir赋值为1,代表向p的右边查找
else if (ph < h)
dir = 1;
//5.如果传入的hash值等于p节点的hash值和key,则p为目标节点,返回p节点
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
//6.如果k所属的类没有实现Comparable接口 或者 k和p节点的key相等
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0) {
//6.1 第一次符合条件, 从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;
}
//6.2 否则使用定义的一套规则来比较k和p节点的key的大小, 用来决定向左还是向右查找
dir = tieBreakOrder(k, pk);
}
//将当前p节点赋值给xp(x的父节点)
TreeNode<K,V> xp = p;
//7. 如果dir<=0则需要去p的左节点查找,看下是否为null,如果为null则证明找到了要插入的位置,不为null则进入下一次循环
//如果dir>0则同上
if ((p = (dir <= 0) ? p.left : p.right) == null) {
//xp的next节点
Node<K,V> xpn = xp.next;
//8.创建新的节点,其中x的next节点为xpn,即将x节点插入xp和xpn的中间
TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
//9.维护x,xp,xpn之间的树形关系
if (dir <= 0)
xp.left = x;
else
xp.right = x;
//xp的next节点赋值为x
xp.next = x;
//将x的parent和prev节点设置为xp
x.parent = x.prev = xp;
//如果xpn不为空则将xpn的prev设置为x节点,与创建x时候的next是xpn相互对应
if (xpn != null)
((TreeNode<K,V>)xpn).prev = x;
//10.进行红黑树的插入平衡调整
moveRootToFront(tab, balanceInsertion(root, x));
return null;
}
}
}
TreeNode.tieBreakOrder
定义一套规则用于极端情况下比较两个参数的大小。
// 用于不可比较或者hashCode相同时进行比较的方法, 只是一个一致的插入规则,用来维护重定位的等价性。
static int tieBreakOrder(Object a, Object b) {
int d;
if (a == null || b == null ||
(d = a.getClass().getName().
compareTo(b.getClass().getName())) == 0)
d = (System.identityHashCode(a) <= System.identityHashCode(b) ?
-1 : 1);
return d;
}
TreeNode.treeifyBin
将链表转换为红黑树 (前提条件是链表中的元素需要大于等于9,并且表长度要大于64,但是如果表长度是小于64的话,这时候就只会扩容,因为扩容了之后链表上的元素就有可能会分散了)
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
` //如果数组为空或者长度是小于64的话就执行初始化或扩容
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
//获取并判断链表头节点是否为空
else if ((e = tab[index = (n - 1) & hash]) != null) {
//下面代码在进行单向链表转双向链表
//hd : 记录此时单向链表的头节点
//tl : 链表中的前一个节点
TreeNode<K,V> hd = null, tl = null;
do {
//将普通Node转换为树形Node
TreeNode<K,V> p = replacementTreeNode(e, null);
//如果tl为空此时记录下双向链表的头节点,切换为双向链表后会替换掉之前的单向链表
if (tl == null)
hd = p;
else {
//不为空则前后节点相互连接
p.prev = tl;
tl.next = p;
}
//重新赋值上一个节点
tl = p;
//继续迭代单向链表
} while ((e = e.next) != null);
//最后将切换好的双向链表重新替换掉之前的单向链表并判断是否为null,之后则调用双向链表的头节点的treeify方法来转换为红黑树
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
TreeNode.treeify
通过双向链表生成红黑树
final void treeify(Node<K,V>[] tab) {
//根节点
TreeNode<K,V> root = null;
//1.将调用此方法的节点赋值为x,以x作为起点,开始进行遍历
for (TreeNode<K,V> x = this, next; x != null; x = next) {
//记录next节点
next = (TreeNode<K,V>)x.next;
x.left = x.right = null;
//2.如果root为null则更新root相关属性
if (root == null) {
x.parent = null;
x.red = false;
root = x;
}
//3.如果不为null则从root开始遍历红黑树
else {
K k = x.key;
int h = x.hash;
//key的类如果实现了 comparable接口则存储key的Class对象,否则就是null
Class<?> kc = null;
//4.从根节点开始遍历红黑树
for (TreeNode<K,V> p = root;;) {
int dir, ph;
K pk = p.key;
//5.当前遍历的节点的hash值大于给定值,此时dir为-1,代表向p的左边查找
if ((ph = p.hash) > h)
dir = -1;
//6.当前遍历的节点的hash值小于给定值,此时dir为1,代表向p的右边查找
else if (ph < h)
dir = 1;
//7.hash值相等,则比较key值,如果k没有实现Comparable接口 或者 x节点的key和p节点的key相等
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
//7.1 使用定义的一套规则来比较x节点和p节点的大小,用来决定向左还是向右查找
dir = tieBreakOrder(k, pk);
TreeNode<K,V> xp = p;
//8.dir<=0则向p左边查找,否则向右查找,如果为null,则代表该位置即为x的目标位置
if ((p = (dir <= 0) ? p.left : p.right) == null) {
//9.x和xp节点的属性设置
x.parent = xp;
if (dir <= 0)
xp.left = x;
else
xp.right = x;
//10.进行红黑树的插入平衡(左旋,右旋,变色)来保证当前树符合红黑树的要求
root = balanceInsertion(root, x);
break;
}
}
}
}
moveRootToFront(tab, root);
}
红黑树的平衡以及左旋右旋这里因为篇幅原因,不做解释,后续单独发一篇文章针对红黑树来写
TreeNode.moveRootToFront
确保给定的根是桶内的第一个节点
static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root) {
int n;
//1.判断根节点和数组不为null
if (root != null && tab != null && (n = tab.length) > 0) {
//2.计算出根节点的下标
int index = (n - 1) & root.hash;
//3.通过下标获取桶内第一个元素
TreeNode<K,V> first = (TreeNode<K,V>)tab[index];
//4.根节点如果不是桶内第一个元素的话那就更新桶内第一个元素,并且维护双向链表的头节点为根节点
if (root != first) {
//rn和rp都是通过双向链表来获取的
//rn : 根节点的next节点
//rp : 根节点的上一个节点
Node<K,V> rn;
//4.1 更新桶内第一个元素为根节点
tab[index] = root;
TreeNode<K,V> rp = root.prev;
//4.2和4.3 相当于从双向链表中剔除了根节点,然后4.4是把根节点给放到了链表的头部
//4.2 如果rn不为null则rn的上一个节点变成之前原先根节点的上一个节点
if ((rn = root.next) != null)
((TreeNode<K,V>)rn).prev = rp;
//4.3 如果rp不为null则rp的下一个节点变成了原先根节点的下一个节点
if (rp != null)
rp.next = rn;
//4.4 链表的头节点不为null的话则更新头节点的前驱为root节点
if (first != null)
first.prev = root;
//root节点后驱为之前的链表头
//root节点的前驱变成null
root.next = first;
root.prev = null;
}
//5.检查是否符合红黑树的标准
assert checkInvariants(root);
}
}
红黑树的定义
- 每个结点或是红的,或是黑的
- 根节点是黑的
- 每个叶节点是黑的
- 如果一个结点是红的,那它的两个儿子都是黑的
- 对每个节点,从该节点到其子孙节点的所有路径上包含相同数目的黑色节点
ConcurrentHashMap
继承和实现
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
implements ConcurrentMap<K,V>, Serializable {}
内部属性
/**
* 2的30次方
*/
private static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* 默认的初始容量,必须是2的幂,最多为2的30次方
*/
private static final int DEFAULT_CAPACITY = 16;
/**
* 最大可能的(非2的幂)数组大小。toArray和相关方法需要
*/
static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
/**
* 该表的默认并发级别。未使用,但为与该类以前版本的兼容性而定义
*/
private static final int DEFAULT_CONCURRENCY_LEVEL = 16;
/**
* 该表的负载因子。在构造函数中重写此值仅影响初始表容量。实际的浮点值通常不使用——使用n - (n >>> 2)这样的表达式来表示相关的调整大小阈值更简单。1.8的concurrentHashMap在首次初始化之后会通过n - (n >>> 2) 来计算阈值而不是通过n * 0.75的方式
*/
private static final float LOAD_FACTOR = 0.75f;
/**
* 使用树而不是列表的bin计数阈值。当向至少有这么多节点的桶中添加元素时,桶将转换为树。该值必须大于2,并且应该至少为8,以符合在树木移除中关于收缩后转换回普通箱的假设。
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* 在调整大小操作期间取消树化(拆分)bin的bin计数阈值。应小于TREEIFY_THRESHOLD,且最多为6,以便在移除时进行收缩检测。
*/
static final int UNTREEIFY_THRESHOLD = 6;
/**
* 可以对桶进行树化的最小表容量。(否则,如果一个bin中的节点太多,则会调整表的大小。)该值应至少为4 * TREEIFY_THRESHOLD,以避免调整大小阈值与树化阈值之间的冲突。
*/
static final int MIN_TREEIFY_CAPACITY = 64;
/**
*每个传输步骤的最小重新绑定数。范围被细分以允许多个调整大小线程。此值用作下限,以避免调整大小器遇到过多的内存争用。该值至少为DEFAULT_CAPACITY。
*/
private static final int MIN_TRANSFER_STRIDE = 16;
/**
* 在sizeCtl中用于生成戳记的位数。对于32位数组,必须至少为6。
*/
private static int RESIZE_STAMP_BITS = 16;
/**
* 可以帮助调整大小的最大线程数。必须符合32 - RESIZE_STAMP_BITS位。
*/
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
/**
* 在sizeCtl中记录尺寸戳的位移。
*/
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
/*
* 节点哈希字段的编码
*/
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
// 0x7fffffff 为int的最大整数值,一个数和0x7fffffff做&运算可以确保返回的是正数,hash函数中会用到,因为负数concurrentHashMap中有其他含义
static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash
/** cpu的数量,以设置某些大小的界限 */
static final int NCPU = Runtime.getRuntime().availableProcessors();
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
}
/**
* 桶的数组。在第一次插入时惰性初始化。大小永远是2的幂。由迭代器直接访问。
*/
transient volatile Node<K,V>[] table;
/**
* 下一个要使用的表;仅在调整大小时为非空。
*/
private transient volatile Node<K,V>[] nextTable;
/**
* 基本计数器值,主要在没有争用时使用,但在表初始化竞争期间也用作回退。经CAS更新。
*/
private transient volatile long baseCount;
/**
* 表初始化和调整大小控件。当为负值时,表正在初始化或调整大小,否则-(1 +活动调整大小线程的数量)。否则,当table为null时,将保留创建时使用的初始表大小,或者保留默认值为0。初始化后,保存要调整表大小的下一个元素计数值。
sizeCtl < -1 : 容器正在扩容,高16位存储SizeStamp (可以理解为扩容版本号,用以区分连续的多次扩容),低16位代表着有n-1个线程正在参与扩容
sizeCtl == -1 : 表示table正在初始化
sizeCtl == 0 : 表示初始值
sizeCtl > 0 : table数组如果未初始化则表示初始化的容量,如果已经初始化则表示下次扩容时的阈值
*/
private transient volatile int sizeCtl;
/**
*调整大小时要分割的下一个表索引(加一个)。
*/
private transient volatile int transferIndex;
/**
* 自旋锁(通过CAS锁定)在调整大小和/或创建CounterCells时使用。
*/
private transient volatile int cellsBusy;
/**
* 计数单元表。当非空时,size是2的幂。
*/
private transient volatile CounterCell[] counterCells;
构造方法
//空参构造
public ConcurrentHashMap() {
}
//带有容量的构造函数
public ConcurrentHashMap(int initialCapacity) {
if (initialCapacity < 0)
throw new IllegalArgumentException();
int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
MAXIMUM_CAPACITY :
tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
this.sizeCtl = cap;
}
//传入map结构的构造函数
public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
this.sizeCtl = DEFAULT_CAPACITY;
putAll(m);
}
//带有容量以及负载因子的构造函数
public ConcurrentHashMap(int initialCapacity, float loadFactor) {
this(initialCapacity, loadFactor, 1);
}
//带有容量以及负载因子和并发级别的构造函数
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;
}
put方法
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();
//1.hash算法
int hash = spread(key.hashCode());
//计算桶内节点个数
int binCount = 0;
//2.死循环,内部通过break跳出
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
//3.如果数组为空或者数组长度为0则初始化数组
if (tab == null || (n = tab.length) == 0)
tab = initTable();
//4.通过Unsafe操作获取指定数组下标的元素如果为null则先创建元素并通过cas来插入节点,成功则break,否则继续自旋
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
}
//5.如果发现此时元素hash值为MOVED(-1),则证明此时有线程在对数组扩容,此时会先去帮助扩容,扩容后再次尝试插入元素
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
//6.走到这里证明上述条件都不满足,此时会去添加可重入锁,锁对象为桶内第一个元素
// - 数组不为空
// - 桶内首个元素不为空
// - 当前桶内没有在进行扩容
//下面针对的情况就是桶内结构为链表还有红黑树了
else {
V oldVal = null;
synchronized (f) {
//7. 加锁之后再次判断此时没有对桶内第一个元素进行更改
if (tabAt(tab, i) == f) {
//8 hash值大于等于0证明此时桶内结构为链表,因为如果是红黑树的话,此时首个节点应该是 //TreeBin,hash值为-2
if (fh >= 0) {
binCount = 1;
//8.1 此时遍历链表
for (Node<K,V> e = f;; ++binCount) {
K ek;
//8.2 遍历的过程中找到了指定key,判断是否需要覆盖,onlyIfAbsent如果为false则覆盖,然后 //break
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
//8.3 遍历到了链表的尾端还没有找到,此时创建节点然后通过尾插法将节点插入,然后break
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
//9. 桶内结构为红黑树,红黑树的话则调用红黑树的putTreeVal方法插入元素
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;
}
}
}
}
//10. 如果binCount != 0 则证明一定进入到了链表和红黑树的逻辑中,binCount >= TREEIFY_THRESHOLD(8),则证明链表的长度最少为7,此时转为红黑树
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
//11. 添加成功元素后进行计数,要添加的线程可能会很多,因为没有加锁,所以内部通过baseCount 和 CountCell数组来优化线程并发计数 . 除了元素的计数还有扩容的逻辑
addCount(1L, binCount);
return null;
}
spread
hash函数 : 最后和HASH_BITS (2的31次方-1) 进行&运算,可以确保返回的数肯定是正数,因为负数有额外的用处
static final int spread(int h) {
return (h ^ (h >>> 16)) & HASH_BITS;
}
initTable
初始化table数组 (通过自旋 + cas的方式来进行的)
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
//1.数组没有被创建好就一直循环
while ((tab = table) == null || tab.length == 0) {
//2.如果sizeCtl < 0 则证明有线程在进行初始化,此时让出cpu使用权(cpu不一定听)
if ((sc = sizeCtl) < 0)
Thread.yield(); // lost initialization race; just spin
//3.第一个进来的线程会将sizeCtl由0转变为-1,表示此时有线程正在进行初始化了,其他线程先不用管
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
//4.再次判断数组是否为空,double check,如果不为空则创建数组
if ((tab = table) == null || tab.length == 0) {
//5.sc不大于0此时用默认的容量 (16)
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
//6.创建数组
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
//7.将创建好的数组赋值给内部属性
table = tab = nt;
//8.sc此时等于n * 0.75 (n - (n >>> 2) === n * 0.75)
sc = n - (n >>> 2);
}
} finally {
//9.将sc赋值给sizeCtl
sizeCtl = sc;
}
break;
}
}
return tab;
}
helpTransfer
当插入元素时发现元素hash值为MOVED(-1)时,此时当前线程先去帮助扩容,扩容完毕之后再去插入元素
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
Node<K,V>[] nextTab; int sc;
//1.数组不为空 && 当前桶第一个元素节点类型为ForwardingNode, && ForwardingNode节点内部的新数组是不为空的 (正常情况确实是这样的,因为假设这是第二个线程,第一个线程先把这个桶内的元素都给转移完了,此时老数组的当前位置就是ForwardingNode,新数组也是有值的,极端情况可能发生变化,这里加判断应该是为了严谨性)
if (tab != null && (f instanceof ForwardingNode) &&
(nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
//2.获取扩容版本号
int rs = resizeStamp(tab.length);
//和addCount方法中类似,下面有详细讲解
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.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
transfer(tab, nextTab);
break;
}
}
return nextTab;
}
return table;
}
resizeStamp
Integer.numberOfLeadingZeros(n) : 用以返回二进制中前导0的个数
- 如果 n 是 0,那么返回值是 32(因为整数是 32 位的,所以有 32 个前导零)。
- 如果 n 是 1,那么返回值是 31(因为只有一个位是 1,其余都是零)。
- 如果 n 是 2,那么返回值是 30(因为只在倒数第二位为1,即 10,其余位都是零)。
- 如果 n 是 8,那么返回值是 28(因为只在倒数第四位为1,即 1000,其余位都是零)。
static final int resizeStamp(int n) {
return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
}
这个返回的值在扩容的时候会用到 (主要会将这个值左移16位,将低16位移到高位上,然后再加2,低16位末尾为10,表示有 (n-1)个线程正在扩容)
TreeBin.putTreeVal
添加红黑树节点,和1.8hashMap类似
final TreeNode<K,V> putTreeVal(int h, K k, V v) {
Class<?> kc = null;
boolean searched = false;
for (TreeNode<K,V> p = root;;) {
int dir, ph; K pk;
if (p == null) {
first = root = new TreeNode<K,V>(h, k, v, null, null);
break;
}
else if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
else if ((pk = p.key) == k || (pk != null && k.equals(pk)))
return p;
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0) {
if (!searched) {
TreeNode<K,V> q, ch;
searched = true;
if (((ch = p.left) != null &&
(q = ch.findTreeNode(h, k, kc)) != null) ||
((ch = p.right) != null &&
(q = ch.findTreeNode(h, k, kc)) != null))
return q;
}
dir = tieBreakOrder(k, pk);
}
TreeNode<K,V> xp = p;
if ((p = (dir <= 0) ? p.left : p.right) == null) {
TreeNode<K,V> x, f = first;
first = x = new TreeNode<K,V>(h, k, v, f, xp);
if (f != null)
f.prev = x;
if (dir <= 0)
xp.left = x;
else
xp.right = x;
if (!xp.red)
x.red = true;
else {
lockRoot();
try {
root = balanceInsertion(root, x);
} finally {
unlockRoot();
}
}
break;
}
}
assert checkInvariants(root);
return null;
}
treeIfyBin
链表转红黑树
private final void treeifyBin(Node<K,V>[] tab, int index) {
Node<K,V> b; int n, sc;
//1.如果数组不为空
if (tab != null) {
//2.如果数组长度小于64则先扩容,大于等于64则尝试去转为红黑树
if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
tryPresize(n << 1);
//3.转化为红黑树之前再次判断此时桶内第一个元素不是空的,并且hash值也是大于等于0的
else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
//4.对桶内首个元素进行加锁
synchronized (b) {
//5.double check判读此时桶内首个元素没有发生变化
if (tabAt(tab, index) == b) {
//6.将Node转变为TreeNode并形成一个新的单向链表
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;
}
//7.将新的单向链表通过构造函数传递给TreeBin,TreeBin内部会将单向链表给构建成红黑树,TreeBin作为一个封装红黑树的这么一个节点
setTabAt(tab, index, new TreeBin<K,V>(hd));
}
}
}
}
}
addCount
内部包含元素计数以及map数组扩容的逻辑
/**
* x : 计数增加的值
* check :
* 如果是put方法走链表逻辑的话,这里传入的是链表的长度,如果走红黑树逻辑,这里传入2
* 如果是remove方法,addCount方法的两个参数都是-1
*/
private final void addCount(long x, int check) {
//as : 局部CountCell数组
//b : baseCount
//s : 为map中table数组的所有元素数量
CounterCell[] as; long b, s;
//条件一 : 将内部volatile修饰的属性counterCells数组赋值给局部变量as,并判断as是否不等于null
//条件二 : 因为是或运算,只有as数组是空的,才会执行当前条件,尝试通过cas将baseCount += x,cas执行失败则进入if条件中,否则进入下面map中table数组扩容的逻辑
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
//a : 当前线程在CountCell数组中操作的元素
//v : 当前线程在CountCell数组中操作的元素的value值
//m : countCell数组的长度
CounterCell a; long v; int m;
//uncontended : cas更新时是否有竞争 . true : 没有竞争 false : 有竞争
boolean uncontended = true;
//条件一,二 : countCell数组为空,长度为0
//条件三 : (以上条件都不满足)ThreadLocalRandom.getProbe()获得一个当前线程有关的hash值,和countCell数组进行&运算,获取其线程在数组中对应的位置,如果这个位置为空则证明当前线程还没有对数组该位置进行初始化
//条件四 : (以上条件都不满足)尝试通过cas来对当前线程在CountCell数组中的元素进行累加操作,失败则进入fullAddCount方法,成功则不进入if块
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的条件有哪些,其实也就是fullAddCount方法内部到底要干些什么事情
//1.countCell数组为空 --> 需要对countCell数组进行初始化
//2.通过cas对baseCount累加失败 --> 因为baseCount竞争很激烈,所以需要进入fullAddCount方法确保肯定可以累加上
//3.当前线程在CountCell数组中的对应的元素为空,需要进行初始化
//4.通过cas来对CountCell中的元素进行累加时候失败了
fullAddCount(x, uncontended);
return;
}
//check为是否扩容的标识变量
//如果是put方法传进来的check就等于binCount
//binCount >= 1 当前命中桶链表的长度
//binCount == 0 当前命中的桶为null
//binCount == 2 当前桶位置已经树化
//如果是remove方法则传入的是-1
if (check <= 1)
return;
//获取当前table数组的元素个数
s = sumCount();
}
//这里表示调用的地方一定是put方法中,以下代码主要为扩容
if (check >= 0) {
//tab : 哈希表的局部table数组 , nt : 扩容后的新数组 , n : 扩容前数组长度 sc : sizeCtl的局部变量
Node<K,V>[] tab, nt; int n, sc;
//条件一 :
// 如果sizeCtl为正数 则表示 : 当前元素的数量超过了要扩容的阈值
// 如果sizeCtl为负数 则表示 : 此时正在扩容中
//条件二 : table数组不为空,基本都成立
//条件三 : 将table数组的长度赋值给n并判断是否越界,因为越界的话就不能扩容了
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
//当前线程扩容的版本号 : 如果是16 -> 32 ,
//rs = 32795(十进制) 二进制 :0000 0000 0000 0000 1000 0000 0001 1011
int rs = resizeStamp(n);
//这里的条件如果满足证明此时table数组正在扩容,当前线程理论上应该协助其他线程完成扩容
if (sc < 0) {
//条件一 : (sc >>> RESIZE_STAMP_SHIFT) != rs
// true : 当前线程获取到的扩容版本号非本次扩容 (数组长度如果发生变化,那么扩容的版本就不一样)
// false : 当前线程获取到的版本号是本次扩容
//条件二 : jdk8中有bug,sc == rs + 1这个条件不对,应该是sc == (rs << 16 + 1)
// 下面带大家来推导出这个正确条件的过程
// 我们通过else if条件可以知道,如果第一个线程对数组开始扩容了,那么会将rs右移16位,根据上面的rs二进制来 // 计算的话,如下所示
// rs : 0000 0000 0000 0000 1000 0000 0001 1011,
// 右移16位 : 1000 0000 0001 1011 0000 0000 0000 0000
// 右移16位之后会再加2
// +2 : 1000 0000 0001 1011 0000 0000 0000 0010,
// 此时会将最终计算好的结果赋值给sizeCtl,
// 这里我们发现,如果sizeCtl为负数的话,那么低16位的值 - 1 就是参与线程的数量
// 假如此时就一个线程,那么它在transfer()方法中执行完扩容后,会将sizeCtl - 1,
// sizeCtl : 1000 0000 0001 1011 0000 0000 0000 0010
// ` -1 : 1000 0000 0001 1011 0000 0000 0000 0001
// 此时我们就会发现,这个减一之后的sizeCtl也就是sc和下面这个条件的判断中是一样的,就代表着此时没有 // 线程在进行扩容了,扩容都已经完成了
// 第二个条件sc == (rs << 16) + 1
// 如果为true则表示此时没有线程在扩容了,扩容结束了
// 如果为false则表示当前线程可以参与进来
//条件三 : 这个其实也是有bug的 rs 也是没有进行左移16位,应该是(rs << 16) + MAX_RESIZERS (2的16次方 //- 1),如果为true则表示超过了最大线程数,如果为false则表示当前线程可以参与进来
//条件四 : nextTable为空表示当前扩容已经结束了
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
//如果上述条件都不满足证明当前线程可以来参与扩容
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
//条件成立证明当前线程是触发扩容的第一个线程,此时nextTable为空,上面条件二中对这里rs的左移进行了详细的介绍,这 //里就不解释了
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
s = sumCount();
}
}
}
fullAddCount
private final void fullAddCount(long x, boolean wasUncontended) {
int h;
//1.获取当前线程的随机数为空则初始化
if ((h = ThreadLocalRandom.getProbe()) == 0) {
ThreadLocalRandom.localInit(); // force initialization
h = ThreadLocalRandom.getProbe();
wasUncontended = true;
}
boolean collide = false; // True if last slot nonempty
//自旋
for (;;) {
CounterCell[] as; CounterCell a; int n; long v;
//2.cell数组不为空的情况
if ((as = counterCells) != null && (n = as.length) > 0) {
//2.1 当前线程在数组中对应位置的元素为null,以下需要先创建cell元素
if ((a = as[(n - 1) & h]) == null) {
//尝试添加cell元素
if (cellsBusy == 0) { // Try to attach new Cell
//采用乐观锁模式,先创建,等最后赋值前再判断
CounterCell r = new CounterCell(x); // Optimistic create
//如果当前数组没有人在操作,那就尝试获取cell数组的操作权 (cellsBusy改为1)
if (cellsBusy == 0 &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
//是否创建成功
boolean created = false;
try { // Recheck under lock
//再次校验数组不为空以及对应位置元素没有被初始化,如果有的话当前线程就不添加了,否则可能会导 //致覆盖
CounterCell[] rs; int m, j;
if ((rs = counterCells) != null &&
(m = rs.length) > 0 &&
rs[j = (m - 1) & h] == null) {
//添加到元素中
rs[j] = r;
//更新标志位
created = true;
}
} finally {
//复原cellsBusy
cellsBusy = 0;
}
//成功添加元素后break,否则继续自旋
if (created)
break;
continue; // Slot is now non-empty
}
}
collide = false;
}
//3.如果有冲突的话,先更新冲突标志位,然后再重新自旋
else if (!wasUncontended) // CAS already known to fail
wasUncontended = true; // Continue after rehash
//4.更新了标志位以后再次通过cas来尝试增加计数值,如果竞争不激烈就break,还不行就继续自旋,反正必须得成功才能退 //出
else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
break;
//5.如果已经有其他线程对cell数组扩容了或者cell数组长度 大于等于 cpu的数量的话更新collide的值,进入下一次自 //旋
else if (counterCells != as || n >= NCPU)
collide = false; // At max size or stale
//6.如果collide为false的话就改为true,表示下一次自旋具备了扩容的资格了
else if (!collide)
collide = true;
//7.尝试对cell数组进行扩容
else if (cellsBusy == 0 &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
try {
//确保数组没有发生改变
if (counterCells == as) {// Expand table unless stale
//按照两倍来扩容
CounterCell[] rs = new CounterCell[n << 1];
//拷贝之前的元素
for (int i = 0; i < n; ++i)
rs[i] = as[i];
//更新扩容后的数组
counterCells = rs;
}
} finally {
cellsBusy = 0;
}
//扩容后将标志位改为false
collide = false;
//使用扩容后的cell数组继续重试
continue; // Retry with expanded table
}
//重新计算随机值
h = ThreadLocalRandom.advanceProbe(h);
}
//8.如果cell数组为空这里先看下有没有线程在操作cell数组 (cellsBusy == 0) 以及确认cell数组没有被更改
//没有线程在操作cell数组的话该线程通过cas尝试更新cellsBusy为1,之后创建数组
else if (cellsBusy == 0 && counterCells == as &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
//是否初始化成功
boolean init = false;
try { // Initialize table
//再次确认cell数组没有被其他线程初始化
if (counterCells == as) {
//创建长度为2的数组
CounterCell[] rs = new CounterCell[2];
//计算当前线程在数组中的位置,并在对应位置初始化cell对象
rs[h & 1] = new CounterCell(x);
//将局部变量赋值给内部属性
counterCells = rs;
//更改初始化标志
init = true;
}
} finally {
//初始化完之后更新cellsBusy标志
cellsBusy = 0;
}
//初始化成功break
if (init)
break;
}
//9.如果当前cell数组是空的,并且有线程正在创建cell数组了,那么当前线程尝试对baseCount进行累加
else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
break; // Fall back on using base
}
}
transfer
table数组扩容方法
1.8concurrentHashMap的扩容方法是支持多个线程同时来扩容的,1.7也支持是因为每个线程都只对自己的segment中的数组来进行扩容,1.8是对同一个数组,具体思路 :
- 首先如果扩容数组如果没有被创建则创建2倍容量的数组
- 然后会进入一个自旋遍历
- 首先会计算步长,将扩容任务分段,从后向前开始转移
- 如果遍历到的元素为空则置为fwd (ForwardingNode),遍历到已经处理过的则重新更新遍历元素索引和区间值
- 之后就是两种情况,一种是链表一种是红黑树,这种桶需要对桶首个节点加synchronized锁 (红黑树锁的是TreeBin)
- 链表 : 遍历链表和1.7类似,重用扩容后仍在在同一个槽中的连续序列,这个序列可能是高位也可能是低位的,头结点为lastRun,然后再遍历链表,补全高低位链表,直到遍历到的元素等于lastRun就结束,最后将高低位链表放置到新数组i和i+n的位置上,最后将旧数组i位置置为fwd
- 红黑树 : 和链表类似,但是是构建高低位的双向链表,然后会分别统计两个链表的长度,如果长度小于等于6,则会取消树化,否则如果对立链表不为空则重新构建树结构,为空的话此时不需要进行改变,直接移树即可
- 最后扩容结束后先尝试用cas将sizeCtl-1,sizeCtl中低16位表示扩容的线程数量就少一个了,之后会在根据sizeCtl判断是不是最后一个线程,如果是则更新最新数组并计算扩容阈值给sizeCtl,不是最后一个线程直接返回
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
//根据cpu数量来决定步长 (如果是多核cpu那么stride = n / 8)
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // subdivide range
//如果传入的nextTab为空的话,证明是扩容的第一个线程来的
if (nextTab == null) { // initiating
try {
@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;
}
//赋值给成员属性nextTtable
nextTable = nextTab;
//transferIndex默认为之前旧表的容量
transferIndex = n;
}
//新表的容量
int nextn = nextTab.length;
//转移完一个元素后会将旧数组桶上第一个元素的位置置为ForwardingNode,其他线程在遍历旧数组put的时候也能感知到
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
//是否需要继续计算扩容的区间
boolean advance = true;
//当前线程扩容是否完毕
boolean finishing = false; // to ensure sweep before committing nextTab
//自旋
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
//这个while循环的目的是为了更新/初始化扩容区间
while (advance) {
int nextIndex, nextBound;
//对i减1后判断是否大于等于bound,
// 如果为true证明此时还没有完成次区间任务
// 如果为false的话就是当前区间任务已经完成了,或者是第一次进来这里
if (--i >= bound || finishing)
advance = false;
//transferIndex 小于等于0证明所有元素都已经转移完了
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
//通过cas尝试更新transferIndex的值
// 如果是第一次进来的话假设长度为16
// transferIndex = 14 (16 - 2)
// nextBound = 14
// nextIndex = 16
// i = 15
// bound = 14
//假设这个时候有另外一个线程也来参与了扩容
// transferIndex = 12 (14 - 2)
// nextBound = 12
// nextIndex = 14
// i = 13
// bound = 12
//这样就分成了两个块
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;
//检查一遍后没有问题走到这里,扩容结束后做后续工作,将nextTable设置为null,table设置为扩容后的新数组,sizeCtl //设置为下次扩容阈值
if (finishing) {
nextTable = null;
table = nextTab;
sizeCtl = (n << 1) - (n >>> 1);
return;
}
//第一次进来会先走这里将sizeCtl-1,代表sizeCtl低16位的线程数量-1
// 如果成功则会再次判断此时当前线程是不是扩容的最后一个线程
// 如果不是扩容的最后一个线程直接return
// 如果是最后一个线程则将finishing和advance都改为true,并将n赋值给i,重新遍历一遍数组recheck
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
}
}
//如果遍历到的节点为null则更新为fwd
else if ((f = tabAt(tab, i)) == null)
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) {
//runBit决定是高位还是低位 (高位为1则代表在新数组中的i + n,否则在新数组中的i)
int runBit = fh & n;
//lastRun为重用扩容后仍在同一个槽中的连续序列的头节点
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;
}
}
//runBit等于0则把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);
}
//将低位链表放置新数组i位置
setTabAt(nextTab, i, ln);
//将高位链表放置新数组i + n位置
setTabAt(nextTab, i + n, hn);
//旧数组i位置色之后为fwd
setTabAt(tab, i, fwd);
advance = true;
}
//红黑树情况和1.8hashMap类似
else if (f instanceof 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;
}
}
}
}
}
}
HashMap面试点
参考 : https://joonwhee.blog.csdn.net/article/details/106324537?spm=1001.2014.3001.5502
jdk7和8HashMap的区别
数据结构不同
jdk7的数据结构使用的是数组 + 链表 ,数组中每一个元素相当于是一个桶,一个桶可能为空,可能只有一个元素,可能有多个元素以链表来进行存储
jdk8的数据结构使用的是 **数组 + 链表(单链表,双向链表) + 红黑树** ,比jdk7多了一个双向链表和红黑树,红黑树主要用来解决链表过长导致的查询性能差,通过阈值来进行切换两个数据结构 (**如果链表长度大于8,并且数组长度大于等于64,则进行转为红黑树,如果小于64,则会采取扩容数组来尝试将链表尽可能缩短链表的长度**)
新元素插入的位置不同
jdk7采用的是头插法 (扩容转移元素的时候也是使用的头插法,头插法速度相对来说比较快,插入的时候无需遍历链表,但是在多线程扩容的时候使用头插法会出现循环链表的问题,循环链表会导致cpu占用率100%)
jdk8采用的是尾插法(jdk8中反正要去遍历到链表的尾部,正好也得统计节点的个数,反正都要遍历链表,所以直接使用了尾插法,也避免了循环链表的情况)
hash算法不同
jdk7采用的hash算法比较复杂,hash算法越复杂,其实生成的hashcode就越散列,hashcode更散列,发生hash冲突的几率就更小,那hashmap的查询性能就更好,jdk7中因为没有红黑树,所以只能优化hash算法jdk8中有了红黑树自然就不需要那么复杂的hash算法了,hash算法复杂了导致cpu的计算时间也会增加,虽然增加不大,但是肯定jdk8的hash算法比jdk7的hash算法要快些
扩容的步骤和条件不同
jdk7中采用的扩容是每次就转移一个元素,jdk7中除开判断size是否大于阈值以外,还判断了当前tab[i]是否为空,不为空的时候才会扩容,jdk8则没有了这个条件
jdk8中则是通过循环每一个节点,然后构建高低位的两个链表,最后统一进行转移
扩容后是否会对hash值重新计算
jdk7中有可能会对key进行重新hash(重新hash和哈希种子有关系),而jdk8中没有这个逻辑了
jdk7hash算法
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
jdk7和8的concurrentHashMap如何保证并发安全的
jdk 7 :
主要利用Unsafe操作 + ReentrantLock + 分段思想
主要使用了Unsafe操作中的一些方法 :
1.compareAndSwapObject : 通过cas的方式修改对象的某个属性
2.putOrderedObject : 并发安全的给数组的某个位置赋值
3.getOrderedObject : 并发安全的获取数组的某个位置
分段思想是为了提高concurrentHashMap的并发度,分段数越大则支持的最大并发量越高,程序员可以通过concurrentLevel参数来指定并发量,ConcurrentHashMap的内部类segment就是用来表示一个段的
每个segment就是一个小型的hashMap,当调用ConcurrentHashMap的put方式时,最终会调用到segment的put方法,而segment继承了ReetrantLock,所以segment自带可重入锁,当调用segment的put方式时,会利用可重入锁加锁,加锁成功后再将待插入的key,value插入到小型hashmap中,插入完成后解锁,锁的粒度是concurrentHashMap中segment数组的中的某一个节点
jdk 8 :
主要利用了Unsafe操作 + Synchronized关键字
Unsafe操作和jdk7中类似,主要负责并发安全的修改对象的属性值或数组中的值
synchronized主要负责在需要操作某个桶时 (该位置不为空)进行加锁,如果是链表则对链表头加锁,如果是红黑树则对treeBin来进行加锁