一、 JDK7中的HashMap
1. 数据结构
- 数组+链表
- 链地址法解决Hash冲突;
- 链表没有头结点,链表的第一个元素放在数组里,后续的元素用
next
指针连接起来;
2. 属性
2.1 成员变量源码
public class HashMap<K,V>
extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
{
// 静态常量:数组的默认初始容量,必须是2的幂次方,初始值16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
// 静态常量:数组的最大容量,2的30次方
static final int MAXIMUM_CAPACITY = 1 << 30;
// 静态常量:默认加载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 静态常量:空数组对象
static final Entry<?,?>[] EMPTY_TABLE = {};
// 分配的数组空间,长度必须是2的幂次方
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
// map中key-value的节点总数,包括在链表中的节点
transient int size;
// 触发map扩容的元素总数阈值
int threshold;
// map的加载因子
final float loadFactor;
// map中`key-value`添加或删除的次数
transient int modCount;
// 开启rehash的阈值的默认值
static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;
/**
* 计算hash值使用的hash种子
*/
transient int hashSeed = 0;
// 省略其他
// ...
}
transient
关键字的作用是序列化对象时,不序列化该属性;
2.2 加载因子
- 加载因子 = HashMap中存在的元素总数 / HashMap中数组长度;
- 注意:元素总数包括在链表上的元素数,数组长度是指当前分配的数组空间的大小;
- 加载因子默认是
0.75
;
2.3 modCount
- modCount记录Map中
key-value
添加或删除元素的次数,(修改key对应的value值不会影响modCount值); - 该属性的值作用是在使用迭代器遍历map时,如果元素个数有修改,则快速失败;
- 如果在迭代过程中需要删除或增加map中元素,可以使用迭代器的删除或增加方法;
3. 初始化
- 初始化时数组长度为
16
; - 构造方法可以指定初始化大小
size
,但是在具体初始化时会初始化为大于size
且值为2的幂次方的最小值; - HashMap的构造方法只是执行给成员变量赋值,调用的初始化方法
init()
为空方法,所以初始化时并未分配空间; - 在添加元素调用
put
方法时,会判断数组是否为空,如果为空,这时才去初始化数组;
3.1 构造方法初始化变量
构造方法只是初始化成员变量,并没有分配数组空间:
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();
}
void init() {
}
- 构造方法先初始化成员变量,然后调用
init
方法; init
方法为空方法,说明调用构造方法时并没有分配空间;
3.2 put第一个元素时分配空间
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
// 省略其他处理,put方法详细见下文
// ...
}
- put元素时,如果数组未分配空间,则通过
inflateTable
方法分配空间,未分配空间时,threshold
值为初始化大小;
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);
}
- 真实初始化的容量
capacity
会通过roundUpToPowerOf2
方法计算出大于toSize
且为2的幂次方的最小值; - 然后通过
table = new Entry[capacity]
初始化数组;
roundUpToPowerOf2
方法
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;
}
roundUpToPowerOf2
方法会调用Integer
类的highestOneBit
方法计算小于number
且值为2的幂次方的最大值;- 参数里
number-1
是当number刚好为2的幂次方时,计算出来的就是number;
4. 添加元素
4.1 静态内部类Entry
Entry
对象封装了要添加的节点,包含key
、value
、next
和hash
属性:
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash;
// 省略成员方法
// ...
}
final
修饰key
,key
值不可修改;next
属性用来构建链表;- Entry里会记录当前
key
计算的hash
值;
4.2 添加元素步骤
- 如果数组为空,则初始化;
- 根据
key
计算hash值hash = hash(key)
; - 根据
hash
计算数组下标index = hash(key)&(length-1)
; - 根据
index
找到链表,遍历链表,查找key
值是否已经存在,如果存在则替换value
值,返回旧的值oldValue
; - 如果
key
值未存在,则将新的key-value
生成的Entry
节点插入链表头部;
put
方法
put
方法负责放入key-value
;
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
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;
}
- 初始化:
put
方法里会判断数组是否为空,如果为空,则进行初始化,即map空间是在添加第一个元素时进行的初始化;构造方法只负责设置成员变量的值; - 同key覆盖:
put
方法会先循环变量key
对应的下标下的链表,如果key
值已经存在,则替换value
,将旧的值oldValue
返回; - 新key加节点:如果
key
值不存在,则使用addEntry
方法向链表添加新元素,添加时使用的是头插法; - key为null处理:如果
key==null
,则直接调用putForNullKey
方法处理放入key
为null
;
addEntry
方法
addEntry
方法将新的key-value
放入map:
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
添加元素之前,会先判断是否需要扩容,扩容条件是(size >= threshold) && (null != table[bucketIndex])
,即需要同时满足这两个条件才能扩容:
- 条件1:当前元素总量大于等于扩容阈值,
扩容阈值=数组长度*加载因子
; - 条件2:当前key值计算的数组下标中元素不为空,即当前要添加的
key-value
值由hash冲突;
createEntry
方法
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[bucketIndex] = new Entry<>(hash, key, value, e);
该行代码使用的是头插法;
putForNullKey
方法
putForNullKey
方法,负责添加Key为null节点:
private V putForNullKey(V value) {
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(0, null, value, 0);
return null;
}
- HashMap支持Key为
null
的节点,该节点存储在数组下标为0
的链表中;
4.3 根据key值计算数组下标
计算hash值
hash
方法计算key的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);
}
- 最终调用的是
Object
的hashCode
方法计算key
的hash值; - 移位异或是进行折叠运算,使hash值的高位都参与到计算
index
里(因为计算index是通过与数组length
位运行,length
数字可能很小,hash折叠一下可以让高位也参与到index
的计算,从而提高hash散列性);
计算index
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);
}
- hash值与数组长度减一
lenght-1
进行按位或运算,效果就是对数组长度取模; - 数组
length
必须是2的幂次方,位运算才能达到取模的效果; - 位运算速度更快;
5. 扩容
5.1 扩容步骤
- 添加新节点时,根据扩容条件判断是否需要扩容;
- 如果需要扩容,调用
resize
方法进行扩容,新的table
长度为当前table的2倍; - 创建完新的table后,调用
transfer
方法,将原来table中的元素进行移动,根据initHashSeedAsNeeded(newCapacity)
方法返回值判断是否需要重新hash,如果不需要rehash
,则直接按照Entry
里存放的hash
计算index
addEntry
方法判断扩容条件
添加新元素时,会判断是否需要扩容,扩容条件在addEntry
方法里判断
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
根据if条件可以看到,扩容需要同时满足两个条件:
- 条件1:当前元素总量大于等于扩容阈值,
扩容阈值=数组长度*加载因子
; - 条件2:当前key值计算的数组下标中元素不为空,即当前要添加的
key-value
有hash冲突;
resize
方法执行扩容
addEntry
方法里满足扩容条件时,调用resize
方法进行扩容,参数是当前table
长度的2倍
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = new Entry[newCapacity];
transfer(newTable, initHashSeedAsNeeded(newCapacity));
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
- 扩容时,先创建一个新的数组,然后调用
transfer
方法将原有的元素移动到新数组里; transfer
方法的第二个参数控制移动元素时,是否需要重新rehash
,如果不需要rehash,则根据Entry
记录的hash值,直接重新计算index
即可;
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;
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;
}
}
}
- 扩容时,链表里的元素依然使用头插法插入新的数组下的链表里,结果会产生扩容后链表中元素顺序颠倒;
- 如果不触发
rehash
,则扩容后元素在新数组中的index位置为oldIndex
或oldIndex+oldTableSize
;
5.2 扩容时是否rehash
扩容时,根据扩容后table的容量调用initHashSeedAsNeeded
方法,根据返回值确定是否需要rehash
:
final boolean initHashSeedAsNeeded(int capacity) {
boolean currentAltHashing = hashSeed != 0;
boolean useAltHashing = sun.misc.VM.isBooted() &&
(capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
boolean switching = currentAltHashing ^ useAltHashing;
if (switching) {
hashSeed = useAltHashing
? sun.misc.Hashing.randomHashSeed(this)
: 0;
}
return switching;
}
hashSeed
初始值为0,所以currentAltHashing
为false
;- 并且,只有当
switch
为true
且useAltHashing
也为true
时,才会修改hashSeed
;而useAltHashing
为true
时,要想switch
也为true
,则要求hashSeed==0
,所以只有在第一次开启rehash
时,才会修改hashSeed
; sun.misc.VM.isBooted()
即虚拟机是否启动,为true
;Holder.ALTERNATIVE_HASHING_THRESHOLD
是开启rehash
的table
容量最大阈值,即当table
容量大于等于该值时,开启rehash
,由内部类Hold
管理;该值的默认值是ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;
说明一般情况下不会开启rehash;- 所以
currentAltHashing=false
并且useAltHashing=false
,异或运算结果为false
,即switching
为false
; - 结论:一般情况下不开启
rehash
,只有当扩容后新table
的容量超过了需要开启rehash的阈值时,才会开启rehash
;
5.3 rehash阈值修改
HashMap的静态内部类Holder
管理ALTERNATIVE_HASHING_THRESHOLD
的值:
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 {
// altThreshold不为null时,使用altThreshold,否则使用ALTERNATIVE_HASHING_THRESHOLD_DEFAULT
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;
}
}
altThreshold
可以用来修改开启rehash
的阈值,该值在jdk.map.althashing.threshold
里配置,如果没有配置,则使用ALTERNATIVE_HASHING_THRESHOLD_DEFAULT
,见HashMap属性一节,该值默认为Integer.MAX_VALUE
二、 JDK7中的ConcurrentHashMap
- 不支持null:JDK7中ConcurrentHashMap存放的
key
和value
都不能为null
,否则会抛异常; - 线程安全:JDK7中的
ConcurrentHashMap
是线程安全的,使用的是分段加锁的方式,即ConcurrentHashMap
中维护一个Segment
数组,Segment
内部维护一个HashEntry
数组,相当于将HashEntry
数组切割为多段,在同一个Segment
中的HashEntry
数组使用同一把锁; Segment
内部类:Segment
类继承了ReentrantLock
类,所有JDK7中的ConcurrentHashMap
使用的是ReentrantLock
锁;- 分段扩容:添加元素触发扩容时只扩容对应
Segment
对象中的HashEntry
数组,不影响其他对象中的数组(除非扩容的是Segment
数组中的第0号元素,因为该元素作为创建其他Segment
元素的模板); - Segment数组长度不变:
ConcurrentHashMap
初始化后,Segment
数组的长度不再变化; - Segment数组长度:
ConcurrentHashMap
中Segment
数组的长度为大于等于concurrentLevel
且值为2的幂次方的最小值,默认值为16
; - HashEntry数组长度:
Segment
对象中HashEntry
数组的长度为initialCapacity
除以Segment
数组的商向上取整,然后找到大于等于该商且值为2的幂次方的最小值,如果该值小于2,则默认取值为2,默认值是2
;
1. Hashtable存在的问题
- Hashtable是从JDK1.0加入的并发安全的Map;
- Hashtable直接在所有的方法上加
synchronized
的关键字,将整个方法变成同步方法,虽然线程安全,但是效率不高; ConcurrentHashMap
替代了Hashtable
实现一个更高效的线程安全的Map;
2. 属性字段
public class ConcurrentHashMap<K, V> extends AbstractMap<K, V>
implements ConcurrentMap<K, V>, Serializable {
/* ---------------- Constants -------------- */
static final int DEFAULT_INITIAL_CAPACITY = 16;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认的 concurrentLevel
static final int DEFAULT_CONCURRENCY_LEVEL = 16;
// 每个分段数组的最小容量,必须是2的幂次方
static final int MIN_SEGMENT_TABLE_CAPACITY = 2;
// 最大分段量,必须是2的幂次方,且小于2的24次方
static final int MAX_SEGMENTS = 1 << 16; // slightly conservative
// 获取锁的重试次数
static final int RETRIES_BEFORE_LOCK = 2;
/* ---------------- Fields -------------- */
private transient final int hashSeed = randomHashSeed(this);
// 根据index计算分段值的掩码
final int segmentMask;
final int segmentShift;
// 分段,每个分段都是一部分数组
final Segment<K,V>[] segments;
transient Set<K> keySet;
transient Set<Map.Entry<K,V>> entrySet;
transient Collection<V> values;
// 省略方法
// ...
}
ConcurrentHashMap
内部维护了一个Segment
数组;
Segment
内部类
// 内部类Segment
static final class Segment<K,V> extends ReentrantLock implements Serializable {
static final int MAX_SCAN_RETRIES =
Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;
transient volatile HashEntry<K,V>[] table;
transient int count;
transient int modCount;
transient int threshold;
final float loadFactor;
// 省略方法
// ...
}
Segment
对象内部维护了loadFactor
、threshold
、modCount
等属性和一个HashEntry
数组;
HashEntry
内部类
static final class HashEntry<K,V> {
final int hash;
final K key;
volatile V value;
volatile HashEntry<K,V> next;
// 省略方法
// ...
}
3. 初始化
3.1 构造方法初始化
public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
/**
* #1 concurrentLevle最大值 1<<16 即,2的16次方
*/
if (concurrencyLevel > MAX_SEGMENTS)
concurrencyLevel = MAX_SEGMENTS;
// Find power-of-two sizes best matching arguments
int sshift = 0;
int ssize = 1;
/**
* #2 ssize为大于等于concurrentLevel且为2的幂次方的最小值
*/
while (ssize < concurrencyLevel) {
++sshift;
ssize <<= 1;
}
this.segmentShift = 32 - sshift;
this.segmentMask = ssize - 1;
/**
* #3 initialCapacity最大值为1<<30 即,2的30次方
*/
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
/**
* #4 c等于初始化容量除以大于等于concurrentLevel且为2的幂次方的最小值;如果除法结果有余数,则c再加1;
* ssize是2的幂次方,initialCapacity不一定是2的幂次方
*/
int c = initialCapacity / ssize;
if (c * ssize < initialCapacity)
++c;
/**
* #5 cap初始化为segment中table的最小容量,即2
*/
int cap = MIN_SEGMENT_TABLE_CAPACITY;
/**
* #6 如果cap小于c,则计算为大于等于c且值为2的幂次方的最小值
*/
while (cap < c)
cap <<= 1;
// create segments and segments[0]
/**
* #7 创建 Segment对象,Segment对象中HashEntry数组元素个数为cap个
*/
Segment<K,V> s0 =
new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
(HashEntry<K,V>[])new HashEntry[cap]);
/**
* #8 创建Segment数组,数组元素个数为ssize
*/
Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
this.segments = ss;
}
默认初始化参数执行过程
默认初始化参数,initialCapacity
为16
,concurrencyLevel
也为16
:
- #2结束:
ssize=16
,设置ssize
为大于等于concurrencyLevel
且为2的幂次方的最小值; - #4结束:
c=1
,ssize=16
,设置c
为initialCapacity
除以ssize
的商值,如果有余数再加1; - #5结束:
cap=2
,c=1
,ssize=16
- #6结束:
cap=2
,c=1
,ssize=16
,设置cap
为大于等于c
且值为2的幂次方的最小值,且不小于2; - #7结束:Segment对象中
HashEntry
数组长度为cap=2
; - #8结束:ConcurremtHashMap中
Segment
数组长度为ssize=16
;
自定义初始化参数执行过程
调用构造函数,传入初始化参数,initialCapacity
为50
,concurrencyLevel
也为10
:
- #2结束:
ssize=16
- #4结束:
c=4
,ssize=16
- #5结束:
cap=2
,c=4
,ssize=16
- #6结束:
cap=4
,c=4
,ssize=16
- #7结束:Segment对象中HashEntry数组长度为
cap=4
; - #8结束:ConcurremtHashMap中Segment数组长度为
ssize=16
;
总结:
- ConcurrentHashMap中
Segment
数组的长度为:大于等于concurrentLevel
且值为2的幂次方的最小值;默认为16
- Segment对象中
HashEntry
数组的长度为:大于等于(initialCapacity
除以ConcurrentHashMap
中Segment
数组长度的商向上取整)且值为2的幂次方的最小值,且不小于2;初始默认值为2; - 所以ConcurrentHashMap如果使用默认构造方法,则初始化后ConcurrentHashMap中Segment数组有16个元素,每个Segment元素中HashEntry数组有2个元素,共有
16*2
个HashEntry元素; - 初始化一个
Segment0
对象的作用是:记录计算出来的Segment
对象相关的参数,在创建其他Segment对象时,直接使用0号位置的Segment对象属性即可;
构造方法初始化的数据
构造方法执行了以下操作:
- 确定了
ConcurrentHashMap
中Segment
数组的大小; - 确定了
Segment
对象中HashEntry
数组的大小; - 初始化ConcurrentHashMap的
Segment
数组; - 初始化一个
Segment
对象放到Segment
数组的0
号位置;
4. 添加元素
- 添加元素也是使用的头插法;
4.1 添加元素步骤
- 根据
key
值计算hash
值; - 根据
hash
值计算元素在Segment
数组的下标j
; - 根据下标
j
找到Segment
对象,如果不存在则创建; - 调用
Segment
对象的put
方法添加元素; - Segment的
put
方法根据hash
值计算出key
在HashEntry
数组的下标;
ConcurrentHashMap的put
方法
public V put(K key, V value) {
Segment<K,V> s;
if (value == null)
throw new NullPointerException();
int hash = hash(key);
int j = (hash >>> segmentShift) & segmentMask;
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);
}
value==null
时,会抛出异常,说明ConcurrentHashMap不能存放value=null
的节点;(hash >>> segmentShift) & segmentMask
的作用是:取hash
值的高位,计算节点在Sgement
数组的下标j
;- 计算元素在
Segment
数组中的位置使用hash
的高位,而计算元素在HashEntry
数组中的位置使用hash
的低位,是为了提高散列性,如果计算两个数组下标使用的是相同的高位或低位,则进入Segment
对象的hash
值计算的HashEntry
的下标大概率会相同,从而导致Segment
对象中HashEntry
数组不散列; Segment<K,V>)UNSAFE.getObject(segments, (j << SSHIFT) + SBASE)
的作用是从数组segments
里取出下标为j
的元素;s = ensureSegment(j);
的作用是,如果segments
数组中下标为j
的元素不存在,则创建一个Segment
对象;- 最后调用
Segment
对象的put
方法将元素添加到Segment
中的HashEentry
数组中;
生成Segment对象的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;
if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
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);
HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
== null) { // recheck
Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
== null) {
if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
break;
}
}
}
return seg;
}
(Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)
的作用是从ss
数组里获取下标为k
(下标不是u
,u
是根据k
计算出来的偏移量)的Segment
对象;- 方法中判断了三次
(seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null
,最后使用UNSAFE
的compareAndSwapObject
操作,说明创建Segment
处理多线程并发不是使用锁,而是使用自旋锁
(while
循环)+CAS
操作(比较与交换,乐观锁) - 创建
Segment
对象时,根据ss[0]
对象作为模板,这里解释了为什么在构造方法初始化时会初始化一个ss[0]
对象;
Segment的put
方法
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value);
V oldValue;
try {
HashEntry<K,V>[] tab = table;
int index = (tab.length - 1) & hash;
HashEntry<K,V> first = entryAt(tab, index);
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 {
if (node != null)
node.setNext(first);
else
node = new HashEntry<K,V>(hash, key, value, first);
int c = count + 1;
// #1 扩容
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
rehash(node);
else
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
unlock();
}
return oldValue;
}
Segment
类继承在ReentrantLock
,在添加元素之前先tryLock
加锁,添加完成后unlock
解锁;tryLock
尝试获取锁失败后不会阻塞,会立即返回false
;lock
方法获取锁失败后会阻塞等待;#1
位置判断是否需要扩容的条件是(c > threshold && tab.length < MAXIMUM_CAPACITY)
scanAndLockForPut
方法重新尝试获取锁
private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
HashEntry<K,V> first = entryForHash(this, hash);
HashEntry<K,V> e = first;
HashEntry<K,V> node = null;
int retries = -1; // negative while locating node
while (!tryLock()) {
HashEntry<K,V> f; // to recheck first below
if (retries < 0) {
if (e == null) {
if (node == null) // speculatively create node
node = new HashEntry<K,V>(hash, key, value, null);
retries = 0;
}
else if (key.equals(e.key))
retries = 0;
else
e = e.next;
}
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;
}
}
return node;
}
- 如果
put
方法中第一次尝试获取锁tryLock
失败,则使用scanAndLockForPut
方法重试tryLock
,并且在重试过程提取创建Node
节点; - 如果尝试次数达到了最大次数,则直接调用
lock
方法,阻塞等待锁; lock
方法获取锁失败会线程阻塞,不会占用cpu资源,而如果在while
循环里不断使用tryLock
方法尝试获取锁,则会一直占用cpu资源(自旋锁
)
5. 扩容
每一个Segment
对象是一个独立的存放HashEntry
的Map,扩容时是扩容Segment
对象里的HashEntry
数组;
扩容条件
Segment的put
方法里判断是否需要扩容,条件是(c > threshold && tab.length < MAXIMUM_CAPACITY)
,即添加元素后Segment
中table
数组长度大于扩容阈值threshold
且table
长度小于最大长度时执行扩容;
扩容方法 rehash
private void rehash(HashEntry<K,V> node) {
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;
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;
/**
* #1 如果链表中只有一个元素,则直接将元素移动到新数组
*/
if (next == null) // Single node on list
newTable[idx] = e;
else { // Reuse consecutive sequence at same slot
HashEntry<K,V> lastRun = e;
int lastIdx = idx;
/**
* #2 将链表最后新下标相同的节点直接移动到新数组
*/
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;
// Clone remaining nodes
/**
* #3 将除尾部的元素克隆到新数组
*/
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);
}
}
}
}
int nodeIndex = node.hash & sizeMask; // add the new node
node.setNext(newTable[nodeIndex]);
newTable[nodeIndex] = node;
table = newTable;
}
- 移动与克隆:
#1
位置的代码: 如果数组链表中只有一个元素,则直接移动到新数组;#2
位置的for循环:将链表尾部连续的新下标相同的元素移动到新数组;#3
位置的for循环:将剩余的元素(如果还有的话)克隆到新数组中;
ConcurrentHashMap
扩容时,没有重新计算hash
值,也没有控制重新计算hash
值的参数开关;- 扩容只是将对应的
Segment
对象中的HashEntry
数组的容量扩容一倍,如果扩容的不是ss[0]
元素(ss[0]
位置的Segment
对象是创建新对象的模板),则不影响其他的Segment
对象; rehash
方法本身不需要加锁,因为外层调用put
方法已经加锁了;