1.前言,分析当前问题
不用说,concurrenthashmap就是为了解决map操作时并发问题,因为hashmap在多线程扩容的时候,扩容有个特征就是扩容一次后链表中的数据和原数据顺序是反的,比如在数组索引位子a的链表数据是1,2,3.扩容后数组a的链表数据就成了3,2,1.这个时候线程2并发的话,链表数据和next数据就是反的,就有可能形成死循环.
hashtable可以解决这个问题,但是他是所有的方法都加上synchronized,虽然解决问题,但是影响性能,他会锁住整个数组,但是有时候如果并发不是在同一个索引位子,其实是没有问题的.
concurrenthashmap使用的是分段锁的方式,hashtable锁住整个table这么粗暴的方法可以变相的柔和点,比如在多线程的环境下,对不同的数据集进行操作时其实根本就不需要去竞争一个锁,因为他们不同hash值,不会因为rehash造成线程不安全,所以互不影响,这就是锁分离技术,将锁的粒度降低,利用多个锁来控制多个小的table
2.concurrenthashmap源码分析
先看结构图
从图中可以看到concurrenthashmap是由segement数组+hashEntry数组+链表组成
2.1:构造方法源码
/**
* 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.
*
* concurrencyLevel:并发级别
* initialCapacity:segement数组下所有hashEntry数组的大小
*/
@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;
//concurrentHashMap中segement数组的大小
int ssize = 1;
//确保segment数组的大小是2的幂次方最右接近并发量的那个数
while (ssize < concurrencyLevel) {
++sshift;
ssize <<= 1;
}
this.segmentShift = 32 - sshift;
this.segmentMask = ssize - 1;
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//通过所有segment数组下所有hashEntry数组大小除以segment数组大小计算每个segment下hashEntry数组的大小
int c = initialCapacity / ssize;
//
if (c * ssize < initialCapacity)
++c;
int cap = MIN_SEGMENT_TABLE_CAPACITY;
//确保每个segement下hashEntry数组大小是2
while (cap < c)
cap <<= 1;
// 创建segment对象,以后如果其他某个segment数组位子(除了第一个,第一个就是放的这个)需要创
//建segment就使用这个segment对象的结构参数来创建segment
Segment<K,V> s0 =
new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
//创建实际大小是所有segment下所有数组大小
//(也就是我们给的初始化大小)/并发级别后向上取整的大小.最小是2的hashEntry数组大小
(HashEntry<K,V>[])new HashEntry[cap]);
//创建一个segment数组, 大小是2的幂次方最右接近并发量的那个数
Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
//将创建的segment放在segment数组索引为0 的位子
UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
this.segments = ss;
}
2.2: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 <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();
int hash = hash(key);
//计算得到segment的索引.
// segmentMask是segment数组大小-1
// sshift是segment数组大小ssize由2的次幂计算得到的那个幂指数,比如ssize是16就是2的4次方,此时sshift就是4
// segmentShift 是 32 - ssize
int j = (hash >>> segmentShift) & segmentMask;
// 安全的通过unsafe从segment数组获取到索引j位子的segment对象,如果为空
if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck
(segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
//创建segment数组j位子的segment对象
s = ensureSegment(j);
//已经有了segment对象,将key,value放到segment中
return s.put(key, hash, value, false);
}
安全的创建segement数组并放入到segement数组中,简单描述就是如果segement数组u位子为空,那么就是用segement数组第一个segement对象的一些基本信息(比如segement下的hashEntry数组长度,加载因子等)创建u位置的segement对象,然后使用自旋锁,使用unsafe的cas安全的将新建的segement对象放入到segement数组中.返回新建的segement对象
/**
* Returns the segment for the given index, creating it and
* recording in segment table (via CAS) if not already present.
* 通过unsafe的cas创建segment数组给定索引位子的segment对象,
* 并放在segment数组该索引位子,返回创建的segement对象
*
* @param k the index
* @return the segment
*/
@SuppressWarnings("unchecked")
private Segment<K,V> ensureSegment(int k) {
final Segment<K,V>[] ss = this.segments;
//计算segment的索引k的物理地址
long u = (k << SSHIFT) + SBASE; // raw offset
Segment<K,V> seg;
//如果segment数组在u的位置没有segment对象,没有其他线程创建过segmen对象放在u索引位子
if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
//使用构造方法创建的第一个segment对象中的结构参数,比如segment内部hashentry数组大小,加载因子等
// use segment 0 as prototype,使用segement数组的第一个segement对象作为样本,使用第一个segement对象的参数信息来进行当前u位子的segement对象的创建,
//例如segement数组中u位子加载因子和u位子上segement下挂的hashEntry数组的大小等都是使用segement数组第一个segement对象的信息.
Segment<K,V> proto = ss[0];
// segement数组第一个位子下hashEntry数组长度
int cap = proto.table.length;
float lf = proto.loadFactor;
int threshold = (int)(cap * lf);
// 创建一个和segement数组第一个segement对象下的hashEntry数组大小一样的hashEntry数组.
HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
//再次判断segment在u的位置是否已经有了segment对象,防止其他线程已经创建,现在重复创建
if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
== null) { // recheck
//如果没有被创建,就自己创建一个segment对象
Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
//自旋锁,使用unsaft的cas给segment数组u位子放上创建的segment对象.
while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
== null) {
// 如果u的位子是null,将创建的segement s放入到segement数组ss中索引u的位子, seg不为空,结束循环.
if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
break;
}
}
}
//返回自己创建好或者别的线程创建好的segment对象.
return seg;
}
/**
* 遍历segment中的hashEntry数组
* 如果有key和hash都相同的就将新的数据覆盖老数据,老数据返回回去,中断循环hashEntry数组
* 如果一直循环到hashEntry数组最后e.next==null,
* 就会创建一个新的hashEntry对象,使用头插法插入到hashEntry数组,然后将链表往下移动一位(类比).
*
*/
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
//这个方式是segement的put方法,segment是继承ReentrantLock
// 加锁,先用trylock再使用lock阻塞加锁,并且得到一个新创建的hashEntry对象
HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value);
V oldValue;
try {
//当前segment中的hashEntry数组.
HashEntry<K,V>[] tab = table;
//计算得到索引值
int index = (tab.length - 1) & hash;
//获取索引值的那个hashEntry对象.
HashEntry<K,V> first = entryAt(tab, index);
//遍历这个hashEntry数组tab
for (HashEntry<K,V> e = first;;) {
//当前hashEntry不是空
if (e != null) {
K k;
//当前hashEntry的索引和传入的相同
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
//将当前hashEntry的旧值返回回去
oldValue = e.value;
if (!onlyIfAbsent) {
//将新值赋值给当前的hashEntry
e.value = value;
++modCount;
}
break;
}
// 继续循环hashEntry数组tab,判断是否和传进来的key和hash是否一致.
e = e.next;
}
else {
if (node != null)
//如果在scanAndLockForPut方法中创建过这个要put的hashEntry对象
//头插法放入到tab的hashEntry数组中
// 将第一个节点放到当前hashEntry对象后面,也就是头插法
node.setNext(first);
else {
//没有创建就创建好后,头插法放入到tab的hashEntry数组中
node = new HashEntry<K,V>(hash, key, value, first);
}
int c = count + 1;
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
//如果segement下的hashEntry数量超过阈值,扩容,将旧的数据克隆到新的hashEntry数组中
rehash(node);
else
//将传入的数据放在index索引位,注意这里的index是hashEntry数组头结点的索引,
// 也就是说将新的hashEntry对象放在原来第一个节点的位子,
// 将原来第一个节点放在这个新的hashEntry后面,完成头插法后的往下移动位子,链表长度增加一个.
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
//解锁
unlock();
}
return oldValue;
}
/**
*这个方法有两个出口,一个是 tryLock() 成功了,循环终止,另一个就是重试次数超过了
*MAX_SCAN_RETRIES,进到 lock() 方法,此方法会阻塞等待,直到成功拿到独占锁。
*这个方法就是看似复杂,但是其实就是创建了一个hashEntry对象并且获取该 segment 的独占锁
*
*
*/
private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
HashEntry<K,V> first = entryForHash(this, hash);
HashEntry<K,V> e = first;
HashEntry<K,V> node = null;
int retries = -1; // negative while locating node
//循环获取锁
while (!tryLock()) {
HashEntry<K,V> f; // to recheck first below
if (retries < 0) {
if (e == null) {
if (node == null) // speculatively create node
// 进到这里说明数组该位置的链表是空的,没有任何元素
// 进到这里的另一个原因是 tryLock() 失败
node = new HashEntry<K,V>(hash, key, value, null);
retries = 0;
}
else if (key.equals(e.key))
retries = 0;
else
e = e.next;
}
else if (++retries > MAX_SCAN_RETRIES) {
// 重试次数如果超过 MAX_SCAN_RETRIES(单核1多核64),那么不抢了,进入到阻塞队列等待锁
// lock() 是阻塞方法,直到获取锁后返回
lock();
break;
}
else if ((retries & 1) == 0 &&
(f = entryForHash(this, hash)) != first) {
// 这个时候就是有新的元素进到了链表,成为了新的表头
// 这边的策略是,重新走一遍 scanAndLockForPut 方法
e = first = f; // re-traverse if entry changed
retries = -1;
}
}
return node;
}
jdk8 hashmap的put方法源码
/**
* Implements Map.put and related methods
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab;
//当前hash对应的当前节点
Node<K,V> p;
int n, i;
//将hashMap的node数组赋值给tab,判断当前hashMap为空就开始初始化或者两倍扩容
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//获取到当前hash的node对象数据
if ((p = tab[i = (n - 1) & hash]) == null)
//如果为空,就创建当前node对象,赋值给tab数组的i(就是当前hash)位子.
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//如果当前key或hash相同,就将当前节点赋值给新创建的node类型字段e
e = p;
else if (p instanceof TreeNode)
//如果当前节点p是treeNode,就是用treeNode的put方法
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//循环链表
for (int binCount = 0; ; ++binCount) {
//当前hash的下个节点数据为空,也就是循环到了链表最后面,这里使用的是尾插法,这里就是和1.7不一样的地方
if ((e = p.next) == null) {
//创建该node节点,并赋值给p的next节点
p.next = newNode(hash, key, value, null);
//如果链表长度大于8
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
//就将node数组下hash位子的链表下的所有node转成treeNode再转成红黑树
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}