1.7版本
概述
注意,以下代码都是1.7版本(不同版本代码不一样),最下面有1.8版本部分内容
ConcurrentHashMap是线程安全的key value存储结构,底层也是数组+链表的结构
下面是构造图,存放数据的是Segments数组(元素是Segment类),每个Segment类中都有HashEntry的数组,每个 HashEntry 是一个链表结构的元素
注意,用 volatile修饰table数组,一个线程修改后别的线程立刻可见
static final class Segment<K,V> extends ReentrantLock implements Serializable {
private static final long serialVersionUID = 2249069246763182397L;
// 和 HashMap 中的 HashEntry 作用一样,真正存放数据的桶
transient volatile HashEntry<K,V>[] table;
transient int count;
// 快速失败(fail—fast)用到的变量
transient int modCount;
// 大小
transient int threshold;
// 负载因子
final float loadFactor;
}
ConcurrentHashMap 采用了分段锁技术,其中 Segment 继承于 ReentrantLock,
理论上 ConcurrentHashMap 支持 CurrencyLevel (Segment 数组数量)的线程并发。每当一个线程占用锁访问一个 Segment 时,不会影响到其他的 Segment。就是说如果容量大小是16他的并发度就是16,可以同时允许16个线程操作16个Segment而且还是线程安全的。
每个 Segment 维护着一个 HashEntry 数组里的元素,当要对 HashEntry 的数据进行修改时,就必须先获得对应的 Segement 锁
为什么要用二次hash,主要原因是为了构造分离锁,使得对于map的修改不会锁住整个容器,提高并发能力。当然,没有一种东西是绝对完美的,二次hash带来的问题是整个hash的过程比hashmap单次hash要长,所以,如果不是并发情形,不要使concurrentHashmap。
JAVA7之前ConcurrentHashMap主要采用锁机制,在对某个Segment进行操作时,将该Segment锁定,不允许对其进行非查询操作,而在JAVA8之后采用CAS无锁算法,这种乐观操作在完成前进行判断,如果符合预期结果才给予执行,对并发操作提供良好的优化.
初始化
下面是this的源码和对应的注释
/**
* Creates a new, empty map with the specified initial
* capacity, load factor and concurrency level.
*
* @param initialCapacity the initial capacity. The implementation
* performs internal sizing to accommodate this many elements.
* @param loadFactor the load factor threshold, used to control resizing.
* Resizing may be performed when the average number of elements per
* bin exceeds this threshold.
* @param concurrencyLevel the estimated number of concurrently
* updating threads. The implementation performs internal sizing
* to try to accommodate this many threads.
* @throws IllegalArgumentException if the initial capacity is
* negative or the load factor or concurrencyLevel are
* nonpositive.
*/
@SuppressWarnings("unchecked")
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,找到大于并发要求值的最小2的倍数,用于当成segment的数组长度
while (ssize < concurrencyLevel) {
++sshift;
ssize <<= 1;
}
this.segmentShift = 32 - sshift;
this.segmentMask = ssize - 1;
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//初始化要求的长度除以segment数组长度,
//是为了计算每个segment中的数组HashEntry数组应该有多少个元素,
int c = initialCapacity / ssize;
//c的值乘以ssize必须大于initialCapacity,因为c是除的结果,
//如果有余数,c+1再乘ssize肯定大于initialCapacity
if (c * ssize < initialCapacity)
++c;
//用于计算c的值是否小于最小值,如果c很小(等于1),
//默认segment有2个元素,如果c的值大,找到大于c的2的倍数
//cap用于构造一个segment元素中数组的长度
int cap = MIN_SEGMENT_TABLE_CAPACITY;
while (cap < c)
cap <<= 1;
//为了减少占用空间,除了第一个 Segment 之外,
//剩余的 Segment 采用的是延迟初始化的机制,
//仅在第一次需要时才会创建(通过 ensureSegment 实现)
//方便后面创建的segment对象可以直接使用这个segment的参数,不需要重复计算
// create segments and segments[0]
Segment<K,V> s0 =
new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
(HashEntry<K,javascript:void(0)V>[])new HashEntry[cap]);
Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
//为了保证延迟初始化存在的可见性,
//访问 segments 数组及 table 数组中的元素均通过 volatile 访问,
//主要借助于 Unsafe 中原子操作 getObjectVolatile 来实现
// 将s0插入到下标index=0上
UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
this.segments = ss;
}
新增元素
/**
* Maps the specified key to the specified value in this table.
* Neither the key nor the value can be null.
*
* <p> The value can be retrieved by calling the <tt>get</tt> method
* with a key that is equal to the original key.
*
* @param key key with which the specified value is to be associated
* @param value value to be associated with the specified key
* @return the previous value associated with <tt>key</tt>, or
* <tt>null</tt> if there was no mapping for <tt>key</tt>
* @throws NullPointerException if the specified key or value is null
*/
@SuppressWarnings("unchecked")
public V put(K key, V value) {
Segment<K,V> s;
if (value == null)
throw new NullPointerException();
//通过key计算hash值
int hash = hash(key);
//通过hash计算出segment数组下标
int j = (hash >>> segmentShift) & segmentMask;
//如果数组下标的元素为空,生成segment元素并赋值给s
//如果数组下标的元素不为空,就赋值给s,获取元素是CAS操作
if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck
(segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
s = ensureSegment(j);
//在数组下标的元素中新增值
return s.put(key, hash, value, false);
}
此处跟HashMap很像,
hash的计算
- 如果是字符串,用字符串的hash进行替换,减少hash冲突的概率
- 获取key的hash code,然后跟h做异或(相同为0,不同为1)
- 最下面那几行,目的是在散列计算中减少碰撞的概率,但为啥那么写,我也没看懂
ensureSegment返回一个segment
/**
* Returns the segment for the given index, creating it and
* recording in segment table (via CAS) if not already present.
*
* @param k the index
* @return the segment
*/
@SuppressWarnings("unchecked")
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) {
//用第0个元素的值构造新的segment元素,避免计算,速度快
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);
//生成新的元素中HashEntry列表
HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
//再次看下元素是否为空,每次都是CAS原子操作,但是防止不了多线程并发
if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
== null) { // recheck
//生成新的segment元素
Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
//如果获取到的元素为空,用CAS替换,
//如果替换没成功再次while查看,如果被其他线程并发替换了,while就退出
//自旋锁的设计,不停while循环尝试
while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
== null) {
if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
break;
}
}
}
return seg;
}
segment插入元素
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
//获取可重入锁,如果没有获得锁就一直等着
HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value);
V oldValue;
try {
//找到segment对应的table
HashEntry<K,V>[] tab = table;
//根据hash算出下标
int index = (tab.length - 1) & hash;
//找到HashTable列表的首节点,要遍历链表
HashEntry<K,V> first = entryAt(tab, index);
for (HashEntry<K,V> e = first;;) {
//从首节点开始遍历,查看是否有KEY相同的元素,有就覆盖
if (e != null) {
K k;
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 {
//遍历到链表的尾部,e为null,要插入新的元素到链表中
//用头插法,新增节点node的next指向first,然后将node放到头结点
if (node != null)
node.setNext(first);
else
node = new HashEntry<K,V>(hash, key, value, first);
int c = count + 1;
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
//扩容逻辑
rehash(node);
else
//将新增节点放到HashEntry数组的下标元素(链表首个元素)中
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
//最终解锁
unlock();
}
return oldValue;
}
scanAndLockForPut()
/**
* Scans for a node containing given key while trying to
* acquire lock, creating and returning one if not found. Upon
* return, guarantees that lock is held. UNlike in most
* methods, calls to method equals are not screened: Since
* traversal speed doesn't matter, we might as well help warm
* up the associated code and accesses as well.
*
* @return a new node if key not found, else null
*/
private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
//获取在Segment对应位置的首节点
HashEntry<K,V> first = entryForHash(this, hash);
HashEntry<K,V> e = first;
HashEntry<K,V> node = null;
//找到标志位就会变为0
int retries = -1; // negative while locating node
//不停的做自旋锁,尝试抢锁
while (!tryLock()) {// 尝试加锁,无论是否抢到锁,都立刻返回值,不阻塞
// 循环遍历e,直到找到key值相等或为null
HashEntry<K,V> f; // to recheck first below
if (retries < 0) {
if (e == null) {
if (node == null) // speculatively create node
//如果首节点是空的,new个新节点
node = new HashEntry<K,V>(hash, key, value, null);
retries = 0;
}
else if (key.equals(e.key)) //找到KEY一样的节点
retries = 0;
else
e = e.next;//首节点不是空的而且KEY不一样,在链表中往后遍历
}
else if (++retries > MAX_SCAN_RETRIES) {
//while超过最大重试次数,用阻塞的锁,只有等到抢到锁了才会往下走
lock();
break;
}
else if ((retries & 1) == 0 &&
(f = entryForHash(this, hash)) != first) {
// 在抢锁的过程中,有可能别的线程修改了值
// retries为偶数,且重现获取的首节点值与first不相等时
// 重新进行遍历
e = first = f; // re-traverse if entry changed
retries = -1;
}
}
return node;
}
entryForHash()
static final <K,V> HashEntry<K,V> entryForHash(Segment<K,V> seg, int h) {
HashEntry<K,V>[] tab;
// 根据hash值获取对应在Segment中的值
return (seg == null || (tab = seg.table) == null) ? null :
(HashEntry<K,V>) UNSAFE.getObjectVolatile
(tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
}
扩容rehash()
/**
* Doubles size of table and repacks entries, also adding the
* given node to new table
*/
@SuppressWarnings("unchecked")
private void rehash(HashEntry<K,V> node) {
/*
* Reclassify nodes in each list to new table. Because we
* are using power-of-two expansion, the elements from
* each bin must either stay at same index, or move with a
* power of two offset. We eliminate unnecessary node
* creation by catching cases where old nodes can be
* reused because their next fields won't change.
* Statistically, at the default threshold, only about
* one-sixth of them need cloning when a table
* doubles. The nodes they replace will be garbage
* collectable as soon as they are no longer referenced by
* any reader thread that may be in the midst of
* concurrently traversing table. Entry accesses use plain
* array indexing because they are followed by volatile
* table write.
*/
HashEntry<K,V>[] oldTable = table;
int oldCapacity = oldTable.length;
int newCapacity = oldCapacity << 1;//新扩容长度为老长度2倍
threshold = (int)(newCapacity * loadFactor);
HashEntry<K,V>[] newTable =
(HashEntry<K,V>[]) new HashEntry[newCapacity]; //申请新的数组
int sizeMask = newCapacity - 1;
//数组中每个元素都是个链表,以元素为维度for循环,每个元素还要处理链表
for (int i = 0; i < oldCapacity ; i++) {
HashEntry<K,V> e = oldTable[i];
//如果元素为null,不用做操作,如果不为null,要挪到新的数组上
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;
//看看整个链表在新数组的位置是不是都和头节点一样
//如果新的索引下标有和头结点不一样的节点
//(老的数组一样下标的元素在新的元素中可能下标不一致)
//找到第一个和头节点不一致的元素,比如1~5节点在老的数组中都是0下标
//但新的数组可能:1~3在0下标,4~5在8下标(只是举个例子)
//此时找到新的数组中,和头结点(1节点)不一样的第一个下标(4节点)
//找到4节点后5节点就不会修改变量,因为lastIdx已经修改过了
for (HashEntry<K,V> last = next;
last != null;
last = last.next) {
int k = last.hash & sizeMask;
if (k != lastIdx) {
lastIdx = k;
lastRun = last;
}
}
//将上述例子的4节点放到新的数组中
newTable[lastIdx] = lastRun;
//将上述1~3节点挪到新数组中,头插法
// 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
node.setNext(newTable[nodeIndex]);
newTable[nodeIndex] = node;
table = newTable;
}
获取元素
计算在哪个segment中,通过CAS查到内存数据,在segment元素中查到在HashEntry数组中哪个节点,然后根据节点遍历链表,key值地址相等或者(hash 和equasl同时相等)查找元素
注意这地方没有锁,由于是volatile作用,保持可见性,直接查内存,所以不用担心查到脏数据
16版本
concurrenthashmap1.7和1.8的区别
ConcurrentHashMap 与HashMap和Hashtable 最大的不同在于:put和 get 两次Hash到达指定的HashEntry,第一次hash到达Segment,第二次到达Segment里面的Entry,然后在遍历entry链表。 从1.7到1.8版本,由于HashEntry从链表 变成了红黑树所以 concurrentHashMap的时间复杂度从O(n)到O(log(n))。
HashEntry最小的容量为2,Segment的初始化容量是16,HashEntry在1.8中称为Node,链表转红黑树的值是8 ,当Node链表的节点数大于8时Node会自动转化为TreeNode,会转换成红黑树的结构。
其实可以看出JDK1.8版本的ConcurrentHashMap的数据结构已经接近HashMap,相对而言,ConcurrentHashMap只是增加了同步的操作来控制并发,
- JDK1.7版本的ReentrantLock+Segment+HashEntry,Segment数组的意义就是将一个大的table分割成多个小的table来进行加锁,也就是上面的提到的锁分离技术,而每一个Segment元素存储的是HashEntry数组+链表,这个和HashMap的数据存储结构一样。
- JDK1.8版本中synchronized+CAS+HashEntry+红黑树,移除Segment,使锁的粒度更小,Synchronized + CAS + Node
概述
数据存放就一个数组table,没有segment了。
sizeCtl:表初始化和调整大小控制。如果为负数,则表正在初始化或调整大小:-1 表示初始化,否则 -(1 + 活动调整大小线程的数量)。否则,当 table 为 null 时,保存要在创建时使用的初始表大小,或者默认为 0。初始化后,保存下一个元素计数值,根据该值调整表的大小
初始化
此处并没有申请数组空间,是在PUT的时候做数组空间初始化
新增元素
put
/**
* Maps the specified key to the specified value in this table.
* Neither the key nor the value can be null.
*
* <p>The value can be retrieved by calling the {@code get} method
* with a key that is equal to the original key.
*
* @param key key with which the specified value is to be associated
* @param value value to be associated with the specified key
* @return the previous value associated with {@code key}, or
* {@code null} if there was no mapping for {@code key}
* @throws NullPointerException if the specified key or value is null
*/
public V put(K key, V value) {
return putVal(key, value, false);
}
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
// put的ket、value不能为null
if (key == null || value == null) throw new NullPointerException();
//通过code计算hash,使用扰动处理,减少碰撞概率
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh; K fk; V fv;
//如果第一次调用put,初始化数组
if (tab == null || (n = tab.length) == 0)
tab = initTable();
//如果hash的位置还没有值,CAS新增一个元素就返回
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
break; // no lock when adding to empty bin
}
//以下逻辑都是在HASH位置上已经有值了,走链表逻辑
//扩容
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
//onlyIfAbsent一般是false,不会走到这个逻辑
else if (onlyIfAbsent // check first node without acquiring lock
&& fh == hash
&& ((fk = f.key) == key || (fk != null && key.equals(fk)))
&& (fv = f.val) != null)
return fv;
else {
V oldVal = null;
//HASH对应数组元素头结点加锁
synchronized (f) {
//重新计算数组HASH位置头结点有没有被篡改,防止高并发
if (tabAt(tab, i) == f) {
//fh是hash,大于0走链表,否则走红黑树
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)))) {
//这是找到相同的KEY,替换VALUE
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
//不停寻找链表的next节点,直到末尾还没找到就新增一个节点
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key, value);
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;
}
}
else if (f instanceof ReservationNode)
throw new IllegalStateException("Recursive update");
}
}
if (binCount != 0) {
//如果链表深度超过阈值,转为红黑树
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
//计数
addCount(1L, binCount);
return null;
}
initTable()
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) { // 若当前table未初始化,进入循环
if ((sc = sizeCtl) < 0) // sizeCtl<0 代表在初始化或调整大小
Thread.yield(); // 让出cpu调度时间
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { // CAS 将sizeCtl修改为-1,表示当前table正在初始化
try {
if ((tab = table) == null || tab.length == 0) { // 双重锁检查
int n = (sc > 0) ? sc : DEFAULT_CAPACITY; // sc大于0表示,初始化了容量直接使用该容量,反之则使用默认容量16
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt; // 初始化table
sc = n - (n >>> 2); // sc保存当前扩容阈值,即为0.75x当前容量
}
} finally {
sizeCtl = sc;
}
break;
}
}
return tab;
}
获取数据
//会发现源码中没有一处加了锁
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
int h = spread(key.hashCode()); //计算hash
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {//读取首节点的Node元素
if ((eh = e.hash) == h) { //如果该节点就是首节点就返回
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
//hash值为负值表示正在扩容,这个时候查的是ForwardingNode的find方法来定位到nextTable来
//eh=-1,说明该节点是一个ForwardingNode,正在迁移,此时调用ForwardingNode的find方法去nextTable里找。
//eh=-2,说明该节点是一个TreeBin,此时调用TreeBin的find方法遍历红黑树,由于红黑树有可能正在旋转变色,所以find里会有读写锁。
//eh>=0,说明该节点下挂的是一个链表,直接遍历该链表即可。
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
while ((e = e.next) != null) {//既不是首节点也不是ForwardingNode,那就往下遍历
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
总结:
- 在1.8中ConcurrentHashMap的get操作全程不需要加锁,这也是它比其他并发集合比如hashtable、用Collections.synchronizedMap()包装的hashmap;安全效率高的原因之一。
- get操作全程不需要加锁是因为Node的成员val是用volatile修饰的和数组用volatile修饰没有关系。
- 数组用volatile修饰主要是保证在数组扩容的时候保证可见性。
参考文章:
ConcurrentHashMap1.8源码解析_坚持一定很Cool的博客-CSDN博客
ConcurrentHashMap1.7源码解析_坚持一定很Cool的博客-CSDN博客_concurrenthashmap源码分析1.7
ConcurrentHashmap(1.8)get操作——为什么它不需要加锁呢_刘翔UP的博客-CSDN博客_concurrenthashmapget为什么不加锁