J.U.C中实现Map接口的并容器有ConcurrentHashMap和ConcurrentSkipListMap。
ConcurrentHashMap
ConcurrentHashMap是并发容器中锁分拆的一个经典设计。
ConcurrentHashMap 内部布局:
public class MyConcurrentHashMap extends AbstractMap
implements ConcurrentMap, Serializable {
/** 数据段初始容量为16,默认为 16 个数据段 */
static final int DEFAULT_INITIAL_CAPACITY = 16;
/** 默认装载因子为 */
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/** 默认并发级别为 16 */
static final int DEFAULT_CONCURRENCY_LEVEL = 16;
/** 数据段table的最小容量,避免next使用时,需要立即扩容 */
static final int MIN_SEGMENT_TABLE_CAPACITY = 2;
/** 最数据段段数量 65536 */
static final int MAX_SEGMENTS = 1 << 16; // slightly conservative
/** hash掩码*/
final int segmentMask;
/** 偏移量,与segmentMask一起定位数据段 */
final int segmentShift;
/** 数据段 */
final Segment[] segments;
transient Set keySet;// Key集合
transient Set> entrySet;// entry集合
transient Collection values;// value集合
/** 元素 k-v键值对 */
static final class HashEntry {... ...}
/** 数据段,继承自ReentrantLock简化加锁*/
static final class Segment
extends ReentrantLock implements Serializable {... ...}
... ...
}
ConcurrentHashMap默认是分为16个数据段,每个数据段在在添加或修改数据时会各自加锁,意味着在理想的情况下可以由16个线程同时写一个ConcurrentHashMap。
内部类HashEntry和Segment是两个最重要的基础设施。
static final class HashEntry {
final int hash;// hash码 不可变
final K key;// 键 不可变
volatile V value;// 值 可见行
volatile HashEntry next;// 后继实体 可见性
// 构造方法
HashEntry(int hash, K key, V value, HashEntry next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
// 设置后继实体next
final void setNext(HashEntry n) {
UNSAFE.putOrderedObject(this, nextOffset, n);
}
... ...
}
static final class Segment extends
ReentrantLock implements Serializable {
/** 元素数组*/
transient volatile HashEntry[] table;
/** 元素个数*/
transient int count;
/** 修改次数*/
transient int modCount;
/**rehash临界值,
* 当 table 中包含的 HashEntry 元素的个数超过本变量值时,
* 触发 table 的再散列*/
transient int threshold;
/** 负载因子*/
final float loadFactor;
/** 构造Segment,负载因子,临界条件,table */
Segment(float lf, int threshold, HashEntry[] tab)
/** put方法*/
final V put(K key, int hash, V value, boolean onlyIfAbsent)
/** 再散列*/
private void rehash(HashEntry node)
/** 删除元素*/
final V remove(Object key, int hash, Object value)
/** 替换元素值*/
final boolean replace(K key, int hash, V oldValue, V newValue)
... ...
}
HashEntry就是Map中的元素,也就通常说的键值对。
Segment是数据段实际存放HashEntry的地方,HashEntry放在数组table中。HashEntry是单向链表的结构,由于HASH算法是将一个大集合映射到一个小集合,所以存在多个元素映射到同一个元素的情况,这种情况叫做“hash碰撞”,也就是hash值相同,将所有hash值相同的HashEntry放到这个链表中形成一个“hash桶”中。
Segment中的put、remove就是元素的实际修改方法,对ConcurrentHashMap的put、remove操作会委托到这里。
put操作:
public V put(K key, V value) {
Segment s;// 数据段
if (value == null)// 空值检测
throw new NullPointerException();
// 取hash值,如果key为空,这里会抛异常
int hash = hash(key);
// 根据hash码 取段定位
int j = (hash >>> segmentShift) & segmentMask;
// 从segments拿到数据段,如果段为空,进入ensureSegment 会新建一个段出来
if ((s = (Segment)
UNSAFE.getObject(segments, (j << SSHIFT) + SBASE)) == null) {
s = ensureSegment(j);
}
return s.put(key, hash, value, false);
}
首先根据key的hash码取得分段的定位,拿到分段,如果没有hash码定位的分段则新建,然后将put操作委托给分段Segment。
Segment put操作:
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
// 尝试获取锁,获取成功返回node为null,
//否则说明有其他线程对此数据段进行更新操作
// 进入scanAndLockForPut,进行重试,
//如果重试重试次数大于MAX_SCAN_RETRIES进行lock
// 阻塞加锁,否则一直自旋,获得锁后返回。
HashEntry node = tryLock() ?
null : scanAndLockForPut(key, hash, value);
V oldValue;
try {
HashEntry[] tab = table;
// 获取table索引
int index = (tab.length - 1) & hash;
// 获取table索引为index的第一个HashEntry
HashEntry first = entryAt(tab, index);
// 遍历table[index]上的HashEntry链
for (HashEntry e = first;;) {
if (e != null) { // 如果HashEntry不为null
K k;
if ((k = e.key) == key
|| (e.hash == hash && key.equals(k))) {
oldValue = e.value;
if (!onlyIfAbsent) {
// 如果存在key且hash相等,
// onlyIfAbsent为false,则更新旧值为value
e.value = value;
++modCount;// 修改数+1
}
break;
}
e = e.next;
} else {
// 节点不为null,
// 将node放在table[index]的HashEntry链的头部
if (node != null) {
node.setNext(first);
// 节点为null,
// 建新的HashEntry,放在链头,next指向链的原始头部
} else {
node = new HashEntry(hash, key, value, first);
}
//元素数量
int c = count + 1;
//元素数量大于临界条件,且小于最大容量,
//对HashTable扩容,创建2倍原始容量的Hashable
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
rehash(node);
else
// 添加node到table
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
unlock();
}
return oldValue;
}
首先尝试获取锁,如果获取锁失败,在scanAndLockForPut()方法中线性探测哈希桶,探测不到key对应的HashEntry,则创建一个新的HashEntry,一直重试MAX_SCAN_RETRIES次,还没有获取锁则放弃自旋使用阻塞方式获取锁。
如果在桶中找到KEY相同的节点,进行值变更,否则将新节点插入当前链表头。如果元素数量大于rehash临界值,进行重新hash新建一个为当前容器容量两倍的容器,将 table 指向新容器。
get操作:
public V get(Object key) {
Segment s;//数据段
HashEntry[] tab;//数据段中hash表
int h = hash(key.hashCode());//获取hash码
long u = (((h >>> segmentShift) &
segmentMask) << SSHIFT) + SBASE;
if ((s = (Segment)UNSAFE.getObjectVolatile(segments, u))
//段不为空且hash表不为空
!= null && (tab = s.table) != null) {
for (HashEntry e = (HashEntry)
UNSAFE.getObjectVolatile(tab, ((long)(((tab.length - 1) & h))
<< TSHIFT) + TBASE);
e != null; e = e.next) {//遍历hash桶
K k;
if ((k = e.key) == key || (e.hash == h && key.equals(k)))
return e.value;
}
}
return null;
}
先用hash定位段,再用hash定位段中的table,如果HashEntry不存在hash桶,只需要两步就能取出非常高效。
而且过程不需要加锁,put/remove都是针对HashEntry内的变量和指针进行原子性的赋值操作。getObjectVolatile方法以Volatile方式获取变量,能确保变量的可见性,取出的都是最新值。
ConcurrentHashMap的并发性主要体现在锁分拆上,分段加锁使ConcurrentHashMap具有更高的吞吐量,落在每把锁上的请求频率、持有时间会降低。
ConcurrentSkipListMap
SkipList跳表是一种随机化的数据结构,基于并联的链表,其效率可比拟于二叉查找树。
图
跳表中的节点具有右向与下向指针。从第一层开始遍历,如果右端的值比期望的大,那就往下走一层,继续往前走,所以在列表中的查找可以快速的跳过部分列表,并因此得名。
ConcurrentSkipListMap就是基于SkipList结构实现的map,在理论上能够在O(log(n))时间内完成查找、插入、删除操作。能像ConcurrentHashMap一样在并发环境下使用,又能像TreeMap一样使Key按照的自然顺序排序或者按照compareTo方法排序。
在并发环境下ConcurrentSkipListMap的性能比加锁的TreeMap高,逊于ConcurrentHashMap。调用ConcurrentSkipListMap的size时,由于多个线程可以同时对映射表进行操作,所以映射表需要遍历整个链表才能返回元素个数,这个方法慎用。
码字不易,转载请保留原文连接https://www.jianshu.com/p/a7f50d1ed33f