目录
一、JDK7中的实现方式
为了提高并发,在JDK7中,一个HashMap被拆分为多个子HashMap。每一个HashMap称作一个Segment,多个线程操作多个Segment互相独立。
具体来说,每个Segment都继承自ReentrantLock,Segment的数量等于锁的数量,这些锁彼此之间相互独立,即所谓的分段锁。
重要参数
// 初始化默认值 16
static final int DEFAULT_INITIAL_CAPACITY = 16;
// 默认加载因子 0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 该表的默认并发级别
static final int DEFAULT_CONCURRENCY_LEVEL = 16;
// 数组最大容量 2的30次方
static final int MAXIMUM_CAPACITY = 1 << 30;
// 每段表的最小容量(每个HashEntry数组的长度)。必须是2的幂,至少是2,以避免在延迟构建之后在下次使用时立即调整大小。
static final int MIN_SEGMENT_TABLE_CAPACITY = 2;
// 允许的最大段数;用于绑定构造函数参数。必须是小于1<<24的二次幂。
static final int MAX_SEGMENTS = 1 << 16; // slightly conservative
// 用于锁定之前大小和包含值方法的非同步重试次数
static final int RETRIES_BEFORE_LOCK = 2;
构造函数
// 传入Segment[]数组的容量为16,默认加载因子0.75,默认级别16
public ConcurrentHashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);
}
// 自定义传入Segment[]数组的容量,默认加载因子0.75,默认级别16
public ConcurrentHashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);
}
// 自定义传入Segment[]数组的容量、加载因子,默认级别16
public ConcurrentHashMap(int initialCapacity, float loadFactor) {
this(initialCapacity, loadFactor, DEFAULT_CONCURRENCY_LEVEL);
}
// 泛型上限,Map对象传入键值对,键-传入K类或K的子类,值-传入V类或V的子类
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);
}
// 传入Segment[]数组的容量、加载因子、默认级别,初始化
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;
int ssize = 1;
// 找到Segment数组长度,大于等于并发级别的2的幂次值,如:level=16,ssize=16;level=17,ssize=32
// 这里的concurrencyLevel默认为16,所以此循环算出来sshift为4
while (ssize < concurrencyLevel) {
++sshift;
ssize <<= 1;
}
// segmentShift用于定位参与散列预算的位数,默认为28
// 这里的sshift,因为上面位操作共移动了4次,2^4=16可以快速计算当前Segment数组容量,所以后面的put中计算j的值(将要存放的Segment数组中的下标) >>>(32-sshift)&15 的结果会在[0-15]区间上(其实就是让高4位参与计算)
// 如果concurrencyLevel=17,ssize=32,sshift=5,就让高5位参与运算
this.segmentShift = 32 - sshift;
// 散列运算的掩码,默认为15,掩码的二进制各个位的值都是1,目的是为了计算得到key的hash值在Segment数组中的位置
this.segmentMask = ssize - 1;
// 限制数组容量最大数
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
// Segment里HashEntry数组的长度,如果c大于1,就会取大于等于c的2的N次幂值,所以cap不是1就是2的N次方。
int c = initialCapacity / ssize;
if (c * ssize < initialCapacity)
++c;
// HashEntry数组的最小长度为2
int cap = MIN_SEGMENT_TABLE_CAPACITY;
while (cap < c)
cap <<= 1;
// create segments and segments[0]
// 计算出segments[0]原型,下次put时,如果位置为空,则可以直接引用segments[0]中的参数,如:loadFactor、threshold 、HashEntry[]数组长度
Segment<K,V> s0 = new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
(HashEntry<K,V>[])new HashEntry[cap]);
// 创建Segment数组,默认为16大小
Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
// 设置ss对象数组中SBASE偏移地址对应的object型field的值为s0。这是一个有序或者有延迟的方法,并且不保证值的改变被其他线程立即看到。只有在field被volatile修饰并且期望被意外修改的时候使用才有用。
UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
this.segments = ss;
}
为了提高hash的计算性能,会保证数组的大小始终是2的整数次方。
第一个参数initialCapacity是整个concurrentHashMap的初始大小。用initialCapacity/ssize是每个Segment的初始大小。
第二个参数是负载因子,传给了Segment的内部,当每个Segment的元素个数达到一定阈值,进行rehash。Segment的个数不能扩容,但每个Segment的内部可以扩容。
put函数分析
// 这里的key和value都不能为null
public V put(K key, V value) {
Segment<K,V> s;
// 传入的值不能为空
if (value == null)
throw new NullPointerException();
// 计算key的hash值,HashMao中key可以为null,但是这里的key不能为null;假如key可以为null的话,你无法知道get(null)返回的null是什么意思
int hash = hash(key);
// 在创建时已经计算出this.segmentShift = 32 - sshift; 其中sshift默认为4
// this.segmentMask = ssize - 1; 其中ssize默认为16
// 先对计算出的hash值右移shift位,然后在&运算mask得到当前key要存放在哪个segment[]中
int j = (hash >>> segmentShift) & segmentMask;
// 这里的UNSAFE.getObject取的是内存中segments数组中第j个位置的元素。并赋值给s,判断是否为空
// (j << SSHIFT)是计算的偏移量,SBASE是segments数组的开始地址,两者相加就是将要存放的地址
if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck
(segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
// 如果当前位置上为空(没有Segment对象),则生成一个Segment对象,放在j个位置上,这里可能会产生并发问题
s = ensureSegment(j);
// 在Segment中的HashEntry[]中put数据
return s.put(key, hash, value, false);
}
多个线程可能同时调用ensureSegment的Segment[j]进行初始化,在这个函数里需要避免重复初始化。
private Segment<K,V> ensureSegment(int k) {
// 获取当前Segment[]对象
final Segment<K,V>[] ss = this.segments;
// 定位操作,跟上边 j << SSHIFT) + SBASE 一样
long u = (k << SSHIFT) + SBASE; // raw offset
Segment<K,V> seg;
// double check,保证多个线程并发情况下,只有一个线程创建成功
// 判断当前位置Segemnt对象是否为空
if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
// 得到Segemnt[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);
// 根据Segemnt[0]中HashEntry[],创建HashEntry[]数组,为了放入当前Segment中
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);
// 这里采用while循环,目的是在多线程情况下,让它一直判断是否已经创建好了Segemnt
while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
// CAS操作,保证只有一个线程创建
// 如果还是为null,则往里面添加Segment
if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
// 这里的break目的是为了结束while循环判断
break;
}
}
}
// 返回创建好的Segment对象
return seg;
}
使用CAS自旋进行初始化。接下来是元素放进去的过程。
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
// tryLock()和lock()方法:
// tryLock():不会阻塞。如果能拿到锁,返回true;如果不能拿到锁,返回false。
// lock():会阻塞。会一直等待拿锁
// 尝试对当前Segment对象上锁,如果能拿到锁,则返回null;如果不能拿到锁,则用scanAndLockForPut()方法去尝试获取锁的等待的过程中去执行一些其他操作。比如:去创建HashEntry对象出来
HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value);
V oldValue;
try {
// 取当前Segment对象里面的HashEntry[]
HashEntry<K,V>[] tab = table;
// 计算要存放在HashEntry[]中的下标值,做的是低位&运算
int index = (tab.length - 1) & hash;
// 取HashEntry[]中第index中的位置,这里entryAt()方法采用的是直接取内存中的值
HashEntry<K,V> first = entryAt(tab, index);
// 从头开始遍历HashEntry链表
for (HashEntry<K,V> e = first;;) {
// 如果当前HashEntry对象不为空
if (e != null) {
K k;
// 判断循环到当前的key值是否等于要存放的key值
if ((k = e.key) == key || (e.hash == hash && key.equals(k))) {
// 如果相等,则记录当前HashEntry的值,需要返回
oldValue = e.value;
// 这里的onlyIfAbsent默认传入为false
if (!onlyIfAbsent) {
// 更新当前的最新值,并且把hash映射的次数++
e.value = value;
++modCount;
}
// 找到有相同的key,停止循环
break;
}
// 没有找到相同的key,则把下一个元素赋值给当前元素,继续遍历链表
e = e.next;
}
// 如果当前HashEntry对象为空
else {
// 如果node在上边已经创建好了,则添加进去
if (node != null)
node.setNext(first);
else
// 创建一个HashEntry对象,也就是要插入的对象
node = new HashEntry<K,V>(hash, key, value, first);
// 给HashEntry中元素+1
int c = count + 1;
// 如果数量超过HashEntry的阈值
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
// 则给HashEntry[]扩容
rehash(node);
else
// 否则将当前node头插法添加到HashEntry[]的index下标中
setEntryAt(tab, index, node);
// 哈希映射被修改的次数++
++modCount;
// 更新当前HashEntry[]中的元素个数
count = c;
// 当前的key在链表中没有,已经添加到HashEntry中的情况,返回null
oldValue = null;
break;
}
}
} finally {
// 解锁
unlock();
}
return oldValue;
}
当put的元素存在相同的值时,判断onlyIfAbsent,如果仅不存在时插入那么就不做操作,否则进行覆盖。
如果便利到了尾部也没有相同的值,就在链表头部插入1个新节点,并把table[index]赋值为该节点。
在函数的开始,加锁的时候进行了一次优化。如果tryLock成功获得锁,如果失败执行scanAndLockForPut。
private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
// 通过Hash值计算Segment中相应位置上的第一个HashEntry链表元素
HashEntry<K,V> first = entryForHash(this, hash);
HashEntry<K,V> e = first;
HashEntry<K,V> node = null;
// 重试的次数
int retries = -1; // negative while locating node
// 循环尝试去拿锁,如果拿到锁了则返回node,没有拿到锁则进行循环
// 也就是每次遍历链表中的节点时都会去尝试拿锁
while (!tryLock()) {
// 进行重新检查
HashEntry<K,V> f; // to recheck first below
// 刚开始为-1,则进入条件
if (retries < 0) {
// 一种情况是得到的链表为空,没有元素
// 这里还有一种情况就是链表遍历到尾结点
if (e == null) {
// 再次判断是否有线程创建了HashEntry对象
if (node == null) // speculatively create node
// 如果都满足以上条件,则创建HashEntry对象
node = new HashEntry<K,V>(hash, key, value, null);
// 将重试次数置为0,不让他进入retries < 0这个条件
// 说明此时已经创建HashEntry对象了
retries = 0;
}
// 说明遍历到当前链表的key已经存在
else if (key.equals(e.key))
// 将重试次数置为0,不让他进入retries < 0这个条件
// 说明此时链表中有这个HashEntry对象了
retries = 0;
else
// 继续遍历链表,向下找
e = e.next;
}
// 如果当前重试次数超过最大值,则阻塞,一直等待去拿锁,拿到锁就返回
else if (++retries > MAX_SCAN_RETRIES) {
lock();
break;
}
// (retries & 1) == 0 说明在retries为偶数的时候重新进行链表的判断
else if ((retries & 1) == 0 &&
// 这里的entryForHash()方法跟上边的一样,目的是判断通过Hash值计算Segment中相应位置上的第一个HashEntry链表元素有没有被修改(头插法的原因)。如果有改变,则说明有新元素被插入了
(f = entryForHash(this, hash)) != first) {
// 将当前链表重新赋值,进行遍历
e = first = f; // re-traverse if entry changed
// 重新初始化retries
retries = -1;
}
}
// 返回拿到锁的node节点
return node;
}
总的来说就是一边自旋获得锁,一边遍历链表,若没有发现重复的节点,则提前新建一个节点,为后面再插入节省时间。如果自旋一定次数没有获得锁,调用lock阻塞。
扩容
超过一定阈值后,Segment内部会进行扩容。在原基础上扩容两倍。
private void rehash(HashEntry<K,V> node) {
// 记录原来的HashEntry数组信息,表长
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++) {
// 记录每一个HahsEntry[]中链表的第一个结点
HashEntry<K,V> e = oldTable[i];
// 如果第一个节点不为空,则开始遍历它
if (e != null) {
// 记录第一个节点的下一个节点
HashEntry<K,V> next = e.next;
// 计算在新数组中的下标位置
int idx = e.hash & sizeMask;
// 如果第一个节点的下个节点不存在,则直接将第一个节点转移到新数组中去
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;
// 寻找旧表中在新表中顺序不相同的位置,相同则不进行操作
// 目的是为了减少移动操作
for (HashEntry<K,V> last = next; last != null; last = last.next) {
// 计算原来的hash值再新表中的下标
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);
}
}
}
}
// 插入新节点
// 计算将要存放的位置,头插法插入,最后赋值给table
int nodeIndex = node.hash & sizeMask; // add the new node
node.setNext(newTable[nodeIndex]);
newTable[nodeIndex] = node;
table = newTable;
}
扩容后数组变成两倍原数组大小,新的hash值有两种可能,1.原hash值,2.原hash值加上原数组长度。
所以扩容也做了一次优化,对链表尾部一串新hash值相同的一个子链表,不需要一个个移动,只需要将lastRun位置的节点连接到新数组中。
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);
// 计算当前key在Segemnts数组中的位置
long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
// 如果当前Segments中第u个位置不为空则进行查找
if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
(tab = s.table) != null) {
// 循环遍历当前key在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;
}
}
// 没有找到则返回null
return null;
}
整个get过程进行了两次hash,一次对Segment数组hash,一次对HashEntry数组进行hash。
整个读的过程没有加锁,而是使用了UNSAFE.getObjectVolatile函数读取最新值。