JDK7下的ConcurrentHashMap
capacity:当前数组容量,始终保持 2^n。如果当前的 size 已经达到了阈值,并且要插入的数组位置上已经有元素,那么就会触发扩容,扩容后数组大小为当前的 2 倍。
由于是双倍扩容,迁移过程中,会将原来 table[i] 中的链表的所有节点,分拆到新的数组的 newTable[i] 和 newTable[i + oldLength] 位置上。如原来数组长度是 16,那么扩容后,原来 table[0] 处的链表中的所有元素会被分配到新数组中 newTable[0] 和 newTable[16] 这两个位置。
分段锁:整个 ConcurrentHashMap 由一个个 Segment 组成,Segment 代表”部分“或”一段“的意思,所以很多地方都会将其描述为分段锁。
ConcurrentHashMap 是一个 Segment 数组,Segment 通过继承 ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全。
线程安全的原因:获取segment 的独占锁,然后再进行元素的插入操作
并发度concurrencyLevel
concurrencyLevel:并行级别、并发数、Segment 数。默认是 16,也就是说 ConcurrentHashMap 有 16 个 Segments,所以理论上,这个时候,最多可以同时支持 16 个线程并发写,只要它们的操作分别分布在不同的 Segment 上。这个值可以在初始化的时候设置为其他值,但是一旦初始化以后,它是不可以扩容的。
并发度可以理解为程序运行时能够同时更新ConccurentHashMap且不产生锁竞争的最大线程数,实际上就是ConcurrentHashMap中的分段锁个数,即Segment[]的数组长度。ConcurrentHashMap默认的并发度为16,但用户也可以在构造函数中设置并发度。当用户设置并发度时,ConcurrentHashMap会使用大于等于该值的最小2幂指数作为实际并发度(假如用户设置并发度为17,实际并发度则为32)。运行时通过将key的高n位(n = 32 – segmentShift)和并发度减1(segmentMask)做位与运算定位到所在的Segment。segmentShift与segmentMask都是在构造过程中根据concurrency level被相应的计算出来。
如果并发度设置的过小,会带来严重的锁竞争问题;如果并发度设置的过大,原本位于同一个Segment内的访问会扩散到不同的Segment中,CPU cache命中率会下降,从而引起程序性能下降。(文档的说法是根据你并发的线程数量决定,太多会导性能降低)
初始化过程分析:
public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) { if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0) throw new IllegalArgumentException(); if (concurrencyLevel > MAX_SEGMENTS)//默认segment数为16 concurrencyLevel = MAX_SEGMENTS; // Find power-of-two sizes best matching arguments int sshift = 0; int ssize = 1; //ssize左移使得它为2的n次方。这里是16,sshift是4 while (ssize < concurrencyLevel) { ++sshift; ssize <<= 1; } this.segmentShift = 32 - sshift;//移位数:28 this.segmentMask = ssize - 1; if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; int c = initialCapacity / ssize;//得到每个segment数组的大小 if (c * ssize < initialCapacity) ++c; int cap = MIN_SEGMENT_TABLE_CAPACITY; //这里让数组大小最小值为2。作用是为了让segment数组为最小值2的时候加入第二个元素才进行扩容。 while (cap < c) cap <<= 1; // create segments and segments[0] //创建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]; UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0] this.segments = ss; }
- Segment 数组长度为 16,不可以扩容
- Segment[i] 的默认大小为 2,负载因子是 0.75,得出初始阈值为 1.5,也就是以后插入第一个元素不会触发扩容,插入第二个会进行第一次扩容
- 这里初始化了 segment[0],其他位置还是 null
- 当前 segmentShift 的值为 32 - 4 = 28,segmentMask 为 16 - 1 = 15,简单翻译为移位数和掩码
|
Put过程分析:
链接新节点的下一个节点(HashEntry.setNext())以及将链表写入到数组中(setEntryAt())都是通过Unsafe的putOrderedObject()方法来实现。
这里并未使用具有原子写语义的putObjectVolatile()的原因是:JMM会保证获得锁到释放锁之间所有对象的状态更新都会在锁被释放之后更新到主存,从而保证这些变更对其他线程是可见的。
特别注意:ConcurrentHashMap不允许key和value值为空。按照Doug Lea的说法,这么设计的原因是在ConcurrentHashMap中,一旦value出现null,则代表HashEntry的key/value没有映射完成就被其他线程所见,需要特殊处理。
public V put(K key, V value) { Segment<K,V> s; if (value == null) throw new NullPointerException(); //计算key的hash值 int hash = hash(key); //初始化的时候,segmentShift是28,segmentMask是15 //这里是取hash的高四位,然后再跟15做一个与操作 int j = (hash >>> segmentShift) & segmentMask; //初始化的时候只初始化了segment[0],其他还是null值。所以使用ensureSegment初始化segment[j] 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); } |
final V put(K key, int hash, V value, boolean onlyIfAbsent) { //获取segment的独占锁 HashEntry<K,V> node = tryLock() ? null : scanAndLockForPut(key, hash, value); V oldValue; try { //拿到segment数组 HashEntry<K,V>[] tab = table; //求到数组的下标值 int index = (tab.length - 1) & hash; HashEntry<K,V> first = entryAt(tab, index); for (HashEntry<K,V> e = first;;) { if (e != null) { K k; //segment[index]中有这个key了,覆盖旧值 if ((k = e.key) == key || (e.hash == hash && key.equals(k))) { oldValue = e.value;//返回旧值给调用者 if (!onlyIfAbsent) { e.value = value; ++modCount; } break; } //无这个key,链表往下找,直至找到e == null e = e.next; } else { // 如果node不为 null,那就直接将它设置为链表表头;如果是null,初始化并设置为链表表头 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 //不扩容就将node加到segment数组的index位置上 setEntryAt(tab, index, node); ++modCount; count = c; oldValue = null;//没有旧值就返回null值给调用者 break; } } } finally { unlock(); } return oldValue; } |
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; } 这个方法有两个出口,一个是 tryLock() 成功了,循环终止,另一个就是重试次数超过了 MAX_SCAN_RETRIES,进到 lock() 方法,此方法会阻塞等待,直到成功拿到独占锁。 这个方法就是看似复杂,但是其实就是做了一件事,那就是获取该 segment 的独占锁 |
初始化Segment
在put操作的时候,因为初始化的时候只初始化了segment[0],所以如果j!=0的情况下,需要对segment[j]进行初始化的操作。
这里需要注意的是,当前的操作可能发生在并发的环境下。
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) { //拿到当前的segment[0] Segment<K,V> proto = ss[0]; // use segment 0 as prototype //使用当前的segment[0]的长度和加载因子。这里的segment[0]可能进行过扩容操作。 int cap = proto.table.length; float lf = proto.loadFactor; int threshold = (int)(cap * lf); HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap]; //因为是有可能在并发的环境下,这里if检查是否已经被其他的线程创建了这个segment //使用了Unsafe对象的getObjectVolatile()提供的原子读语义结合CAS来确保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 ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) { //while循环里面是CAS操作,将刚才新创建segment赋值给seg //while循环是为了CAS失败的时候能够成功的赋值给seg并返回 if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s)) break; } } } return seg; } |
Segment内部数组扩容
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. 这里说了一个地方:在默认的阈值下,只有大约六分之一的节点需要进行克隆 */ //拿到segment内部数组,进行新数组的生成,扩容到原来的2倍。左移1位 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++) { 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循环是得到lastRun,lastRun后面的节点都是跟lastRun在同一个新的位置上(lastIdx) for (HashEntry<K,V> last = next; last != null; last = last.next) { int k = last.hash & sizeMask; if (k != lastIdx) { lastIdx = k; lastRun = last; } } //将lastRun及其后面的的节点放在lastIdx的位置上 newTable[lastIdx] = lastRun; // Clone remaining nodes //这个for循环是将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); } } } } int nodeIndex = node.hash & sizeMask; // add the new node node.setNext(newTable[nodeIndex]); newTable[nodeIndex] = node; table = newTable; } |
GET方法分析
public V get(Object key) { Segment<K,V> s; // manually integrate access methods to reduce overhead HashEntry<K,V>[] tab; int h = hash(key); long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE; if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null && (tab = s.table) != null) { 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; } } return null; } //emm....获取hash后对应的位置上的segment中的数组上对应的HashEntry链表,然后遍历匹配返回对应值,没找到则返回null值 |
Remove方法分析
public V remove(Object key) { int hash = hash(key); Segment<K,V> s = segmentForHash(hash); return s == null ? null : s.remove(key, hash, null); } //计算key的hash获取对应的segment |
final V remove(Object key, int hash, Object value) { //获取segment的独占锁 if (!tryLock()) scanAndLock(key, hash); V oldValue = null; try { HashEntry<K,V>[] tab = table; int index = (tab.length - 1) & hash; HashEntry<K,V> e = entryAt(tab, index); HashEntry<K,V> pred = null; //遍历HashEntry的链表 while (e != null) { K k; HashEntry<K,V> next = e.next; if ((k = e.key) == key || (e.hash == hash && key.equals(k))) { V v = e.value; if (value == null || value == v || value.equals(v)) { //这里是:如果第一次就找到了key对应的位置,然后直接将next设成tab 数组index位置上的表头。 if (pred == null) setEntryAt(tab, index, next); else //链表操作,移除当前的节点 pred.setNext(next); ++modCount; --count; //返回的移除的value值 oldValue = v; } break; } pred = e; e = next; } } finally { unlock(); } return oldValue; } |
Put、get、remove的并发情况
put 操作的使用了 CAS 来初始化 Segment 中的数组。添加节点到链表的操作是插入到表头的,所以,如果这个时候 get 操作在链表遍历的过程已经到了中间,是不会影响的。当然,另一个并发问题就是 get 操作在 put 之后,需要保证刚刚插入表头的节点被读取,这个依赖于 setEntryAt 方法中使用的 UNSAFE.putOrderedObject。
扩容是新创建了数组,然后进行迁移数据,最后面将 newTable 设置给属性 table。所以,如果 get 操作此时也在进行,那么也没关系,如果 get 先行,那么就是在旧的 table 上做查询操作;而 put 先行,那么 put 操作的可见性保证就是 table 使用了 volatile 关键字。
get 操作需要遍历链表,但是 remove 操作会"破坏"链表。
如果 remove 破坏的节点 get 操作已经过去了,那么这里不存在任何问题。
如果 remove 先破坏了一个节点,分两种情况考虑。
- 如果此节点是头结点,那么需要将头结点的 next 设置为数组该位置的元素,table 虽然使用了 volatile 修饰,但是 volatile 并不能提供数组内部操作的可见性保证,所以使用了 UNSAFE.putOrderedObject来操作数组。
- 如果要删除的节点不是头结点,它会将要删除节点的后继节点接到前驱节点中,这里的并发保证就是 next 属性是 volatile 的。
参考文章:https://javadoop.com/post/hashmap
https://my.oschina.net/hosee/blog/618953
http://www.importnew.com/22007.html