原理上来说,ConcurrentHashMap 采纳了分段锁技术,其中 Segment 继承于 ReentrantLock。
不会像 HashTable 那样不论是 put 还是 get 操作都须要做同步解决,实践上 ConcurrentHashMap 反对 CurrencyLevel (Segment 数组数量)的线程并发。
每当一个线程占用锁拜访一个 Segment 时,不会影响到其余的 Segment。
就是说如果容量大小是16他的并发度就是16,能够同时容许16个线程操作16个Segment而且还是线程平安的。
public V put(K key, V value) {
Segment<K,V> s;if (value == null) throw new NullPointerException();//这就是为啥他不能够put null值的起因int hash = hash(key);int j = (hash >>> segmentShift) & segmentMask;if ((s = (Segment<K,V>)UNSAFE.getObject (segments, (j << SSHIFT) + SBASE)) == null) s = ensureSegment(j);return s.put(key, hash, value, false);
}
他先定位到Segment,而后再进行put操作。
咱们看看他的put源代码,你就晓得他是怎么做到线程平安的了,要害句子我正文了。
final V put(K key, int hash, V value, boolean onlyIfAbsent) { // 将以后 Segment 中的 table 通过 key 的 hashcode 定位到 HashEntry HashEntry<K,V> node = tryLock() ? null : scanAndLockForPut(key, hash, value); V oldValue; try { 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;
// 遍历该 HashEntry,如果不为空则判断传入的 key 和以后遍历的 key 是否相等,相等则笼罩旧的 value。
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 { // 不为空则须要新建一个 HashEntry 并退出到 Segment 中,同时会先判断是否须要扩容。 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 setEntryAt(tab, index, node); ++modCount; count = c; oldValue = null; break; } } } finally { //开释锁 unlock(); } return oldValue; }
首先第一步的时候会尝试获取锁,如果获取失败必定就有其余线程存在竞争,则利用 scanAndLockForPut() 自旋获取锁。
尝试自旋获取锁。
如果重试的次数达到了 MAX_SCAN_RETRIES 则改为阻塞锁获取,保障能获取胜利。
get 逻辑比较简单,只须要将 Key 通过 Hash 之后定位到具体的 Segment ,再通过一次 Hash 定位到具体的元素上。
因为 HashEntry 中的 value 属性是用 volatile 关键词润饰的,保障了内存可见性,所以每次获取时都是最新值。
ConcurrentHashMap 的 get 办法是十分高效的,因为整个过程都不须要加锁。
1.7尽管能够反对每个Segment并发拜访,然而还是存在一些问题?
因为基本上还是数组加链表的形式,咱们去查问的时候,还得遍历链表,会导致效率很低,这个跟jdk1.7的HashMap是存在的一样问题,所以他在jdk1.8齐全优化了。
jdk1.8他的数据结构是怎么样子的呢?
其中摈弃了原有的 Segment 分段锁,而采纳了 CAS + synchronized 来保障并发安全性。
跟HashMap很像,也把之前的HashEntry改成了Node,然而作用不变,把值和next采纳了volatile去润饰,保障了可见性,并且也引入了红黑树,在链表大于肯定值的时候会转换(默认是8)。