1、JDK7 HashMap
1.1参数
数组的长度为什么必须是2的幂次方
数组的长度是幂次方的原因,在进行计算插入元素对应的index位置时,可以使用
&
与运算代替%
运算;但是要求必须
数组的长度为2的幂次方,这样 hash&(length-1)才能等价于 hash%length
负载因子的本质是什么
负载因子决定了何时会对整个数组进行扩容;
- 当负载因子较小的时候,则threshold比较小,数组扩容的概率大,此时hash冲突比较小,空间效率低,时间效率高
- 当负载因子较大的时候,则threshold较大,数组扩容的概率小,此时hash冲突较大,空间效率高,时间效率低
- 一般情况下负载因子的值为0.75
1.2、简述hashMap的put方法
- 若数组没有初始化,则对数组进行初始化
- 计算hash值
- 根据hash值得到index的值
- 判断在数组中是否有重复的key,若有则直接替换
- 若无,则判断是否需要扩容
- 使用头插法将元素插入
1.3、为什么是线程不安全的(可能产生死环)
在JDK7中,在扩容的时候,由于扩容采用的是头插法,则会导致原来的元素的顺序在新的数组中颠倒;若有多个线程对
hashMap
操作时,可能由于线程的问题,造成数组中的单向链表变为循环链表的情况产生死环这是HashMap在并发环境下使用中最为典型的一个问题,就是在HashMap进行扩容重哈希时导致Entry链形成环。一旦Entry链中有环,势必会导致在同一个桶中进行插入、查询、删除等操作时陷入死循环。
-
可以对hashMap使用同步方法
-
可以使用HashTable
-
可以使用Collections.synchronizedMap
-
可以采用
CourrentHashMap
2、JDK7中的ConcurrentHashMap
ConcurrentHashMap 类中包含两个静态内部类
HashEntry
和Segment
。HashEntry
用来封装映射表的键 / 值对;
Segment
用来充当锁的角色,每个Segment
对象守护整个散列映射表的若干个桶。每个桶是由若干个
HashEntry
对象链接起来的链表。一个ConcurrentHashMap
实例中包含由若干个Segment
对象组成的数组。
ConcurrentHashMap优势就是采用了[锁分段技术],每一个Segment就好比一个自治区,读写操作高度自治,Segment之间互不影响
ConcurrentHashMap当中每个Segment各自持有一把锁。在保证线程安全的同时降低了锁的粒度,让并发操作效率更高。
为了更好的理解 ConcurrentHashMap 高并发的具体实现,我们先来了解它在JDK中的定义。ConcurrentHashMap类中包含两个静态内部类 HashEntry 和 Segment,其中 HashEntry 用来封装具体的K/V对,是个典型的四元组;Segment 用来充当锁的角色,每个 Segment 对象守护整个ConcurrentHashMap的若干个桶 (可以把Segment看作是一个小型的哈希表),其中每个桶是由若干个 HashEntry 对象链接起来的链表。总的来说,一个ConcurrentHashMap实例中包含由若干个Segment实例组成的数组,而一个Segment实例又包含由若干个桶,每个桶中都包含一条由若干个 HashEntry 对象链接起来的链表。特别地,ConcurrentHashMap 在默认并发级别下会创建16个Segment对象的数组,如果键能均匀散列,每个 Segment 大约守护整个散列表中桶总数的 1/16
2.1、较HashMap的并发特性
-
不同Segment的写入是可以并发执行的。
-
同一Segment的一写一读
-
Segment的写入是需要上锁的,因此对同一Segment的并发写入会被阻塞。
小结
Concureent基于分段式的segment操作,对每个segment持有不同的锁,在进行put数据时对该segment加锁
- 对同一个segment读和写不干扰
- 对同一个segement进行写干扰
- 对不同的segment的读写不干扰
2.2、成员变量的定义
段Segment必须为2的整数次幂,段中Entry也必须为2的整数次幂**
Segment的数据结构(继承了可重入锁)
HashEntry数据结构
在HashEntry类中,key,hash和next域都被声明为final的,value域被volatile所修饰,因此HashEntry对象几乎是不可变的,这是ConcurrentHashmap读操作并不需要加锁的一个重要原因。next域被声明为final本身就意味着我们不能从hash链的中间或尾部添加或删除节点,因为这需要修改next引用值,因此所有的节点的修改只能从头部开始。对于put操作,可以一律添加到Hash链的头部。但是对于remove操作,可能需要从中间删除一个节点,这就需要将要删除节点的前面所有节点整个复制(重新new)一遍,最后一个节点指向要删除结点的下一个结点(这在谈到ConcurrentHashMap的删除操作时还会详述)。特别地,由于value域被volatile修饰,所以其可以确保被读线程读到最新的值,这是ConcurrentHashmap读操作并不需要加锁的另一个重要原因。实际上,ConcurrentHashMap完全允许多个读操作并发进行,读操作并不需要加锁。HashEntry代表hash链中的一个节点
static final class Segment<K,V> extends ReentrantLock implements Serializable {
private static final long serialVersionUID = 2249069246763182397L;
/**
* 对segment加锁时,在阻塞之前自旋的次数
*
*/
static final int MAX_SCAN_RETRIES =
Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;
/**
* 每个segment的HashEntry table数组,访问数组元素可以通过entryAt/setEntryAt提供的volatile语义来完成
* volatile保证可见性
*/
transient volatile HashEntry<K,V>[] table;
/**
* 元素的数量,只能在锁中或者其他保证volatile可见性之间进行访问
*/
transient int count;
/**
* 当前segment中可变操作发生的次数,put,remove等,可能会溢出32位
* 它为chm isEmpty() 和size()方法中的稳定性检查提供了足够的准确性.
* 只能在锁中或其他volatile读保证可见性之间进行访问
*/
transient int modCount;
/**
* 当table大小超过阈值时,对table进行扩容,值为(int)(capacity *loadFactor)
*/
transient int threshold;
/**
* 负载因子
*/
final float loadFactor;
/**
* 构造方法
*/
Segment(float lf, int threshold, HashEntry<K,V>[] tab) {
this.loadFactor = lf;
this.threshold = threshold;
this.table = tab;
}
static final class HashEntry<K,V> {
// hash值
final int hash;
// key
final K key;
// 保证内存可见性,每次从内存中获取
volatile V value;
volatile HashEntry<K,V> next;
HashEntry(int hash, K key, V value, HashEntry<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
/**
* 使用volatile语义写入next,保证可见性
*/
final void setNext(HashEntry<K,V> n) {
UNSAFE.putOrderedObject(this, nextOffset, n);
}
2.3、ConcurrentHashMap的数据结构
本质上,ConcurrentHashMap就是一个Segment数组,而一个Segment实例则是一个小的哈希表。由于Segment类继承于ReentrantLock类,从而使得Segment对象能充当锁的角色,这样,每个 Segment对象就可以守护整个ConcurrentHashMap的若干个桶,其中每个桶是由若干个HashEntry 对象链接起来的链表。通过使用段(Segment)将ConcurrentHashMap划分为不同的部分,ConcurrentHashMap就可以使用不同的锁来控制对哈希表的不同部分的修改,从而允许多个修改操作并发进行, 这正是ConcurrentHashMap锁分段技术的核心内涵。进一步地,如果把整个ConcurrentHashMap看作是一个父哈希表的话,那么每个Segment就可以看作是一个子哈希表,如下图所示:
注意,假设ConcurrentHashMap一共分为2n个段,每个段中有2m个桶,那么段的定位方式是将key的hash值的高n位与(2n-1)相与。在定位到某个段后,再将key的hash值的低m位与(2m-1)相与,定位到具体的桶位。
2.4、ConcurrentHashMap的构造函数
构造一个具有
指定容量
、指定负载因子
和指定段数目/并发级别
(若不是2的幂次方,则会调整为2的幂次方)的空ConcurrentHashMap保证Segment的个数与Segment中的Entry的数量都是2的幂次方
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;
//段的数目,segment数组大小(2的幂次方)
int ssize = 1;
//计算段的数目
while (ssize < concurrencyLevel) {
++sshift;
ssize <<= 1;
}
//用于定位段
this.segmentShift = 32 - sshift;
this.segmentMask = ssize - 1;
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//段中hashEntry链表的个数;保证为2的幂次方
int c = initialCapacity / ssize;
if (c * ssize < initialCapacity)
++c;
int cap = MIN_SEGMENT_TABLE_CAPACITY;
while (cap < c)
cap <<= 1;
//先创建Segmet[0]
// create segments and segments[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];
UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
this.segments = ss;
}
2.5、并发操作之put方法(采用分段锁机制实现多个线程间的并发操作)
在ConcurrentHashMap中,线程对映射表做读操作时,一般情况下不需要加锁就可以完成,对容器做结构性修改的操作(比如,put操作、remove操作等)才需要加锁。
(1) map的put方法就做了三件事情,找出segments的位置;判断当前位置有没有初始化,没有就调用ensureSegment()方法初始化;然后调用segment的put方法.
(2) segment的put方法.,获取当前segment的锁,成功接着执行,失败调用scanAndLockForPut方法自旋获取锁,成功后也是接着往下执行.
(3) 通过hash计算出位置,获取节点,找出相同的key和hash替换value,返回.没有找到相同的,设置找出的节点为当前创建节点的next节点,设置创建节点前,判断是否需要扩容,需要调用扩容方法rehash();不需要,设置节点,返回,释放锁.
/**
* map的put方法,定位segment
*/
public V put(K key, V value) {
Segment<K,V> s;
// value不能为空
if (value == null)
throw new NullPointerException();
// 获取hash
int hash = hash(key);
// 定位segments 数组的位置
int j = (hash >>> segmentShift) & segmentMask;
// 获取这个segment
if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck
(segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
// 为null 初始化当前位置的segment
s = ensureSegment(j);
return s.put(key, hash, value, false);
}
ensureSegment方法
/**
*
* @param k 位置
* @return segments
*/
private Segment<K,V> ensureSegment(int k) {
final Segment<K,V>[] ss = this.segments; // 当前的segments数组
long u = (k << SSHIFT) + SBASE; // raw offset // 计算原始偏移量,在segments数组的位置
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 // 获取第一个segment ss[0]
// 这就是为什么要在初始化化map时要初始化一个segment,需要用cap和loadFactoe 为模板
int cap = proto.table.length; // 容量
float lf = proto.loadFactor; // 负载因子
int threshold = (int)(cap * lf); // 阈值
// 初始化ss[k] 内部的tab数组 // recheck
HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
// 再次检查这个ss[k] 有没有被初始化
if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
== null) { // recheck
// 创建一个Segment
Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
// 这里用自旋CAS来保证把segments数组的u位置设置为s
// 万一有多线程执行到这一步,只有一个成功,break
// getObjectVolatile 保证了读的可见性,所以一旦有一个线程初始化了,那么就结束自旋
while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
== null) {
if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
break;
}
}
}
}
Segment中的put方法/
/**
* put到table方法
*/
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;
// 获取第一个桶的第一个元素
// entryAt 底层调用getObjectVolatile 具有volatile读语义
HashEntry<K,V> first = entryAt(tab, index);
for (HashEntry<K,V> e = first;;) {
if (e != null) { // 证明链式结构有数据 遍历节点数据替换,直到e=null
K k;
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) { // 找到了相同的key
oldValue = e.value;
if (!onlyIfAbsent) { // 默认值false
e.value = value; // 替换value
++modCount;
}
break; // 结束循环
}
e = e.next;
}
else { // e=null (1) 之前没有数据 (2) 没有找到替换的元素
// node是否为空,这个获取锁的是有关系的
// (1) node不为null,设置node的next为first
// (2) node为null,创建头节点,指定next为first
if (node != null)
// 底层使用 putOrderedObject 方法 具有volatile写语义
node.setNext(first);
else
node = new HashEntry<K,V>(hash, key, value, first);
int c = count + 1;
// 扩容条件 (1)entry数量大于阈值 (2) 当前table的数量小于最大容量 满足以上条件就扩容
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
// 扩容方法,方法里面具体讲
rehash(node);
else
// 给table的index位置设置为node,
// node为头结点,原来的头结点first为node的next节点
// 底层也是调用的 putOrderedObject 方法 具有volatile写语义
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
unlock();
}
return oldValue;
}
///自旋锁
private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
HashEntry<K,V> first = entryForHash(this, hash); // 根据hash获取头结点
HashEntry<K,V> e = first;
HashEntry<K,V> node = null;
int retries = -1; // 是为了找到对应hash桶,遍历链表时找到就停止
while (!tryLock()) { // 尝试获取锁,成功就返回,失败就开始自旋
HashEntry<K,V> f; // to recheck first below
if (retries < 0) {
if (e == null) { // 结束遍历节点
if (node == null) // 创造新的节点
node = new HashEntry<K,V>(hash, key, value, null);
retries = 0; // 结束遍历
}
else if (key.equals(e.key)) // 找到节点 停止遍历
retries = 0;
else
e = e.next; // 下一个节点 直到为null
}
else if (++retries > MAX_SCAN_RETRIES) { // 达到自旋的最大次数
lock(); // 进入加锁方法,失败进入队列,阻塞当前线程
break;
}
else if ((retries & 1) == 0 &&
(f = entryForHash(this, hash)) != first) {
e = first = f; // 头结点变化,需要重新遍历,说明有新的节点加入或者移除
retries = -1;
}
}
return node;
}
/扩容
扩容方法
/**
*扩容方法
*/
private void rehash(HashEntry<K,V> node) {
// 旧的table
HashEntry<K,V>[] oldTable = table;
// 旧的table的长度
int oldCapacity = oldTable.length;
// 扩容原来capacity的一倍
int newCapacity = oldCapacity << 1;
// 新的阈值
threshold = (int)(newCapacity * loadFactor);
// 新的table
HashEntry<K,V>[] newTable =
(HashEntry<K,V>[]) new HashEntry[newCapacity];
// 新的掩码
int sizeMask = newCapacity - 1;
// 遍历旧的table
for (int i = 0; i < oldCapacity ; i++) {
// table中的每一个链表元素
HashEntry<K,V> e = oldTable[i];
if (e != null) { // e不等于null
HashEntry<K,V> next = e.next; // 下一个元素
int idx = e.hash & sizeMask; // 重新计算位置,计算在新的table的位置
if (next == null) // Single node on list 证明只有一个元素
newTable[idx] = e; // 把当前的e设置给新的table
else { // Reuse consecutive sequence at same slot
HashEntry<K,V> lastRun = e; // 当前e
int lastIdx = idx; // 在新table的位置
for (HashEntry<K,V> last = next;
last != null;
last = last.next) { // 遍历链表
int k = last.hash & sizeMask; // 确定在新table的位置
if (k != lastIdx) { // 头结点和头结点的next元素的节点发生了变化
lastIdx = k; // 记录变化位置
lastRun = last; // 记录变化节点
}
}
// 以下把链表设置到新table分为两种情况
// (1) lastRun 和 lastIdx 没有发生变化,也就是整个链表的每个元素位置和一样,都没有发生变化
// (2) lastRun 和 lastIdx 发生了变化,记录变化位置和变化节点,然后把变化的这个节点设置到新table
// ,但是整个链表的位置只有变化节点和它后面关联的节点是对的
// 下面的这个遍历就是处理这个问题,遍历当前头节点e,找出不等于变化节点(lastRun)的节点重新处理
newTable[lastIdx] = lastRun;
// Clone remaining nodes
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
// 设置next节点,此时已经扩容完成,要从新table里面去当前位置的头结点为next节点
node.setNext(newTable[nodeIndex]);
// 设置位置
newTable[nodeIndex] = node;
// 新table替换旧的table
table = newTable;
}
2.5、get方法()
- 为输入的key左Hash运算,得到hash值
- 通过hash值,定位到对应的Segment对象
- 再次通过hash值,定位到segment当中数组的具体位置
/**
* get 方法
*/
public V get(Object key) {
Segment<K,V> s; // manually integrate access methods to reduce overhead
HashEntry<K,V>[] tab;
int h = hash(key);
long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE; // 获取segment的位置
// getObjectVolatile getObjectVolatile语义读取最新的segment,获取table
if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
(tab = s.table) != null) {
// getObjectVolatile getObjectVolatile语义读取最新的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;
// 找到相同的key 返回
if ((k = e.key) == key || (e.hash == h && key.equals(k)))
return e.value;
}
}
return null;
}
读方法为什么不需要加锁
get 没有加锁,效率高
注意:get方法使用了getObjectVolatile方法读取segment和hashentry,保证是最新的,具有锁的语义,可见性
分析:为什么get不加锁可以保证线程安全
(1) 首先获取value,我们要先定位到segment,使用了UNSAFE的getObjectVolatile具有读的volatile语义,也就表示在多线程情况下,我们依旧能获取最新的segment.
(2) 获取hashentry[],由于table是每个segment内部的成员变量,使用volatile修饰的,所以我们也能获取最新的table.
(3) 然后我们获取具体的hashentry,也时使用了UNSAFE的getObjectVolatile具有读的volatile语义,然后遍历查找返回.==) 总结我们发现怎个get过程中使用了大量的volatile关键字,其实就是保证了可见性(加锁也可以,但是降低了性能),get只是读取操作,所以我们只需要保证读取的是最新的数据即可
2.6、如何计算size,解决一致性问题
ConcurrentHashMap的Size方法是一个嵌套循环,大体逻辑如下:
size方法主要思路是先在没有锁的情况下对所有段大小求和,这种求和策略最多执行RETRIES_BEFORE_LOCK次(默认是两次):在没有达到RETRIES_BEFORE_LOCK之前,求和操作会不断尝试执行(这是因为遍历过程中可能有其它线程正在对已经遍历过的段进行结构性更新);在超过RETRIES_BEFORE_LOCK之后,如果还不成功就在持有所有段锁的情况下再对所有段大小求和。事实上,在累加count操作过程中,之前累加过的count发生变化的几率非常小,所以ConcurrentHashMap的做法是先尝试RETRIES_BEFORE_LOCK次通过不锁住Segment的方式来统计各个Segment大小,如果统计的过程中,容器的count发生了变化,则再采用加锁的方式来统计所有Segment的大小。
1.遍历所有的Segment。
2.把Segment的元素数量累加起来。
3.把Segment的修改次数累加起来。
4.判断所有Segment的总修改次数是否大于上一次的总修改次数。如果大于,说明统计过程中有修改,重新统计,尝试次数+1;如果不是。说明没有修改,统计结束。
5.如果尝试次数超过阈值,则对每一个Segment加锁,再重新统计。
6.再次判断所有Segment的总修改次数是否大于上一次的总修改次数。由于已经加锁,次数一定和上次相等。
7.释放锁,统计结束。
为什么这样设计呢?这种思想和乐观锁悲观锁的思想如出一辙。
为了尽量不锁住所有Segment,首先乐观地假设Size过程中不会有修改。当尝试一定次数,才无奈转为悲观锁,锁住所有Segment保证强一致性。
public int size() {
// Try a few times to get accurate count. On failure due to
// continuous async changes in table, resort to locking.
//Segements数组
final Segment<K,V>[] segments = this.segments;
//保存计算的结果
int size;
boolean overflow; // true if size overflows 32 bits
//modCount修改的次数(本次)
long sum; // sum of modCounts
//modCount修改的次数(上次)
long last = 0L; // previous sum
//尝试循环的次数(也就是无锁计算的次数);当次数达到一定数量的时候,会对整个Segments加锁
int retries = -1; // first iteration isn't retry
try {
//无限循环,直到重试次数==RETRIES_BEFORE_LOCK
for (;;) {
//重试次数达到一定;会对Segments整个数组加锁
if (retries++ == RETRIES_BEFORE_LOCK) {
for (int j = 0; j < segments.length; ++j)
ensureSegment(j).lock(); // force creation
}
sum = 0L;
size = 0;
overflow = false;
//遍历真个Sgement数组
for (int j = 0; j < segments.length; ++j) {
Segment<K,V> seg = segmentAt(segments, j);
if (seg != null) {
//统计每个segment中的修改次数
sum += seg.modCount;
int c = seg.count;
//统计每个segement中的对象数
if (c < 0 || (size += c) < 0)
overflow = true;
}
}
//假如前后两次遍历过程中modCount的值一样,则跳出循环
if (sum == last)
break;
//不以言再次遍历训话
last = sum;
}
} finally {
if (retries > RETRIES_BEFORE_LOCK) {
for (int j = 0; j < segments.length; ++j)
segmentAt(segments, j).unlock();
}
}
return overflow ? Integer.MAX_VALUE : size;
}