一、HashMap详解
1、基本介绍
1.1 概述
HashMap是基于Map接口实现的,元素以key-value的方式存储在map在中,此实现提供所有可选的映射操作,并允许null的key和null的value。 HashMap类与Hashtable类大致等效,不同之处在于它不是线程安全的,并且允许为null。HashMap的UML图如下图所示:
1.2 构造方法
HashMap一共有4个构造方法,如下图所示:
/**
* 构造一个空的HashMap,默认容量为16,负载因子为0.75
*/
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
/**
* 构造一个空的HashMap,容量为指定的initialCapacity,负载因子默认为0.75
*/
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
/**
* 构造一个空的HashMap,具有指定初始值容量和负载因子的HashMap
*/
public HashMap(int initialCapacity, float loadFactor) {
// 判断传入的初始容量是否大于0,如果小于0,则抛出相应异常信息。
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
// 判断传入的初始容量是否大于最大容量,如果大于最大容量,则将initialCapacity赋值为最大容量MAXIMUM_CAPACITY,1 << 30,1左移30位
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
// 如果负载因子loadFactor小于0 或者 如果指定的数字不是一个数字(NaN),返回{true,否则返回false
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
threshold = initialCapacity;
init();
}
1.3 基本属性
常量:
- static final int DEFAULT_INITIAL_CAPACITY:默认初始容量,1 << 4,1左移4位,默认大小为16。
- static final int MAXIMUM_CAPACITY:最大容量,如果任何一个带参数的构造函数隐式指定了较大的值,则使用。
- static final float DEFAULT_LOAD_FACTOR:默认负载因子,值为0.75f。
成员变量:
- transient int size:map中存储key-value的个数
- transient Entry<K,V>[] table:entry数组,长度必须是2的幂。
- int threshold:扩容的阈值(capacity * load factor),如果table == EMPTY_TABLE,那么这个值为map初始容量大小。
- final float loadFactor:哈希表中的负载因子
2、数据结构
HashMap由数组和链表来实现对数据的存储。HashMap采用Entry数组来存储key-value键值对,每一个键值对组成了一个Entry实体,Entry类实际上是一个单向的链表结构,它具有Next指针,可以连接下一个Entry实体,以此来解决Hash冲突的问题。
HashMap中实现了一个静态内部类Entry,其重要的属性有 hash,key,value,next。如下图所示:
JDK1.7数据结构如下图所示:
3、源码解析
3.1 put过程分析
put(K key, V value)方法
public V put(K key, V value) {
// 判断table数组是否为空,如果为空,则初始化数组
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
// 如果key为空,调用putForNullKey方法put数据,最终会将元素存储在table[0]中
if (key == null)
return putForNullKey(value);
// 计算key的哈希值
int hash = hash(key);
// 根据哈希值和table数组长度计算数据存储在table中的索引下标
int i = indexFor(hash, table.length);
// 取出table数组中索引i位置处的Entry e,如果e不为空,循环遍历Entry链表,判断是否有重复的key存在,如果有替换掉key的值
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
// 如果当前节点e的哈希值、key与要put元素的哈希值、key相等
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
// 使用新值替换掉旧值
e.value = value;
// 每当对HashMap中已经存在的键k的put(k,v)调用覆盖条目中的值时,就会调用recordAccess方法。
e.recordAccess(this);
// 返回旧值
return oldValue;
}
}
// 记录修改次数,结构修改是指那些改变HashMap中映射数量或修改其内部结构的修改(例如,重新哈希)。
modCount++;
// 如果不存在重复的key,将当前元素添加到Entry链表中,后面会详细介绍此逻辑
addEntry(hash, key, value, i);
return null;
}
put方法中的初始化数组 inflateTable(int toSize)方法
// put方法中的初始化数组 inflateTable(int toSize)方法
private void inflateTable(int toSize) {
// roundUpToPowerOf2方法的目的就是根据传入toSize计算出一个合理的初始容量,保证数组大小始终为2的n次方。
// 比如在new HashMap(17),通过此方法计算后得出初始化数组的大小为32,也就是最终的capacity要大于等于离toSize最近的2的n次方的数
int capacity = roundUpToPowerOf2(toSize);
// 计算扩容的阈值:capacity * loadFactor
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
// new一个初始数组,大小为capacity
table = new Entry[capacity];
initHashSeedAsNeeded(capacity);
}
putForNullKey(V value) 方法:当元素的key为null时,调用putForNullKey(V value) 方法存储元素数据,实际上当key为null时,数据是存储在table[0]的位置
private V putForNullKey(V value) {
// 获取table[0]位置的数据,如果e不为null,遍历循环链表
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
// 如果e元素的key为null,使用新值替换掉旧值,并返回旧值。
if (e.key == null) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
// 如果table[0]位置没有元素的key为null,则将元素添加到table[0]的链表中
addEntry(0, null, value, 0);
return null;
}
将元素添加到Entry链表中,调用addEntry(int hash, K key, V value, int bucketIndex)方法。将具有指定键、值和散列码的元素添加到指定bucket位置。此方法负责在适当的情况下对table数组进行扩容处理
void addEntry(int hash, K key, V value, int bucketIndex) {
// 如果map中的元素个数size >= 扩容阈值threshold,并且在table的bucketIndex索引下标处有值,需要进一步扩容操作
if ((size >= threshold) && (null != table[bucketIndex])) {
// 扩容操作,数组大小扩容为原来数组大小的2倍。
resize(2 * table.length);
// 扩容以后重新计算hash值,如果key为null,hash值为0。
hash = (null != key) ? hash(key) : 0;
// 扩容后重新计算下标
bucketIndex = indexFor(hash, table.length);
}
// 根据元素hash、key、value值创建一个Entry,赋值到table的bucketIndex位置
createEntry(hash, key, value, bucketIndex);
}
// 将新值放到链表的表头,size++
void createEntry(int hash, K key, V value, int bucketIndex) {
// 获取table数组中bucketIndex位置的值
Entry<K,V> e = table[bucketIndex];
// new 一个 Entry ,将新加入的元素放在链表的表头
table[bucketIndex] = new Entry<>(hash, key, value, e);
// put中元素个数加1
size++;
}
扩容过程:将此map中的数据重新散列到具有更大容量的新数组中。当此map中的键数达到其阈值时,将自动调用此方法。如果当前容量是MAXIMUM_CAPACITY,此方法不会调整map的大小,而是将阈值设置为Integer.MAX_VALUE。这具有防止以后调用的作用。
void resize(int newCapacity) {
// 旧数组entry
Entry[] oldTable = table;
// 旧数组大小
int oldCapacity = oldTable.length;
// 如果旧数组大小等于最大容量值,则将扩容的阈值设置为Integer.MAX_VALUE,作用是防止以后再调用。
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
// 创建一个新的table
Entry[] newTable = new Entry[newCapacity];
// 将所有entry从当前table转移到newTable
transfer(newTable, initHashSeedAsNeeded(newCapacity));
// 重新将table数据赋值为newTable
table = newTable;
// 计算下一次扩容的阈值
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
将所有entry从当前table转移到newTable。
void transfer(Entry[] newTable, boolean rehash) {
// 获取新table容量大小
int newCapacity = newTable.length;
// 循环遍历旧的table数据
for (Entry<K,V> e : table) {
// 当entry不为null
while(null != e) {
// 获取链表entry的下一个元素
Entry<K,V> next = e.next;
// 如果需要重新计算哈希值
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
// 重新计算下标
int i = indexFor(e.hash, newCapacity);
// 当前entry元素的下一个元素为新的table数组中的值(也就是头插法)
e.next = newTable[i];
// 将entry值赋值到新table的索引i位置
newTable[i] = e;
// 保证旧table的entry在下一次while循环使用
e = next;
}
}
}
3.2 get过程分析
get过程相对put过程来说简单得很多,首先根据key计算hash值,再通过位运算(h & (length-1))找到数组索引下标,最后遍历数组下标位置处的Entry链表,找到对应的值即返回。返回指定key的值,如果map中不包含指定key的值,则返回null。
public V get(Object key) {
// 如果key为null,调用getForNullKey()方法,从table的0下标处取值
if (key == null)
return getForNullKey();
// 返回HashMap中指定的key相关联的entry。如果HashMap不包含key的映射,则返回null。
Entry<K,V> entry = getEntry(key);
// 返回具体指
return null == entry ? null : entry.getValue();
}
getEntry(Object key)方法
// 返回指定key的值,如果map中不包含指定key的值,则返回null。
final Entry<K,V> getEntry(Object key) {
// 如果map中没有数据,则返回null。
if (size == 0) {
return null;
}
// 计算hash值
int hash = (key == null) ? 0 : hash(key);
// 获取索引位置处的Entry,循环遍历Entry
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
// 如果entry的hash值与传入的key对应的hash值相等,且key也相等,则返回entry
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
// 未找到对应数据,返回null。
return null;
}
3.3 remove过程分析
remove过程也是非常多简单,大概思路就是根据指定的key计算出hash值和table的索引值,循环遍历链表找到对应的entry删除即可。
//删除并返回与HashMap中指定key相关联的entry。如果HashMap不包含此key的map,则返回null。
public V remove(Object key) {
Entry<K,V> e = removeEntryForKey(key);
return (e == null ? null : e.value);
}
final Entry<K,V> removeEntryForKey(Object key) {
// 如果map中没有数据,则返回null。
if (size == 0) {
return null;
}
// 计算hash值
int hash = (key == null) ? 0 : hash(key);
// 计算数组索引下标
int i = indexFor(hash, table.length);
// 获取索引处的entry值,前驱entry
Entry<K,V> prev = table[i];
// 当前entry
Entry<K,V> e = prev;
while (e != null) {
// 当前entry的下一个entry
Entry<K,V> next = e.next;
Object k;
// 如果当前entry的hash等于需要删除key对应的hash值,且key不为null,相等
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
// 记录修改次数
modCount++;
// map中元素个数-1
size--;
// 删除的是链表中第一个entry,断开当前entry e,table中i索引处的赋值为next
if (prev == e)
table[i] = next;
else
prev.next = next; // 断开当前entry e,前驱entry的下一个entry为next
e.recordRemoval(this);
// 返回删除的entry信息
return e;
}
// 未找到删除的元素,重新给前驱prev赋值为e,当前e赋值为next
prev = e;
e = next;
}
return e;
}
二、ConcurrentHashMap详解
1、基本介绍
1.1 概述
在Java1.5中,并发编程大师Doug Lea给我们带来了concurrent包,而该包中提供的ConcurrentHashMap是线程安全并且高效的HashMap。在并发编程中使用HashMap可能会导致死循环,而使用线程安全的HashTable效率又低下。HashMap 之所以在并发下的扩容造成死循环,是因为多个线程并发进行时,因为一个线程先期完成了扩容,将原 Map 的链表重新散列到自己的表中,并且链表变成了倒序,后一个线程再扩容时,又进行自己的散列,再次将倒序链表变为正序链表。于是形成了一个环形链表,当 get 表中不存在的元素时,造成死循环。于是在多线程并发处理下,ConcurrentHashMap解决了HashMap在扩容到时候造成链表形成环形结构的问题。ConcurrentHashMapUML图如下图所示:
1.2 构造方法
ConcurrentHashMap一共有5个构造方法,如下图所示:
ConcurrentHashMap 初始化方法是通过initialCapacity、loadFactor 和concurrencyLevel(参数concurrencyLevel 是用户估计的并发级别,就是说你觉得最多有多少线程共同修改这个map,根据这个来确定Segment 数组的大小concurrencyLevel 默认是DEFAULT_CONCURRENCY_LEVEL = 16;)等几个参数来初始化segment 数组、段偏移量segmentShift、段掩码segmentMask 和每个segment 里的HashEntry 数组来实现的。
并发级别可以理解为程序运行时能够同时更新 ConccurentHashMap 且不产 生锁竞争的最大线程数,实际上就是 ConcurrentHashMap 中的分段锁个数,即 Segment[]的数组长度。ConcurrentHashMap 默认的并发度为 16,但用户也可以 在构造函数中设置并发度。当用户设置并发度时,ConcurrentHashMap 会使用大 于等于该值的最小 2 幂指数作为实际并发度(假如用户设置并发度为 17,实际 并发度则为 32)。
如果并发度设置的过小,会带来严重的锁竞争问题;如果并发度设置的过大, 原本位于同一个 Segment 内的访问会扩散到不同的 Segment 中,CPU cache 命中 率会下降,从而引起程序性能下降。
segments 数组的长度 ssize 是通过 concurrencyLevel 计算得出的。为了能通 过按位与的散列算法来定位 segments 数组的索引,必须保证 segments 数组的长 度是 2 的 N 次方(power-of-two size),所以必须计算出一个大于或等于 concurrencyLevel 的最小的 2 的 N 次方值来作为 segments 数组的长度。假如 concurrencyLevel 等于 14、15 或 16,ssize 都会等于 16,即容器里锁的个数也是 16。
/**
* 创建一个空的map,默认初始容量(16)、负载因子(0.75)和concurrencyLevel(16)。
*/
public ConcurrentHashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);
}
/**
* 创建一个空的map,指定初始容量,且默认负载因子(0.75)和concurrencyLevel(16)。
*/
public ConcurrentHashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);
}
/**
* 创建一个空的map,指定初始容量和负载因子,且默认concurrencyLevel(16)。
*/
public ConcurrentHashMap(int initialCapacity, float loadFactor) {
this(initialCapacity, loadFactor, DEFAULT_CONCURRENCY_LEVEL);
}
/**
* 创建一个空的map,指定初始容量、负载因子和concurrencyLevel。
*/
public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel) {
// 验证参数合法性
if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
// 如果并发级别concurrencyLevel大于允许的最大值 MAX_SEGMENTS = 1 << 16,则concurrencyLevel等于最大值
if (concurrencyLevel > MAX_SEGMENTS)
concurrencyLevel = MAX_SEGMENTS;
// Find power-of-two sizes best matching arguments
int sshift = 0;
// segment数组的大小,根据并发级别来计算的。
// 以必须计算出一个大于或等 concurrencyLevel 的最小的2 的N 次方值来作为segments 数组的长度。
// 假如concurrencyLevel 等于14、15 或16,ssize 都会等于16,即容器里锁的个数也是16。
int ssize = 1;
while (ssize < concurrencyLevel) {
++sshift;
ssize <<= 1;
}
// 如果使用默认的concurrencyLevel 16,那么计算出来的ssize为16,sshift为4,segmentShift为28,segmentMask为15
// 段偏移量
this.segmentShift = 32 - sshift;
// 段掩码
this.segmentMask = ssize - 1;
// 如果指定的初始容量大于最大容量,则initialCapacity为最大容量值
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
int c = initialCapacity / ssize;
if (c * ssize < initialCapacity)
++c;
// segment 里HashEntry 数组的长度,默认最小值为2,因为这样的话,对于具体的segment,在插入第一个元素的时候不会扩容,插入第二个是时候才会扩容
int cap = MIN_SEGMENT_TABLE_CAPACITY;
while (cap < c)
cap <<= 1;
// 创建一个segment数组,只初始化segments[0]位置处的HashEntry
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];
// 将segments[0]写入数组中
UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
this.segments = ss;
}
1.3 基本属性
ConcurrentHashMap中的常量
- static final int DEFAULT_INITIAL_CAPACITY:table的默认初始容量,在构造函数中未指定时使用,默认大小为16。
- static final float DEFAULT_LOAD_FACTOR:table的默认负载因子,在构造函数中未指定时使用,默认大小为0.75f。
- static final int DEFAULT_CONCURRENCY_LEVEL:table的默认并发级别,在构造函数中未指定时使用,默认大小为16。
- static final int MAXIMUM_CAPACITY:table的最大容量,如果任何一个带参数的构造函数指定了较大的值,则使用。大小为1 << 30,也就是1*2的30次方。
- static final int MIN_SEGMENT_TABLE_CAPACITY:每个segment中HashEntry 的table 最小容量。必须是2的n次方,默认为2。
- static final int MAX_SEGMENTS:最大segment容量,值为1 << 16。
ConcurrentHashMap中的成员变量
- final Segment<K,V>[] segments:segment,每个segment都是一个专用的hash table。
Segment中的常量
- static final int MAX_SCAN_RETRIES:单核CPU的值为1,多核CPU的值为16。
Segment中的成员变量
- transient volatile HashEntry<K,V>[] table:每个segment中的table
- transient int count:元素个数
- transient int modCount:segment中mutative operations总数
- transient int threshold:扩容的阈值
- final float loadFactor:负载因子
Segment内的构造函数:
Segment(float lf, int threshold, HashEntry<K,V>[] tab) {
this.loadFactor = lf;
this.threshold = threshold;
this.table = tab;
}
2、数据结构
ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成。 Segment 是一种可重入锁(ReentrantLock),在 ConcurrentHashMap 里扮演锁的角色;HashEntry 则用于存储键值对数据。一个 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构和 HashMap 类似,是一种数组和链表结构。一个 Segment 里包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素, 每个 Segment 守护着一个 HashEntry 数组里的元素,当对 HashEntry 数组的数据 进行修改时,必须首先获得与它对应的 Segment 锁。如下图所示:
JDK1.7数据结构如下图所示:
3、源码解析
3.1 put过程源码解析
put(K key, V value)方法:将指定的key-value存储到map中,且key-value都不能为空。可以通过调用get方法来获取相应值。首先我们来看一下ConcurrentHashMap中的put方法,在此方法中主要是确定新值要插入到哪一个segment中,而真正put数据的操作是在Segment类中的put方法。
public V put(K key, V value) {
Segment<K,V> s;
// 判断参数是否为空
if (value == null)
throw new NullPointerException();
// 计算key的hash值,如果key为空,则报空指针异常
int hash = hash(key);
// 根据 hash 值找到 Segment 数组中的位置 j
// hash 是 32 位,无符号右移 segmentShift(28) 位,剩下低 4 位
// 再和segmentMask(15) 做一次与操作,也就是说 j 是 hash 值的最后 4 位,也就是segment的数组下标
int j = (hash >>> segmentShift) & segmentMask;
// 在初始化的时候只初始化了segment[0],但是其他位置还是 null,ensureSegment(j) 对 segment[j] 进行初始化
if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck
(segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
s = ensureSegment(j);
// 插入新值到segment中
return s.put(key, hash, value, false);
}
segment中的put方法,由数组+链表组成,这个HashMap数据结构一样。
// segment中的put方法,由数组+链表组成,这个HashMap数据结构一样。
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
// 在往该 segment 写入前,需要先获取该 segment 的独占锁
// put 方法会通过tryLock()方法尝试获得锁,获得了锁,node 为null 进入try语句块,
// 没有获得锁,调用scanAndLockForPut 方法自旋等待获得锁。
HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value);
V oldValue;
try {
// segment中的HashEntry数组table
HashEntry<K,V>[] tab = table;
// 根据hash值和tab长度计算索引下标
int index = (tab.length - 1) & hash;
// 获取索引index处的HashEntry值,first 是tab了数组该位置处的链表的表头
HashEntry<K,V> first = entryAt(tab, index);
// for循环遍历first,两种情况,一种是index处有元素,另一种是index处没有元素。
for (HashEntry<K,V> e = first;;) {
// 如果e不为空
if (e != null) {
K k;
// 如果HashEntry中的key等于新值key或者HashEntry中的hash等于新值hash,且key相等
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
oldValue = e.value;
// 判断是否替换旧值
if (!onlyIfAbsent) {
e.value = value;
++modCount;
}
break;
}
// 继续执行链表下一个HashEntry
e = e.next;
}
else {
// node不为 null,那就直接将它设置为链表表头
if (node != null)
node.setNext(first);
else
// node为null,初始化并设置为链表表头
node = new HashEntry<K,V>(hash, key, value, first);
int c = count + 1;
// 当table数组的大小超过阈值时,将对其进行重新散列、扩容操作。
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
rehash(node);
else
// 没有达到阈值,将 node 放到数组 tab 的 index 位置,也就是将新的节点设置成原链表的表头
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
// 释放锁
unlock();
}
return oldValue;
}
ensureSegment(int k)方法:ConcurrentHashMap 初始化的时候会初始化第一个槽 segment[0],对于其他槽,在插入第一个值的时候再进行初始化。 ensureSegment 方法考虑了并发情况,多个线程同时进入初始化同一个槽 segment[k],但只要有一个成功就可以了。
// 初始化segment:返回给定索引的segment,创建它并(通过CAS)记录在segmetn table 中(如果还没有)。
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[0] 了,
// 使用当前 segment[0] 处的数组长度和负载因子来初始化 segment[k]
// 为什么要用“当前”,因为 segment[0] 可能早就扩容过了
Segment<K,V> proto = ss[0]; // use segment 0 as prototype,使用 segment[0]作为原型,这也就是为什么要初始化segment[0]的原因
int cap = proto.table.length; // segment[0]中table数组的长度
float lf = proto.loadFactor; // segment[0]中负载因子
int threshold = (int)(cap * lf); // 计算下次扩容的阈值
// 创建一个HashEntry数组,容量为cap,也就是segment[k]内部的数组
HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
== null) { // recheck,再次检查一遍该segment[k]是否被其他线程初始化了。
// 创建一个segment
Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
// while 循环 CAS操作,保证多线程下只有一个线程可以成功
while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
== null) {
// 使用CAS操作,当前线程成功设值或其他线程成功设值后,退出
if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
break;
}
}
}
return seg;
}
scanAndLockForPut(K key, int hash, V value)方法:scanAndLockForPut 方法里在尝试获得锁的过程中会对对应hashcode 的链表进行遍历,如果遍历完毕仍然找不到与key 相同的HashEntry 节点,则为后续的 put 操作提前创建一个HashEntry。当tryLock 到一定次数后仍无法获得锁,则进入到阻塞队列等待锁,通过lock申请锁。
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
// 创建一个HashEntry
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) {
// 如果重试次数大于最大的MAX_SCAN_RETRIES(单核是1,多核是64),则进入到阻塞队列等待锁,改为lock()方式获取锁,lock()是阻塞方法,直到获取锁后返回
lock();
break;
}
else if ((retries & 1) == 0 &&
(f = entryForHash(this, hash)) != first) {
// 这个时候是有大问题了,那就是有新的元素进到了链表,成为了新的表头
// 所以这边的策略是,相当于重新走一遍这个 scanAndLockForPut 方法
e = first = f; // re-traverse if entry changed
retries = -1;
}
}
return node;
}
rehash(HashEntry<K,V> node)方法:扩容是新创建了数组,然后进行迁移数据,最后再将 newTable 设置给属性 table。为了避免让所有的节点都进行复制操作:由于扩容是基于 2 的幂指来操作, 假设扩容前某 HashEntry 对应到 Segment 中数组的 index 为 i,数组的容量为 capacity,那么扩容后该 HashEntry 对应到新数组中的 index 只可能为 i 或者 i+capacity,因此很多 HashEntry 节点在扩容前后 index 可以保持不变。
// 对table数组进行扩容处理
private void rehash(HashEntry<K,V> node) {
// 旧table数组的数据
HashEntry<K,V>[] oldTable = table;
// 旧table数组大小
int oldCapacity = oldTable.length;
// 新的table数组大小,为原来table数组大小的2倍
int newCapacity = oldCapacity << 1;
// 计算下次扩容的阈值
threshold = (int)(newCapacity * loadFactor);
// 创建一个新的HashEntry数组
HashEntry<K,V>[] newTable =
(HashEntry<K,V>[]) new HashEntry[newCapacity];
//新的掩码,用于计算元素下标位置
int sizeMask = newCapacity - 1;
// 遍历旧数组,将原来i位置处的元素存储到新数组的i或者i+oldCapacity位置处
for (int i = 0; i < oldCapacity ; i++) {
// 获取i位置处的元素数据e
HashEntry<K,V> e = oldTable[i];
if (e != null) { // 数据不为空
// 当前元素e的下一个元素next
HashEntry<K,V> next = e.next;
// 计算当前元素e在新数组中的位置idx
int idx = e.hash & sizeMask;
// 如果next为空,说明i位置处只有一个元素,那么久简单了,直接赋值给newTable[idx]
if (next == null) // Single node on list(单节点列表)
newTable[idx] = e;
else { // Reuse consecutive sequence at same slot
// e表示链表头结点
HashEntry<K,V> lastRun = e;
// idx表示当前元素e的位置
int lastIdx = idx;
// 遍历这个for循环,找到一个lastRun节点,这个节点之后的所有元素放在table同一个下标位置
for (HashEntry<K,V> last = next;
last != null;
last = last.next) {
// 重新计算last节点的下标位置
int k = last.hash & sizeMask;
if (k != lastIdx) {
lastIdx = k;
lastRun = last;
}
}
//将lastRun节点及其之后的所有节点组成的链表放到新table的lastIdx这个位置
newTable[lastIdx] = lastRun;
// Clone remaining nodes,克隆剩下的节点
//处理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);
}
}
}
}
// 将新的node节点放到newTable中nodeIndex位置的头部
int nodeIndex = node.hash & sizeMask; // add the new node
node.setNext(newTable[nodeIndex]);
newTable[nodeIndex] = node; // 将node赋值给newTable[nodeIndex]完成节点的插入
table = newTable;
}
3.2 get过程分析
get 操作先经过一次再散列,然后使用这个散列值通过散列运算定位到 Segment(使用了散列值的高位部分),再通过散列算法定位到 table(使用了散列值 的全部)。整个 get 过程,没有加锁,而是通过 volatile 保证 get 总是可以拿到最 新值。
public V get(Object key) {
Segment<K,V> s; // manually integrate access methods to reduce overhead
HashEntry<K,V>[] tab;
// 计算key的hash值
int h = hash(key);
long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
// 找到对应的segment,在segment数组中找到对应的table
if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
(tab = s.table) != null) {
// 遍历table下指定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;
if ((k = e.key) == key || (e.hash == h && key.equals(k)))
return e.value;
}
}
return null;
}
3.3 remove过程分析
与 put 方法类似,都是在操作前需要拿到锁,以保证操作的线程安全性。从map中删除指定key,如果map中不存key,则不做任何操作。
ConcurrentHashMap中的remove方法:
public V remove(Object key) {
// 计算hash值
int hash = hash(key);
// 根据hash值获取对应的segment
Segment<K,V> s = segmentForHash(hash);
// 调用segment中的remove方法
return s == null ? null : s.remove(key, hash, null);
}
Segment中的remove方法:
final V remove(Object key, int hash, Object value) {
// 如果尝试获取锁失败,调用scanAndLock方法获取锁
if (!tryLock())
scanAndLock(key, hash);
V oldValue = null;
try {
HashEntry<K,V>[] tab = table;
// 计算key在table数组中的索引下标。
int index = (tab.length - 1) & hash;
// 获取index位置处的元素值
HashEntry<K,V> e = entryAt(tab, index);
HashEntry<K,V> pred = null; // e元素的前节点
// 循环index位置处的HashEntry链表
while (e != null) {
K k;
// 元素e的下一个元素
HashEntry<K,V> next = e.next;
// 如果元素e的key与传入参数key相等或者元素e的hash值、key等于传入参数的hash值、key,表明已经找到了remove的元素
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
// 获取元素e的value值
V v = e.value;
if (value == null || value == v || value.equals(v)) {
if (pred == null) // 表示remove的是第一个元素,直接将next插入到index位置即可
setEntryAt(tab, index, next);
else
pred.setNext(next); // 断开连接,直接将e元素的前节点的next节点设置为next
++modCount;
--count; // map中的元素个数减1
oldValue = v;
}
break;
}
// 下一次循环元素e的前节点元素
pred = e;
// 下一次循环的元素e
e = next;
}
} finally {
// 释放锁
unlock();
}
return oldValue;
}
在ConcurrentHashMap还有一个remove方法,根据指定的key和value来remove元素,逻辑跟单独指定key的差不多,在此不再赘述。
本文主要讲解了JDK1.7中的HashMap和ConcurrentHashMap的基本介绍及其put、get、remove过程的源码分析,作者能力有限,难免会在某些地方分析得不到位,如有不正确的地方希望各位读者指正,大家一起进步,希望对各位有所帮助。下一篇将会详细讲解JDK1.8中的HashMap和ConcurrentHashMap,大家拭目以待。
备注:博主微信公众号,不定期更新文章,欢迎扫码关注。