转自:http://blog.csdn.net/ykdsg/article/details/6257449
很早就想研究ConcurrentHashMap ,不过一直拖拉,我也是个很容易被新奇好玩的技术吸引的人,这个呢有好也有坏。废话不多说上干货。
ConcurrentHashMap 最重要的就是引入了Segment 的概念,他在自己内部定义了这个Class来管理数据,这个Segment 类似于HashMap的定义,因为ConcurrentHashMap 会将对应的读写操作交给Segment 。Segment 在ConcurrentHashMap 内部维护一个Segment 数组(默认16个),先将key值的hash值定位到Segment 数组中,取得对应的Segment 之后再利用Segment 的相应方法(对应HashMap里的方法)来读写数据,写操作会加锁。这样做的好处就是,如果有多个线程对ConcurrentHashMap 操作的时候,理想情况下如果key hash到的Segment 是不同的,那么写操作是可以并发执行的。当然像size(),isEmpty(),containsValue(Object value)这些操作涉及到跨Segment 的操作,需要一定的机制来保证,极端情况下需要锁定所有Segment 来做统计。
Segment 主要继承ReentrantLock,在Java5中,ReentrantLock的性能要远远高于内部锁。在Java6中,由于管理内部锁的算法采用了类似于 ReentrantLock使用的算法,因此内部锁和ReentrantLock之间的性能差别不大。
ReentrantLock的构造函数提供了两种公平性选择:创建非公平锁(默认)或者公平锁。在公平锁中,如果锁已被其它线程占有,那么请求线程会加入到等待队列中,并按顺序获得锁;在非公平锁中,当请求锁的时候,如果锁的状态是可用,那么请求线程可以直接获得锁,而不管等待队列中是否有线程已经在等待该锁。公平锁的代价是更多的挂起和重新开始线程的性能开销。在多数情况下,非公平锁的性能高于公平锁。Java内部锁也没有提供确定的公平性保证, Java语言规范也没有要求JVM公平地实现内部锁,因此ReentrantLock并没有减少锁的公平性。在中等或者更高负荷下,ReentrantLock有更好的性能,并且拥有可轮询和可定时的请求锁等高级功能。具体请参考:http://www.blogjava.net/killme2008/archive/2007/09/14/145195.html
有时间的话也研究下ReentrantLock的源码。
Segment源码分析:
- static final class Segment<K, V> extends ReentrantLock implements
- Serializable
- /**
- * Segment中元素个数
- */
- transient volatile int count;
- /**
- * 更新而导致表数目改变的次数。这是用来确保大量读方法看到一致的快照。
- * 如果在各个segments循环计算size或者检索containsValue时modCounts改变,那么将得到不一致的结果。
- * 通常这个必须重来。
- */
- transient int modCount;
- /**
- * table将会重新hash如果size超过threshold。这个值=(容量 * {@link #loadFactor})
- */
- transient int threshold;
- /**
- * 存放元素的table数组
- */
- transient volatile HashEntry<K, V>[] table;
- /**
- * 装载因子. 尽管这个值对所有的Segment是相同,但是它需要被复制,以避免外部对象对他的引用。
- *
- * @serial
- */
- final float loadFactor;
- Segment(int initialCapacity, float lf) {
- loadFactor = lf;
- setTable(HashEntry.<K, V> newArray(initialCapacity));
- }
可以看到真正存放数据的是HashEntry<K, V>[] table ,这里用到了自定义的class HashEntry ,因为ConcurrentHashMap完全允许多个读操作并发进行,读操作并不需要加锁。如果使用传统的技术,如HashMap中的实现,如果允许可以在hash链的中间删除元素,读操作不加锁将得到不一致的数据。ConcurrentHashMap实现技术是保证HashEntry几乎是不可变的。HashEntry代表每个hash链中的一个节点,其结构如下所示:
- static final class HashEntry<K, V> {
- final K key;
- final int hash;
- volatile V value;
- final HashEntry<K, V> next;
- HashEntry(K key, int hash, HashEntry<K, V> next, V value) {
- this.key = key;
- this.hash = hash;
- this.next = next;
- this.value = value;
- }
- @SuppressWarnings("unchecked")
- static final <K, V> HashEntry<K, V>[] newArray(int i) {
- return new HashEntry[i];
- }
- }
可以看到除了value不是final的,其它值都是final的,这意味着不能从hash链的中间或尾部删除节点,因为这需要修改next引用值,所有的节点的修改只能从头部开始。对于put操作,可以一律添加到Hash链的头部。但是对于remove操作,可能需要从中间删除一个节点,这就需要将要删除节点的前面所有节点整个复制一遍,最后一个节点指向要删除结点的下一个结点。这在讲解删除操作时还会详述。为了确保读操作能够看到最新的值,将value设置成volatile,这避免了加锁。
- /**
- * 根据hash值返回适当取得的第一个entry Returns properly casted first entry of bin for
- * given hash.
- */
- HashEntry<K, V> getFirst(int hash) {
- HashEntry<K, V>[] tab = table;
- return tab[hash & (tab.length - 1)];
- }
- /**
- * 在带锁的情况下读取一个entry的值,如果该值曾经为null就会被调用。 只有在编译器碰巧切换这个HashEntry的初始化到table
- * assignment时,这个时候HashEntry没有初始化好,但是已经可被其他线程可见。 这个在内存模型下是合法 的。
- *
- * 感觉主要是与JIT相关,这句话的意思是说当你tab[index] = new HashEntry()的时候,可能发生partial
- * construct,
- * 也就是说其他的Thread可以看到这个reference,但是对象却没有构造完全。这是由JLS来决定的,因为JLS没有说
- * ,是否应该完成之后再赋值。 而现代的编译器(JIT)可以使用out of
- * ordering来调度指令,可能就把reference的赋值放在构造函数前边。
- * 这篇文章给除了很好的分析:http://www.ibm.com
- * /developerworks/java/library/j-dcl.html。 所以在这里为了避免partial
- * construct,当value是null的时候,也就是说可能发生partial construct的时候,需要lock()来保证,
- * 如果发生了partial construct,那么必然在构造HashEntry,这个HashEntry的构造是在lock()之下的,
- * 所以这里的lock就可以等待对象构造的完全。 Reads value field of an entry under lock.
- * Called if value field ever appears to be null. This is possible only
- * if a compiler happens to reorder a HashEntry initialization with its
- * table assignment, which is legal under memory model but is not known
- * to ever occur.
- */
- V readValueUnderLock(HashEntry<K, V> e) {
- lock();
- try {
- return e.value;
- } finally {
- unlock();
- }
- }
- /**
- * 为什么其他的final的field在partial construct的情况下,不会发生null value之类的错误? 文献中这么说:
- * A new guarantee of initialization safety should be provided. If an
- * object is properly constructed (which means that references to it do
- * not escape during construction), then all threads which see a
- * reference to that object will also see the values for its final
- * fields that were set in the constructor, without the need for
- * synchronization. 一个对像的值域在构造函数没有逃逸,那么将不会出现部分初始化的现象
- */
- V get(Object key, int hash) {
- if (count != 0) { // read-volatile
- HashEntry<K, V> e = getFirst(hash);
- while (e != null) {
- if (e.hash == hash && key.equals(e.key)) {
- V v = e.value;
- if (v != null)
- return v;
- return readValueUnderLock(e); // 这里需要再确认是否有发生部分构造而导致的null
- }
- e = e.next;
- }
- }
- return null;
- }
这里读取采用了加锁的方式,注释里也提到了原因。关于逃逸分析可以参考http://blog.csdn.net/ykdsg/archive/2011/03/17/6255618.aspx
其他的操作像:containsKey, containsValue,clear基本上也是采用get这样的循环方式。这里就不具体分析了。接下来看下put方法,因为涉及到扩容操作。
- V put(K key, int hash, V value, boolean onlyIfAbsent) {
- lock();
- try {
- int c = count;
- if (c++ > threshold) // 需要扩容
- rehash();
- HashEntry<K, V>[] tab = table;
- int index = hash & (tab.length - 1);
- HashEntry<K, V> first = tab[index];
- HashEntry<K, V> e = first;
- while (e != null && (e.hash != hash || !e.key.equals(key)))
- e = e.next;
- V oldValue;
- if (e != null) {// 如果e不等于null,说明键值重复
- oldValue = e.value;
- if (!onlyIfAbsent) {
- e.value = value;
- }
- } else {// 如果e为null说明需要增加一个
- oldValue = null;
- ++modCount;
- tab[index] = new HashEntry<K, V>(key, hash, first, value);
- count = c; // write-volatile
- }
- return oldValue;
- } finally {
- unlock();
- }
- }
其中关键的地方是rehash()方法和++modCount;这个操作,后面会讲到为什么要维护modCount(在改变元素个数的情况下++)。
- void rehash() {
- HashEntry<K, V>[] oldTable = table;
- int oldCapacity = oldTable.length;
- if (oldCapacity >= MAXIMUM_CAPACITY)
- return;
- // 扩容一倍
- HashEntry<K, V>[] newTable = HashEntry.newArray(oldCapacity << 1);
- threshold = (int) (newTable.length * loadFactor);
- int sizeMask = newTable.length - 1;
- for (int i = 0; i < oldCapacity; i++) {
- // 必须保证现有的map可以继续读,所以我们不能清空每个槽
- HashEntry<K, V> e = oldTable[i];
- if (e != null) {
- HashEntry<K, V> next = e.next;
- int idx = e.hash & sizeMask;
- // 如果只是单个节点
- if (next == null)
- newTable[idx] = e;
- else {
- HashEntry<K, V> lastRun = e;
- int lastIdx = idx;
- // 先把链表末端的节点移到新的槽位,然后把其余的节点克隆到新的槽位 ,rehash之后idx相同的元素
- // 这些元素相当于一个串一起移到新的槽位 ,例如:原来oldTable[i] 下有10个HashEntry,
- // 按照下面的算法,假设从第5个开始,这些HashEntry在newTable中计算的hash值都一样,那么只要移动第5位即可。
- for (HashEntry<K, V> last = next; last != null; last = last.next) {
- // 在新table中的hash值
- int k = last.hash & sizeMask;
- if (k != lastIdx) {
- lastIdx = k;
- lastRun = last;
- }
- }
- // 这里这样处理不知道是否有算法的保证,因为怎么确定newTable[lastIdx]在循环中一直没有值?
- newTable[lastIdx] = lastRun;
- // 复制剩下的所有HashEntry
- for (HashEntry<K, V> p = e; p != lastRun; p = p.next) {
- int k = p.hash & sizeMask;
- HashEntry<K, V> n = newTable[k];
- newTable[k] = new HashEntry<K, V>(p.key, p.hash, n,
- p.value);
- }
- }
- }
- }
- table = newTable;
- }
可以看到rehash()方法就是把table数组扩容一倍,再把原来的引用计算出在新数组的位置e.hash & sizeMask ,然后放过去,原来旧的table数组就交给垃圾回收。
- /**
- * Remove; match on key only if value null, else match both.
- */
- V remove(Object key, int hash, Object value) {
- lock();
- try {
- int c = count - 1;
- HashEntry<K, V>[] tab = table;
- int index = hash & (tab.length - 1);
- HashEntry<K, V> first = tab[index];
- HashEntry<K, V> e = first;
- while (e != null && (e.hash != hash || !key.equals(e.key)))
- e = e.next;
- V oldValue = null;
- if (e != null) {
- V v = e.value;
- if (value == null || value.equals(v)) {
- oldValue = v;
- // 被删除节点的后面列表可以保留,但是前面的需要复制
- ++modCount;
- HashEntry<K, V> newFirst = e.next;
- // 如果原来是1,2,3,4,5,6,7;要移除第4位,那么新的序列变成3,2,1,5,6,7
- for (HashEntry<K, V> p = first; p != e; p = p.next)
- newFirst = new HashEntry<K, V>(p.key, p.hash,
- newFirst, p.value);
- tab[index] = newFirst;
- count = c; // write-volatile
- }
- }
- return oldValue;
- } finally {
- unlock();
- }
- }
remove方法其实就是需要重新建立next链,所以需要复制。
基本上Segment特殊一点的方法就上面几个了。
接下来看ConcurrentHashMap是怎么使用的。因为ConcurrentHashMap做了二次hash,一些方法像entrySet()方法就要重写了。
- @Override
- public Set<java.util.Map.Entry<K, V>> entrySet() {
- Set<Map.Entry<K, V>> es = entrySet;
- return (es != null) ? es : (entrySet = new EntrySet());
- }
- final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
- public Iterator<Map.Entry<K,V>> iterator() {
- return new EntryIterator();
- }
- public boolean contains(Object o) {
- if (!(o instanceof Map.Entry))
- return false;
- Map.Entry<?,?> e = (Map.Entry<?,?>)o;
- V v = ConcurrentHashMap.this.get(e.getKey());
- return v != null && v.equals(e.getValue());
- }
- public boolean remove(Object o) {
- if (!(o instanceof Map.Entry))
- return false;
- Map.Entry<?,?> e = (Map.Entry<?,?>)o;
- return ConcurrentHashMap.this.remove(e.getKey(), e.getValue());
- }
- public int size() {
- return ConcurrentHashMap.this.size();
- }
- public void clear() {
- ConcurrentHashMap.this.clear();
- }
- }
- final class EntryIterator extends HashIterator implements
- Iterator<Entry<K, V>> {
- @Override
- public Map.Entry<K, V> next() {
- HashEntry<K, V> e = super.nextEntry();
- return new WriteThroughEntry(e.key, e.value);
- }
- }
- final class WriteThroughEntry extends AbstractMap.SimpleEntry<K, V> {
- WriteThroughEntry(K k, V v) {
- super(k, v);
- }
- /**
- * Set our entry's value and write through to the map. The value to
- * return is somewhat arbitrary here. Since a WriteThroughEntry does not
- * necessarily track asynchronous changes, the most recent "previous"
- * value could be different from what we return (or could even have been
- * removed in which case the put will re-establish). We do not and
- * cannot guarantee more.
- */
- @Override
- public V setValue(V value) {
- if (value == null)
- throw new NullPointerException();
- V v = super.setValue(value);
- MyConcurrentHashMap.this.put(getKey(), value);
- return v;
- }
- }
- /* ---------------- Iterator Support -------------- */
- abstract class HashIterator {
- int nextSegmentIndex;
- int nextTableIndex;
- HashEntry<K, V>[] currentTable;
- HashEntry<K, V> nextEntry;
- HashEntry<K, V> lastReturned;
- HashIterator() {
- nextSegmentIndex = segments.length - 1;
- nextTableIndex = -1;
- advance();
- }
- public boolean hasMoreElements() {
- return hasNext();
- }
- //循环segments 返回nextEntry
- final void advance() {
- if (nextEntry != null && (nextEntry = nextEntry.next) != null)
- return;
- while (nextTableIndex >= 0) {
- if ((nextEntry = currentTable[nextTableIndex--]) != null)
- return;
- }
- while (nextSegmentIndex >= 0) {
- Segment<K, V> seg = segments[nextSegmentIndex--];
- if (seg.count != 0) {
- currentTable = seg.table;
- for (int j = currentTable.length - 1; j >= 0; --j) {
- if ((nextEntry = currentTable[j]) != null) {
- nextTableIndex = j - 1;
- return;
- }
- }
- }
- }
- }
- public boolean hasNext() {
- return nextEntry != null;
- }
- HashEntry<K, V> nextEntry() {
- if (nextEntry == null)
- throw new NoSuchElementException();
- lastReturned = nextEntry;
- advance();
- return lastReturned;
- }
- public void remove() {
- if (lastReturned == null)
- throw new IllegalStateException();
- ConcurrentHashMap.this.remove(lastReturned.key);
- lastReturned = null;
- }
- }
其中HashIterator 的方法advance会循环Segment和其中的table数据,并分别记录下标,下次会在原来的下标继续循环。
构造函数
- 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;
- //Segment数组长度
- int ssize = 1;
- while (ssize < concurrencyLevel) {
- ++sshift;
- ssize <<= 1;
- }
- segmentShift = 32 - sshift;
- segmentMask = ssize - 1;
- this.segments = Segment.newArray(ssize);
- if (initialCapacity > MAXIMUM_CAPACITY)
- initialCapacity = MAXIMUM_CAPACITY;
- int c = initialCapacity / ssize;
- if (c * ssize < initialCapacity)
- ++c;
- //每个Segment的初始容量
- int cap = 1;
- while (cap < c)
- cap <<= 1;
- for (int i = 0; i < this.segments.length; ++i)
- this.segments[i] = new Segment<K, V>(cap, loadFactor);
- }
就是初始化一些基本的值,初始化好Segment数组,接下来的几个构造函数都是调用这个,只是提供了一些初始值,默认的Segment数组长度是16,装载因子是0.75f,初始容量是16。因为构造好Map之后,Segment数组是不会扩容的,如果要放的数据比较多的话,传入比较大的concurrencyLevel 可以支持比较好的并发性。
现在看下get方法的实现
- @Override
- public V get(Object key) {
- int hash = hash(key.hashCode());
- return segmentFor(hash).get(key, hash);
- }
- /**
- * Returns the segment that should be used for key with given hash
- *
- * @param hash
- * the hash code for the key
- * @return the segment
- */
- final Segment<K, V> segmentFor(int hash) {
- return segments[(hash >>> segmentShift) & segmentMask];
- }
很简单,就是先根据hash找到对应的segment ,然后再调用segment 的get方法。其他的像put ,containsKey,replace 等这些值涉及到单个segment 的操作都是类似的。
下面看下涉及到跨段操作的几个方法
- public boolean isEmpty() {
- final Segment<K, V>[] segments = this.segments;
- /*
- * We keep track of per-segment modCounts to avoid ABA problems in which
- * an element in one segment was added and in another removed during
- * traversal, in which case the table was never actually empty at any
- * point. Note the similar use of modCounts in the size() and
- * containsValue() methods, which are the only other methods also
- * susceptible to ABA problems.
- */
- /**
- *保存每个segment
- * 的modCounts值,避免ABA问题在一个segment中一个元素被增加和一个元素在遍历的时候被删除。在这种情况下
- * ,table数组没有真正的 empty 在任何一个点。注意相似的在
- * size()方法和containsValue()方法,基本上只有这些方法比较容易受ABA问题的影响。
- */
- int[] mc = new int[segments.length];
- int mcsum = 0;
- for (int i = 0; i < segments.length; ++i) {
- if (segments[i].count != 0)
- return false;
- else
- mcsum += mc[i] = segments[i].modCount;
- }
- // If mcsum happens to be zero, then we know we got a snapshot
- // before any modifications at all were made. This is
- // probably common enough to bother tracking.
- /*
- * 如果mcsm==0,那么我们刚好得到一个在任何修改操作之前的快照。如果不是那可能就是常见的打扰跟踪。
- * 但貌似也不是百分百的,如果在if的时候mcsum是0的
- * ,但是这个时候线程被切开了,其他的线程增加或者删除了元素,这个时候再切回来,貌似就不准了
- * 不过就像上面说的这个就是个快照,在多线程的情况下不会那么精准,基本上的业务都应该可以忍受这样的进度,如果非要准确值,估计只有加锁了。
- */
- if (mcsum != 0) {
- // 如果mcsum不为0, 那么说明在统计的过程中有变化,需要重新统计
- for (int i = 0; i < segments.length; ++i) {
- //modCount有变化就说明不为empty
- if (segments[i].count != 0 || mc[i] != segments[i].modCount)
- return false;
- }
- }
- return true;
- }
- @Override
- public int size() {
- final Segment<K, V>[] segments = this.segments;
- long sum = 0;
- long check = 0;
- int[] mc = new int[segments.length];
- // Try a few times to get accurate count. On failure due to
- // continuous async changes in table, resort to locking.
- for (int k = 0; k < RETRIES_BEFORE_LOCK; ++k) {
- check = 0;
- sum = 0;
- int mcsum = 0;
- for (int i = 0; i < segments.length; ++i) {
- sum += segments[i].count;
- mcsum += mc[i] = segments[i].modCount;
- }
- if (mcsum != 0) {
- for (int i = 0; i < segments.length; ++i) {
- check += segments[i].count;
- if (mc[i] != segments[i].modCount) {
- // 如果这个时候segments有过数据的改变,那么强制从新统计
- check = -1; // force retry
- break;
- }
- }
- }
- if (check == sum)
- break;
- }
- // 加锁统计
- if (check != sum) { // Resort to locking all segments
- sum = 0;
- for (int i = 0; i < segments.length; ++i)
- segments[i].lock();
- for (int i = 0; i < segments.length; ++i)
- sum += segments[i].count;
- for (int i = 0; i < segments.length; ++i)
- segments[i].unlock();
- }
- if (sum > Integer.MAX_VALUE)
- return Integer.MAX_VALUE;
- else
- return (int) sum;
- }
可以看到跨Segment的操作中,先是是不锁表的,但是在多线程的情况下,就会造成数据的不一致,这里就用到了Segment中的modCount来做比较,如果modCount有变化就说明被其他线程污染了,就需要重新做统计,这个时候也是不带锁的。但是这样的循环不可能无限进行下去,所以做了限制,在不带锁的情况下允许进行2次尝试,如果还是受到其他线程的污染,那就要加锁统计了。注意要顺序的加锁再顺序的解锁,不然可能会出现死锁。containsValue的是实现与size相似。
本文基于java6 的源码分析,对一些英文的翻译不一定很到位,一些理解上也存在偏颇,欢迎大家指正。