ConcurrentHashMap是Java并发包中提供的一个线程安全且高效的HashMap实现(若对HashMap的实现原理还不甚了解,可参考 另外一篇文章HashMap实现原理及源码分析),ConcurrentHashMap在并发编程的场景中使用频率很是之高,本文就来分析下ConcurrentHashMap的实现原理,并对其实现原理进行分析(JDK1.7).html
众所周知,哈希表是中很是高效,复杂度为O(1)的数据结构,在Java开发中,咱们最多见到最频繁使用的就是HashMap和HashTable,可是在线程竞争激烈的并发场景中使用都不够合理。node
HashMap :先说HashMap,HashMap是线程不安全的,在并发环境下,可能会造成环状链表(扩容时可能形成,具体缘由自行百度google或查看源码分析),致使get操做时,cpu空转,因此,在并发环境中使用HashMap是很是危险的。算法
HashTable : HashTable和HashMap的实现原理几乎同样,差异无非是1.HashTable不容许key和value为null;2.HashTable是线程安全的。可是HashTable线程安全的策略实现代价却太大了,简单粗暴,get/put全部相关操做都是synchronized的,这至关于给整个哈希表加了一把大锁,多线程访问时候,只要有一个线程访问或操做该对象,那其余线程只能阻塞,至关于将全部的操做串行化,在竞争激烈的并发场景中性能就会很是差。编程
HashTable性能差主要是因为全部操做须要竞争同一把锁,而若是容器中有多把锁,每一把锁锁一段数据,这样在多线程访问时不一样段的数据时,就不会存在锁竞争了,这样即可以有效地提升并发效率。这就是ConcurrentHashMap所采用的"分段锁"思想。数组
ConcurrentHashMap采用了很是精妙的"分段锁"策略,ConcurrentHashMap的主干是个Segment数组。缓存
final Segment[] segments;安全
Segment继承了ReentrantLock,因此它就是一种可重入锁(ReentrantLock)。在ConcurrentHashMap,一个Segment就是一个子哈希表,Segment里维护了一个HashEntry数组,并发环境下,对于不一样Segment的数据进行操做是不用考虑锁竞争的。(就按默认的ConcurrentLeve为16来说,理论上就容许16个线程并发执行,有木有很酷)数据结构
因此,对于同一个Segment的操做才需考虑线程同步,不一样的Segment则无需考虑。多线程
Segment相似于HashMap,一个Segment维护着一个HashEntry数组并发
transient volatile HashEntry[] table;
HashEntry是目前咱们提到的最小的逻辑处理单元了。一个ConcurrentHashMap维护一个Segment数组,一个Segment维护一个HashEntry数组。
static final class HashEntry { final int hash; final K key; volatile V value; volatile HashEntry next; //其余省略 }
咱们说Segment相似哈希表,那么一些属性就跟咱们以前提到的HashMap差不离,好比负载因子loadFactor,好比阈值threshold等等,看下Segment的构造方法
Segment(float lf, int threshold, HashEntry[] tab) { this.loadFactor = lf;//负载因子 this.threshold = threshold;//阈值 this.table = tab;//主干数组即HashEntry数组 }
咱们来看下ConcurrentHashMap的构造方法
1 public ConcurrentHashMap(int initialCapacity, 2 float loadFactor, int concurrencyLevel) { 3 if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0) 4 throw new IllegalArgumentException(); 5 //MAXSEGMENTS 为1<<16=65536,也就是最大并发数为65536 6 if (concurrencyLevel > MAXSEGMENTS) 7 concurrencyLevel = MAXSEGMENTS; 8 //2的sshif次方等于ssize,例:ssize=16,sshift=4;ssize=32,sshif=5 9 int sshift = 0; 10 //ssize 为segments数组长度,根据concurrentLevel计算得出 11 int ssize = 1; 12 while (ssize < concurrencyLevel) { 13 ++sshift; 14 ssize <<= 1; 15 } 16 //segmentShift和segmentMask这两个变量在定位segment时会用到,后面会详细讲 17 this.segmentShift = 32 - sshift; 18 this.segmentMask = ssize - 1; 19 if (initialCapacity > MAXIMUMCAPACITY) 20 initialCapacity = MAXIMUMCAPACITY; 21 //计算cap的大小,即Segment中HashEntry的数组长度,cap也必定为2的n次方. 22 int c = initialCapacity / ssize; 23 if (c * ssize < initialCapacity) 24 ++c; 25 int cap = MIN_SEGMENT_TABLECAPACITY; 26 while (cap < c) 27 cap <<= 1; 28 //建立segments数组并初始化第一个Segment,其他的Segment延迟初始化 29 Segment s0 = 30 new Segment(loadFactor, (int)(cap * loadFactor), 31 (HashEntry[])new HashEntry[cap]); 32 Segment[] ss = (Segment[])new Segment[ssize]; 33 UNSAFE.putOrderedObject(ss, SBASE, s0); 34 this.segments = ss; 35 }
初始化方法有三个参数,若是用户不指定则会使用默认值,initialCapacity为16,loadFactor为0.75(负载因子,扩容时须要参考),concurrentLevel为16。
从上面的代码能够看出来,Segment数组的大小ssize是由concurrentLevel来决定的,可是却不必定等于concurrentLevel,ssize必定是大于或等于concurrentLevel的最小的2的次幂。好比:默认状况下concurrentLevel是16,则ssize为16;若concurrentLevel为14,ssize为16;若concurrentLevel为17,则ssize为32。为何Segment的数组大小必定是2的次幂?其实主要是便于经过按位与的散列算法来定位Segment的index。至于更详细的缘由,有兴趣的话能够参考个人另外一篇文章《HashMap实现原理及源码分析》,其中对于数组长度为何必定要是2的次幂有较为详细的分析。
接下来,咱们来看看put方法
public V put(K key, V value) { Segment s; //concurrentHashMap不容许key/value为空 if (value == null) throw new NullPointerException(); //hash函数对key的hashCode从新散列,避免差劲的不合理的hashcode,保证散列均匀 int hash = hash(key); //返回的hash值无符号右移segmentShift位与段掩码进行位运算,定位segment int j = (hash >>> segmentShift) & segmentMask; if ((s = (Segment)UNSAFE.getObject // nonvolatile; recheck (segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment s = ensureSegment(j); return s.put(key, hash, value, false); }
从源码看出,put的主要逻辑也就两步:1.定位segment并确保定位的Segment已初始化 2.调用Segment的put方法。
关于segmentShift和segmentMask
segmentShift和segmentMask这两个全局变量的主要做用是用来定位Segment,int j =(hash >>> segmentShift) & segmentMask。
segmentMask:段掩码,假如segments数组长度为16,则段掩码为16-1=15;segments长度为32,段掩码为32-1=31。这样获得的全部bit位都为1,能够更好地保证散列的均匀性
segmentShift:2的sshift次方等于ssize,segmentShift=32-sshift。若segments长度为16,segmentShift=32-4=28;若segments长度为32,segmentShift=32-5=27。而计算得出的hash值最大为32位,无符号右移segmentShift,则意味着只保留高几位(其他位是没用的),而后与段掩码segmentMask位运算来定位Segment。
get/put方法
get方法
public V get(Object key) { Segment s; HashEntry[] tab; int h = hash(key); long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE; //先定位Segment,再定位HashEntry if ((s = (Segment)UNSAFE.getObjectVolatile(segments, u)) != null && (tab = s.table) != null) { for (HashEntry e = (HashEntry) 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; }
get方法无需加锁,因为其中涉及到的共享变量都使用volatile修饰,volatile能够保证内存可见性,因此不会读取到过时数据。
来看下concurrentHashMap代理到Segment上的put方法,Segment中的put方法是要加锁的。只不过是锁粒度细了而已。
final V put(K key, int hash, V value, boolean onlyIfAbsent) { HashEntry node = tryLock() ? null : scanAndLockForPut(key, hash, value);//tryLock不成功时会遍历定位到的HashEnry位置的链表(遍历主要是为了使CPU缓存链表),若找不到,则建立HashEntry。tryLock必定次数后(MAX_SCAN_RETRIES变量决定),则lock。若遍历过程当中,因为其余线程的操做致使链表头结点变化,则须要从新遍历。 V oldValue; try { HashEntry[] tab = table; int index = (tab.length - 1) & hash;//定位HashEntry,能够看到,这个hash值在定位Segment时和在Segment中定位HashEntry都会用到,只不过定位Segment时只用到高几位。 HashEntry first = entryAt(tab, index); for (HashEntry e = first;;) { 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 { if (node != null) node.setNext(first); else node = new HashEntry(hash, key, value, first); int c = count + 1; //若c超出阈值threshold,须要扩容并rehash。扩容后的容量是当前容量的2倍。这样能够最大程度避免以前散列好的entry从新散列,具体在另外一篇文章中有详细分析,不赘述。扩容并rehash的这个过程是比较消耗资源的。 if (c > threshold && tab.length < MAXIMUM_CAPACITY) rehash(node); else setEntryAt(tab, index, node); ++modCount; count = c; oldValue = null; break; } } } finally { unlock(); } return oldValue; }
ConcurrentHashMap做为一种线程安全且高效的哈希表的解决方案,尤为其中的"分段锁"的方案,相比HashTable的全表锁在性能上的提高很是之大。本文对ConcurrentHashMap的实现原理进行了详细分析,并解读了部分源码,但愿能帮助到有须要的童鞋。