ConcurrentHashMap和HashMap的思路差不多,但是因为它支持并发锁,所以引入了分段锁,复杂一些。并发控制使用ReentrantLock来进行获取锁。
数据结构:
整个ConcurrentHashMap是由一个一个的Segment组成,Segment代表一个分段,一个Segment里面包含一个HashEntry数组,每个HashEntry是一个链表结构,当对HashEntry数据的数据进行修改时,必须先获取与它对应的Segment锁。
核心代码如下:
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V> implements ConcurrentMap<K,V>,
Serializable {
//默认初始容量
static final int DEFAULT_INITIAL_CAPACITY = 16;
//默认加载因子(针对Segment数组中的某个Segment中的HashEntry数组扩容)
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//默认Segment数组的大小,也成为并发量
static final int DEFAULT_CONCURRENCY_LEVEL = 16;
//最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//一个Segment的HashEntry数组的最小容量
static final int MIN_SEGMENT_TABLE_CAPACITY = 2;
//一个Segment的HashEntry数组的最大容量
static final int MAX_SEGMENTS = 1 << 16;
// 锁之前重试次数
static final int RETRIES_BEFORE_LOCK = 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的偏移
int ssize = 1;//segment的size = ssize * 2^sshift
//计算并行级别,保持并行级别是2的n次方
while (ssize < concurrencyLevel) {
//探讨默认情况下concurrencyLevel=16,sshift=4,ssize经过4此左移,和并行度相等=16
++sshift;
ssize <<= 1;
}
//下边这两个变量是为了put方法中的计算key对应Segment数组的索引
this.segmentShift = 32 - sshift;//-->默认为28
this.segmentMask = ssize - 1;//-->默认为15
//initialCapacity 是设置整个map初始的大小
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//这里根据initialCapacity 计算segment数组中的每个segment中的HashEntry数组可以分到的大小
//如initialCapacity =64,那么每个segment中的HashEntry数组就可以分到4个
int c = initialCapacity / ssize;
//当不能整除的时候,则让c+1
if (c * ssize < initialCapacity)
++c;
//默认MIN_SEGMENT_TABLE_CAPACITY=2.这个值也是有用的,因为这样的话,对于具体的HashEntry上,插入一个元素不至于扩容,插入第二个的时候才会扩容
int cap = MIN_SEGMENT_TABLE_CAPACITY;
while (cap < c)
cap <<= 1;
// 创建Segment数组,并创建数组的第一个元素,segment[0]
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];
//将s0写入segment[0]
UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
this.segments = ss;
}
public V put(K key, V value) {
Segment<K,V> s;
//判断value是否为null,如果为null则抛出空指针异常
if (value == null)
throw new NullPointerException();
//计算key的hash值
int hash = hash(key);
//根据key的hash值计算出在Segment数组中的位置j
//hash值是32位的,默认情况下先无符号右移28位,剩下高四位,然后&15,还是hash的高四位
//也就是说j是hash的高4位的值,也就是对应的segment数组中的下标
int j = (hash >>> segmentShift) & segmentMask;
//判断该位置是否为null,
if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck
(segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
//如果为null,初始化该位置segment[j],通过ensureSegment(j)
s = ensureSegment(j);
//调用Segment的put方法将数据插入到HashEntry中
//见下方
return s.put(key, hash, value, false);
}
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
//在往segment写之前,需要先获取该segment的独占锁
//获取到了直接返回null
//获取不到就会进入scanAndLockForPut()方法获取锁,初始化node,,具体我也没看懂
HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value);
V oldValue;
try {
//table是segment内部的数组,HashEntry类型的
HashEntry<K,V>[] tab = table;
//再利用hash,求应该放置的数组下标,和hashmap的一样
int index = (tab.length - 1) & hash;
//获取该位置的链表的表头,赋给first
HashEntry<K,V> first = entryAt(tab, index);
//一个死循环
//判断当前位置的链表是否为null,并针对两种情况具体操作
for (HashEntry<K,V> e = first;;) {
if (e != null) {
//如果当前链表中有元素
K k;
//判断当前key是否和当前链表上的节点的元素相等
//如果相等则直接覆盖并跳出循环
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 {
//如果当前链表中没有元素
//判断node是否为null
if (node != null)
//如果不为null则将元素添加在链表的头节点,并指向当前链表的头结点
node.setNext(first);
else
//如果node为null则初始化node,并将value传入,next指向当前链表的头节点
node = new HashEntry<K,V>(hash, key, value, first);
//计数+1
int c = count + 1;
//判断是否需要扩容
//如果当前segment中的元素个数大于扩容阈值并且HashEntry数组的长度小于规定的map最大容量,则进行扩容
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
//具体后边说
rehash(node);
else
//如果没有达到扩容的条件,将node放到数组HashEntry数组的index位置
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
//释放锁
unlock();
}
//返回旧值
return oldValue;
}
//在初始化ConcurrentHashMap的时候,会初始化第一个分段segment[0],对于其他分段,当put的时候才会进行初始化,通过ensureSegment()方法
private Segment<K,V> ensureSegment(int k) {
//获取到当前的Segment数组
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]
//用来当做一个模板来初始化其他的segmengt
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);
//初始化segment[k]内部的HashEntry数组
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 循环,内部用 CAS,当前线程成功设值或其他线程成功设值后,退出
//对于并发操作使用CAS控制
while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
== null) {
if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
break;
}
}
}
return seg;
}
//扩容,put的时候,如果判断该值的插入会导致segment中的元素个数超过阈值,那么先会进行扩容,再插值。
//该方法不需要考虑并发,因为到这里的时候,是持有该segment的独占锁
//扩容是segment里的HashEntry[]数组进行扩容,每个分段里的扩容都是独立的。
private void rehash(HashEntry<K,V> node) {
HashEntry<K,V>[] oldTable = table;
int oldCapacity = oldTable.length;
//新的容量为旧的容量的2倍
int newCapacity = oldCapacity << 1;
//计算新的扩容阈值
threshold = (int)(newCapacity * loadFactor);
//创建新数组
HashEntry<K,V>[] newTable =
(HashEntry<K,V>[]) new HashEntry[newCapacity];
//新的掩码,为容量-1
int sizeMask = newCapacity - 1;
//遍历数组,将原数组位置i处的链表拆分到新数组位置i和i+oldCapacity两个位置
for (int i = 0; i < oldCapacity ; i++) {
//e为链表的第一个元素
HashEntry<K,V> e = oldTable[i];
if (e != null) {
//如果e不为Null
HashEntry<K,V> next = e.next;
//计算应该放置在新数组中的位置
//假设原数组长度为16,e在oldTable[3]处,那么idx只可能是3或者3+16=19
//因为大多数HashEntry中的节点在扩容前后可以保持不变,rehash方法中会定位第一个后续所有节点在扩容后index都保持不变的节点,然后将这个节点之前的所有节点重排即可
int idx = e.hash & sizeMask;
if (next == null) // Single node on list
newTable[idx] = e;
else { // 重复利用一些扩容后,位置不变的节点,这些节点在原先链表的尾部
HashEntry<K,V> lastRun = e;
int lastIdx = idx;
//这个for循环就是找到第一个后续节点新的index不变的节点。
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;
// 第一个后续节点新index不变节点前的所有节点都需要重新创建分配
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放到新数组中刚刚的两个链表之一的头部
int nodeIndex = node.hash & sizeMask; // add the new node
node.setNext(newTable[nodeIndex]);
newTable[nodeIndex] = node;
table = newTable;
}
}
1、初始化
ConcurrentHashMap的初始化是会通过位与运算来初始化Segment的大小,用size来表示,因为size用位于运算来计算( size <<=1 ),所以Segment的大小取值都是以2的N次方,无关concurrencyLevel的取值,当然concurrencyLevel 最大只能用16位的二进制来表示,即65536,换句话说,Segment的大小最多65536个,没有指定concurrencyLevel元素初始化,Segment的大小size默认为16。初始化后,segment数组的长度就不会变化了。扩容的时候其实是对segment中的HashEntry[]数组进行扩容。
每一个Segment元素下的HashEntry的初始化也是按照位于运算来计算,用cap来表示,HashEntry大小的计算也是2的N次方(cap <<=1), cap的初始值为1,所以HashEntry最小的容量为2。
2、put操作
从Segment的继承体系可以看出,Segment继承了ReentrantLock, 也就带有锁的功能,当执行put操作时,会进行第一次key的hash来定位Segment的位置,如果该Segment还没有初始化,即通过CAS操作进行赋值,然后进行第二次hash操作,找到相应的HashEntry的位置,这里会利用继承过来的锁的特性,在将数据插入指定的HashEntry位置时(链表的尾端),会通过继承ReentrantLock的tryLock()方法尝试去获取锁,如果获取成功就直接插入相应的位置,如果已经有线程获取该Segment的锁,那当前线程会以自旋的方式去继续的调用tryLock()方法去获取锁,超过指定次数就挂起,等待唤醒。
3、get操作
ConcurrentHashMap的get操作跟HashMap类似,只是ConcurrentHashMap第一次需要经过一次hash定位到Segment的位置,然后再hash定位到指定的HashEntry,遍历该HashEntry下的链表进行对比,成功就返回,不成功就返回null。
4、size操作
计算ConcurrentHashMap的元素大小是一个有趣的问题,因为他是并发操作的,就是在你计算size的时候,他还在并发的插入数据,可能会导致你计算出来的size和你实际的size有相差(在你return size的时候,插入了多个数据),要解决这个问题,JDK1.7版本用两种方案
for(;;) {
if(retries++ == RETRIES_BEFORE_LOCK) {
for(int j = 0 ; j < segments.length; ++j)
ensureSegment(j).lock(); // force creation
}
sum = 0L;
size = 0;
overflow = false;
for ( int j = 0; j < segments.length; ++j) {
Segment<K,V> seg = segmentAt(segments, j);
if (seg != null) {
sum += seg.modCount;
int c = seg.count;
if (c < 0 || (size += c) < 0 )
overflow = true;
}
}
if (sum == last)
break;
last = sum;
}
}finally {
if (retries > RETRIES_BEFORE_LOCK) {
for ( int j = 0; j < segments.length; ++j)
segmentAt(segments, j).unlock();
}
}
第一种方案他会使用不加锁的模式去尝试多次计算ConcurrentHashMap的size,最多三次,比较前后两次计算的结果,结果一致就认为当前没有元素加入,计算的结果是准确的。
第二种方案是如果第一种方案不符合,他就会给每个Segment加上锁,然后计算ConcurrentHashMap的size返回。
5、并发问题分析
添加节点的操作put和删除节点的操作remove都是加上segment上的独占锁的,所以它们之前自然不会有问题,我们需要考虑的问题就是,get的时候在同一个segment中发生了put或remove操作。
(1)、put操作的线程安全性
(2)、remove操作的线程安全性
总结:
JDK1.7中的ConcurrentHashMap是如何保证线程安全的
JDK1.7中的ConcurrentHashMap的底层原理
JDK1.8中摒弃了Segment的概念,而是直接通过Node数组+ 链表+红黑树的数据结构来实现,并发控制使用Synchronized和CAS来操作。
核心源码如下:
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V> implements ConcurrentMap<K,V>,
Serializable {
/* --------- 常量及成员变量的设计 几乎与HashMap相差无几 -------- */
/**
* 最大容量
*/
private static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* 默认初始容量
*/
private static final int DEFAULT_CAPACITY = 16;
/**
* 单个数组最大容量
*/
static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
/**
* 默认并发等级,也就分成多少个单独上锁的区域
*/
private static final int DEFAULT_CONCURRENCY_LEVEL = 16;
/**
* 扩容因子
*/
private static final float LOAD_FACTOR = 0.75f;
/**
*
*/
transient volatile Node<K,V>[] table;
/**
*
*/
private transient volatile Node<K,V>[] nextTable;
/* --------- 系列构造方法,依然推荐在初始化时根据实际情况设置好初始容量 -------- */
public ConcurrentHashMap() {
}
public ConcurrentHashMap(int initialCapacity) {
if (initialCapacity < 0)
throw new IllegalArgumentException();
int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
MAXIMUM_CAPACITY :
tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
this.sizeCtl = cap;
}
public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
this.sizeCtl = DEFAULT_CAPACITY;
putAll(m);
}
public ConcurrentHashMap(int initialCapacity, float loadFactor) {
this(initialCapacity, loadFactor, 1);
}
public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
if (initialCapacity < concurrencyLevel) // Use at least as many bins
initialCapacity = concurrencyLevel; // as estimated threads
long size = (long)(1.0 + (long)initialCapacity / loadFactor);
int cap = (size >= (long)MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY : tableSizeFor((int)size);
this.sizeCtl = cap;
}
//Node数据结构很简单,是一个链表,但是只允许对数据进行查找,不允许进行修改
static class Node<K,V> implements Map.Entry<K,V> {
//链表的数据结构
final int hash;
final K key;
//val和next都会在扩容时发生变化,所以加上volatile来保持可见性和禁止重排序
volatile V val;
volatile Node<K,V> next;
Node(int hash, K key, V val, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.val = val;
this.next = next;
}
public final K getKey(){
return key;
}
public final V getValue(){
return val;
}
public final int hashCode(){
return key.hashCode() ^ val.hashCode();
}
public final String toString(){
return key + "=" + val;
}
//不允许更新value
public final V setValue(V value) {
throw new UnsupportedOperationException();
}
public final boolean equals(Object o) {
Object k, v, u; Map.Entry<?,?> e;
return ((o instanceof Map.Entry) && (k = (e = (Map.Entry<?,?>)o).getKey()) != null && (v = e.getValue()) != null && (k == key || k.equals(key)) && (v == (u = val) || v.equals(u)));
}
//用于map中的get()方法,子类重写
Node<K,V> find( int h, Object k) {
Node<K,V> e = this;
if (k != null) {
do {
K ek;
if (e.hash == h && ((ek = e.key) == k || (ek != null && k.equals(ek))))
return e;
} while ((e = e.next) != null);
}
r eturn null ;
}
}
/**
* ConcurrentHashMap 的核心就在于其put元素时 利用synchronized局部锁 和
* CAS乐观锁机制 大大提升了本集合的并发能力,比JDK7的分段锁性能更强
*/
public V put(K key, V value) {
return putVal(key, value, false);
}
/**
* 当前指定数组位置无元素时,使用CAS操作 将 Node键值对 放入对应的数组下标。
* 出现hash冲突,则用synchronized局部锁锁住,若当前hash对应的节点是链表的头节点,遍历链表,
* 若找到对应的node节点,则修改node节点的val,否则在链表末尾添加node节点;倘若当前节点是
* 红黑树的根节点,在树结构上遍历元素,更新或增加节点
*/
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 注意!这是一个CAS的方法,将新节点放入指定位置,不用加锁阻塞线程
// 也能保证并发安全
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
// 当前Map在扩容,先协助扩容,在更新值
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else { // hash冲突
V oldVal = null;
// 局部锁,有效减少锁竞争的发生
synchronized (f) { // f 是 链表头节点/红黑树根节点
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
// 若节点已经存在,修改该节点的值
if (e.hash == hash && ((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
// 节点不存在,添加到链表末尾
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
// 如果该节点是 红黑树节点
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
// 链表节点超过了8,链表转为红黑树
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
// 统计节点个数,检查是否需要resize
addCount(1L, binCount);
return null;
}
}
与JDK1.7在同步机制上的区别 总结如下:
JDK1.7 使用的是分段锁机制,其内部类Segment 继承了 ReentrantLock,将 容器内的数组划分成多段区域,每个区域对应一把锁,相比于HashTable确实提升了不少并发能力,但在数据量庞大的情况下,性能依然不容乐观,只能通过不断的增加锁来维持并发性能。而JDK1.8则使用了 CAS乐观锁 + synchronized局部锁 处理并发问题,锁粒度更细,即使数据量很大也能保证良好的并发性。