深度剖析 JDK7 ConcurrentHashMap 中的知识点
来源:互联网 发布:指南针软件 股票 编辑:程序博客网 时间:2024/05/21 13:22
- Lock Stripping
- Unsafe
- 自旋
- Sequential Consistency
- Trade Offs
- 读与写
- 一致性与效率
- 参考资料
Lock Stripping
在 Java 中,普通的 HashMap 是非线程安全的,HashTable 是线程安全的 Map。从下面的代码可以看出来,它的线程安全是通过在 get 和 put 方法上加 synchronized 实现的,锁的粒度是整个对象,两个线程都不能同时 get,性能很差。
public class Hashtable<K,V> extends Dictionary<K,V> implements Map<K,V>, Cloneable, java.io.Serializable { public synchronized V get(Object key) { // 省略具体内容,只看方法定义 } public synchronized V put(K key, V value) { // 省略具体内容,只看方法定义 }}
为了改善并发操作的性能,Java 提供了 ConcurrentHashMap,它使用了锁分离技术,将原先 HashTable 中对象级的大锁,换成了多个小粒度的 Segment 锁,每一个 Segment 都相当于一个 HashTable。
static final class Segment<K,V> extends ReentrantLock implements Serializable { transient volatile HashEntry<K,V>[] table; }
Unsafe
Java 不能直接访问操作系统底层,而是通过本地方法来访问,Unsafe 类提供了一些硬件级别的原子操作。通常,使用 Unsafe 是为了提升性能。
private static final sun.misc.Unsafe UNSAFE; private static final long SBASE; private static final int SSHIFT; static { int ss, ts; try { UNSAFE = sun.misc.Unsafe.getUnsafe(); Class sc = Segment[].class; SBASE = UNSAFE.arrayBaseOffset(sc); ss = UNSAFE.arrayIndexScale(sc); } catch (Exception e) { throw new Error(e); } SSHIFT = 31 - Integer.numberOfLeadingZeros(ss); } static final <K,V> Segment<K,V> segmentAt(Segment<K,V>[] ss, int j) { long u = (j << SSHIFT) + SBASE; return ss == null ? null : (Segment<K,V>) UNSAFE.getObjectVolatile(ss, u); }
在 ConcurrentHashMap 中,segmentAt 方法用于获取 Segment 数组中第 j 个元素的引用,其中的一些常量用到了 Unsafe 的 arrayBaseOffset、arrayIndexScale、getObjectVolatile 方法,其功能如下表所示:
Unsafe 的特性大致可以分为以下几类:
1. 对变量和数组内容的原子访问,自定义内存屏障
2. 对序列化的支持
3. 自定义内存管理/高效的内存布局
4. 与原生代码和其他JVM进行互操作
5. 对高级锁的支持
更多 Unsafe 方法的功能请看:Java 中的 Unsafe 类详解
自旋
重量级的锁往往引起线程切换,而线程切换非常耗时。在竞争不激烈,锁占用时间较短(比线程切换耗时短)的环境里,通过 CPU 轮询来获取锁,往往效率更高。
在 Segment 的 remove 方法中,修改数据时需要获取锁,这时候并没有直接调用会阻塞的 lock 方法,而是调用了 tryLock,获取锁失败时进入了 scanAndLock 方法。scanAndLock 方法先是通过自旋来尝试获得锁,当自旋次数超过阈值 MAX_SCAN_RETRIES 时,调用 lock 方法来获得锁。
final V remove(Object key, int hash, Object value) { if (!tryLock()) scanAndLock(key, hash); // 省略其他 } private void scanAndLock(Object key, int hash) { // similar to but simpler than scanAndLockForPut HashEntry<K,V> first = entryForHash(this, hash); HashEntry<K,V> e = first; int retries = -1; while (!tryLock()) { HashEntry<K,V> f; if (retries < 0) { if (e == null || key.equals(e.key)) retries = 0; else e = e.next; } else if (++retries > MAX_SCAN_RETRIES) { lock(); break; } else if ((retries & 1) == 0 && (f = entryForHash(this, hash)) != first) { e = first = f; retries = -1; } } }
Sequential Consistency
在 scanAndLock 的文档说明里,提到了 “we must lock even if the key is not found, to ensure sequential consistency of updates”,即使 key 不存在,也要加锁,保证更新的顺序一致性。
/** * Scans for a node containing the given key while trying to * acquire lock for a remove or replace operation. Upon * return, guarantees that lock is held. Note that we must * lock even if the key is not found, to ensure sequential * consistency of updates. */ private void scanAndLock(Object key, int hash) {
Sequential Consistency 即顺序一致性,是一种内存一致性模型,它的定义是:
(并发程序在多处理器上的)任何一次执行结果都相同,就像所有处理器的操作按照某个顺序执行,各个微处理器的操作按照其程序指定的顺序进行。换句话说,所有的处理器以相同的顺序看到所有的修改。读操作未必能及时得到此前其他处理器对同一数据的写更新。但是各处理器读到的该数据的不同值的顺序是一致的。
现在的 CPU 和编译器会对代码做各种各样对优化,有时候为了改进性能而把代码执行顺序打乱,这样可能会导致错误对程序执行结果。为了保证顺序一致性,需要使用 Memory Barrier,一般同步原语(例如锁)会隐式地调用 Memory Barrier 指令。
Trade Offs
Trade Offs 即权衡,鱼与熊掌不可兼得。在一些系统性能优化中,经常使用空间换时间,或时间换空间。在 ConcurrentHashMap 中,也有一些权衡。
读与写
请看如下 Segment 类的 get 方法,其中并没有使用锁,显然效率会很高。那么 ConcurrentHashMap 是如何保证在 put 和 rehash 的过程中,读仍然能正常工作呢?
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; }
请看 put 方法的第 32 行,每一次 put 都把新的元素放到 HashEntry 链表的头结点,这样就不会破坏链表的结构,读操作就不会失败。
final V put(K key, int hash, V value, boolean onlyIfAbsent) { 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; 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<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; }
对于 rehash,请看下面的代码,在复制数据时,并没有去改变原先的 HashEntry 的 next 字段。在新的链表中,是重新 new 了 HashEntry 节点来装载数据。这样一来,旧的链表结构没有被破坏,读操作也就不会失败了。
/** * Doubles size of table and repacks entries, also adding the * given node to new table */ @SuppressWarnings("unchecked") private void rehash(HashEntry<K,V> node) { 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 (HashEntry<K,V> last = next; last != null; last = last.next) { int k = last.hash & sizeMask; if (k != lastIdx) { lastIdx = k; lastRun = last; } } newTable[lastIdx] = lastRun; // 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; }
如果 rehash 时,所有 HashEntry 都 new 一个,会生成大量新对象,耗费过多内存。所以上面代码第 24~32 行,做了一些优化,重用了部分旧链表的节点,即代码注释里所写的 “Reuse consecutive sequence at same slot”。重用的关键是,找到旧链表中的一个关键节点 lastRun,该关键节点确保:从 lastRun 到旧链表的尾节点的所有数据,在新链表中的哈希位置一样。
综上所述,ConcurrentHashMap 通过巧妙的设计 put 和 rehash,多消耗了一些内存,换来了不需要加锁的高效的读操作。
一致性与效率
ConcurrentHashMap 的 clear 方法如下所示,其并没有一个全局的锁,这会导致清理完一个 Segment 后,正在清理下一个 Segment 时,前面的 Segment 有可能已经被其他线程放入了数据,这样就会导致 clear 方法结束的时候,ConcurrentHashMap 中可能存在数据。
public void clear() { final Segment<K,V>[] segments = this.segments; for (int j = 0; j < segments.length; ++j) { Segment<K,V> s = segmentAt(segments, j); if (s != null) s.clear(); } }
也就是说,相对于 HashTable 的强一致,ConcurrentHashMap 是弱一致的。那么弱一致性带来了什么好处呢?因为没有全局锁,从而可以提供更好的读写并发能力。
参考资料
- Java 中的 Unsafe 类详解
- sun.misc.Unsafe 的后启示录
- 顺序一致性与缓存一致性
- 维基百科:内存一致性模型
- 深度剖析 JDK7 ConcurrentHashMap 中的知识点
- 深度剖析ConcurrentHashMap
- 深度剖析ConcurrentHashMap
- 2、深度剖析ConcurrentHashMap
- 深度剖析ConcurrentHashMap
- 深度剖析ConcurrentHashMap
- 深度剖析ConcurrentHashMap
- 深度剖析ConcurrentHashMap
- 深度剖析ConcurrentHashMap
- 深度剖析ConcurrentHashMap
- 深度剖析ConcurrentHashMap
- 深度剖析ConcurrentHashMap
- 深度剖析ConcurrentHashMap
- JDK7中的ConcurrentHashMap实现原理
- 并发容器ConcurrentHashMap深度剖析
- [Java并发包学习]深度剖析ConcurrentHashMap
- JAVA源码分析-深度剖析ConcurrentHashMap
- ConcurrentHashmap源码分析(jdk7)
- 常见的css3动画样式
- MySQL 常用SQL 技巧
- 虚拟机安装Ubuntu黑屏解决方法(分区问题)
- 07:矩阵归零消减序列和(1.8编程基础之多维数组)
- HDU 6184 Counting Stars(无向图三元环计数)
- 深度剖析 JDK7 ConcurrentHashMap 中的知识点
- senet
- Lowest Common Ancestor III
- Python之爬虫工具包
- 浅谈promise(起码看完让你会用它的then、catch、resolve、reject、race等方法)
- 百练OJ:2965:玛雅历
- Server Tomcat v7.0 Server at localhost failed to start.解决方法
- CCF 201403-1 相反数
- C++学习笔记07