ConcurrentHashMap 源码解读

本文深入解析了ConcurrentHashMap的工作原理,包括其内部结构、线程安全机制、读写操作细节及其实现上的优化策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

ConcurrentHashMap 源码解读

 ConcurrentMap接口

 

package java.util.concurrent;

public interface ConcurrentMap<K, V> extends Map<K, V>

V putIfAbsent(K key, V value);

     方法等同于以下代码,但这个方法是原子的

     if(!map.containsKey(key))

         return map.put(key,value);

     else

         return map.get(key);

      方法的返回值是原来关联的value,  返回null可能是原来并不关联,或者关联的就是null,如果map允许的话

boolean remove(Object key, Object value);

      key 关联着指定value时才删除

      if((map.containsKey(key) && map.get(key).equals(value)){

         map.remove(key);

         return true;

      }else return false;

      但这个方法是原子的

boolean replace(K key, V oldValue, V newValue);

     相当于以下代码,但这个方法是原子的

     if ((map.containsKey(key) && map.get(key).equals(oldValue)) {

           map.put(key, newValue);

           return true;

       } else

         return false;

 

 

 V replace(K key, V value);

 

      如果key当前关联着某个值,则替换这个值,相当于以下代码,但此方法是原子的

      if ((map.containsKey(key)) {

          return map.put(key, value);

      } else

         return null;

 

 简介

https://www.ibm.com/developerworks/cn/java/java-lo-concurrenthashmap/

 

public class ConcurrentHashMap<K, V> extends AbstractMap<K, V>

implements ConcurrentMap<K, V>, Serializable

 

   支持完全并发读取,支持可调整的期待并发写操作。

所有的操作都是线程安全的,但读操作并不需要加锁,并没有锁住整个表的办法。

   具有弱一致性,读反映了最近的完整更新操作结果;对于批量操作,如putAllclear, 并发读可能只反映了一些entriesinsertion 或者removal;相似的,IteratorsEnumerations返回的elements 反映了哈希表在某一时刻的状态,或者iteratorEnumeration创建时的状态

   并不会抛出ConcurrentModificationException,但是iterators 被设计成一次只为一个线程所使用

 

   构造函数concurrencyLevel 参数,默认值是16,代表期望的并发写操作的线程数。因为key放入hash tables实际上随机的,实际并发数可能与这个不同. 使用一个过大的值会浪费空间和时间,使用一个过小的值会加重线程竟争。

   调整resize 是一个相当缓慢的操作,所以,如果可能的话,构造时提供合适的initialCapacity

  java.util.Hashtable一样,但与java.util.HashMap不同,这个类不允许null作为key或者value

 

HashMap中的hash值的求法

 static int hash(Object x) {

        int h = x.hashCode();

        h += ~(h << 9);

        h ^=  (h >>> 14);

        h +=  (h << 4);

        h ^=  (h >>> 10);

        return h;

    }

 

 

segment的方法

    final Segment<K,V> segmentFor(int hash) {

        return (Segment<K,V>) segments[(hash >>> segmentShift) & segmentMask];

}

ssize是大于等于concurrencyLevel的最小的那个2n次方

segmentMask=ssize-1 hashsegment部分的掩码,segmentShift 是求segmenthash值需要无符号右移的次数

 

 

静态内部类HashEntry

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;

        }

 }

 

由于 HashEntry next 域为 final 型,所以新节点只能在链表的表头处插入。 下图是在一个空桶中依次插入 ABC 三个 HashEntry 对象后的结构图:

1. 插入三个节点后桶的结构示意图:

      注意:由于只能在表头插入,所以链表中节点的顺序和插入的顺序相反。

 

Segment

Segment 类继承于 ReentrantLock 类,从而使得 Segment 对象能充当锁的角色。每个 Segment 对象用来守护其(成员对象 table 中)包含的若干个桶。

table 是一个由 HashEntry 对象组成的数组。table 数组的每一个数组成员就是散列映射表的一个桶。

count 变量是一个计数器,它表示每个 Segment 对象管理的 table 数组(若干个 HashEntry 组成的链表)包含的 HashEntry 对象的个数。每一个 Segment 对象都有一个 count 对象来表示本 Segment 中包含的 HashEntry 对象的总数。注意,之所以在每个 Segment 对象中包含一个计数器,而不是在 ConcurrentHashMap 中使用全局的计数器,是为了避免出现“热点域”而影响 ConcurrentHashMap 的并发性。

这样当需要更新计数器时,不用锁定整个 ConcurrentHashMap。

 

 

 static final class Segment<K,V> extends ReentrantLock implements Serializable {

 

        private static final long serialVersionUID = 2249069246763182397L;

         //The number of elements in this segment's region.

        transient volatile int count;

         //Number of updates that alter the size of the table.

        transient int modCount;

         //(int)(capacity * loadFactor)

        transient int threshold;

        transient volatile HashEntry[] table;

        //所有的segment都相同

        final float loadFactor;

 

        Segment(int initialCapacity, float lf) {

            loadFactor = lf;

            setTable(new HashEntry[initialCapacity]);

        }

 

        void setTable(HashEntry[] newTable) {

            threshold = (int)(newTable.length * loadFactor);

            table = newTable;

        }

 

        HashEntry<K,V> getFirst(int hash) {

            HashEntry[] tab = table;

            return (HashEntry<K,V>) tab[hash & (tab.length - 1)];

        }

    }

 

 

 构造方法

 

 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;

        int ssize = 1;

        while (ssize < concurrencyLevel) {

            ++sshift;

            ssize <<= 1;

        }

        segmentShift = 32 - sshift;

        segmentMask = ssize - 1;

        this.segments = new Segment[ssize];

 

        if (initialCapacity > MAXIMUM_CAPACITY)

            initialCapacity = MAXIMUM_CAPACITY;

        int c = initialCapacity / ssize;

        if (c * ssize < initialCapacity)

            ++c;

        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);

    }

 

 

 

   c的最小值是1,默认构造函数时ssizeinitialCapacity 都是16,此时c1

cap也为1,程序创建了16segments,每个 SegmentHashEntry的长度都是1

loadFactor同外面的一样也是0.75f, threshold 0

 

 

      Segment(int initialCapacity, float lf) {

            loadFactor = lf;

            setTable(new HashEntry[initialCapacity]);

        }

 

   void setTable(HashEntry[] newTable) {

            threshold = (int)(newTable.length * loadFactor);

            table = newTable;

}

 put方法

 

     public V put(K key, V value) {

        if (value == null)

            throw new NullPointerException();

        int hash = hash(key);

        return segmentFor(hash).put(key, hash, value, false);

    }

 

 委派给静态内部类Segmentput方法

   static final class Segment<K,V> extends ReentrantLock implements Serializable

 

 

 

  V put(K key, int hash, V value, boolean onlyIfAbsent) {

            lock();

            try {

                int c = count;

                if (c++ > threshold) // ensure capacity

                    rehash();

                HashEntry[] tab = table;

                int index = hash & (tab.length - 1);

                HashEntry<K,V> first = (HashEntry<K,V>) tab[index];

                HashEntry<K,V> e = first;

                while (e != null && (e.hash != hash || !key.equals(e.key)))

                    e = e.next;

                /**

                 如果equals()返回true,hashCode()方法要返回true,从而e.hash一定要与hash相等; 反之hashe.hash相等不一定能保证equals()返回true;

                 而且如果hash不等,equals()一定不相等(反证法)

                 所以while中是这样写的

                **/

                V oldValue;

                if (e != null) {  //e!=null 说明equals()返回true,找到了,此次是替换

                    oldValue = e.value;

                    if (!onlyIfAbsent)

                        e.value = value;

                }

                else {   //插入,新插入元素要在bin的头部

                    oldValue = null;

                    ++modCount;  //插入才改变modCount

                    tab[index] = new HashEntry<K,V>(key, hash, first, value);

                    count = c; // write-volatile

                }

                return oldValue;

            } finally {

                unlock();

            }

        }

 

 

 get 方法

public V get(Object key) {

int hash = hash(key); // throws NullPointerException if key null

return segmentFor(hash).get(key, hash);

}

      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); // recheck

                    }

                    e = e.next;

                }

            }

            return null;

        }

 

    HashEntry<K,V> getFirst(int hash) {

          HashEntry<K,V>[] tab = table;

         return tab[hash & (tab.length - 1)];

  }

 

 

        /**

         * Read 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();

            }

        }

 

 segment 的扩容rehash

每个segment单独扩容

  int c = count; //count volatile 表示此segment的持有的元素数目

  if (c++ > threshold) // ensure capacity

         rehash();

 

1)对于oldTable 中每个bin里的所有HashEntry,它在新table中的索引要么同oldTable中的一样,要么是

i+oldCapacity;

2)并且新table中,设i<oldCapacity,则新table 索引i处的HashEntry只能来源于oldTable的索引i处,不可能来自于oldTable的其他索引;table索引i+oldCapacity处的HashEntry只能来源于oldTable的索引i处,不可能来自于oldTable的其他索引

3)所以算法中如果oldTable的索引i处如果仅有一个HashEntry,可以 这么写  newTable[idx] = e;

   因为索引idx无论i还是i+oldCapacity,它只能从i处来,而i又仅有一个元素

 

 

void rehash() {

            HashEntry[] oldTable = table;            

            int oldCapacity = oldTable.length;

            if (oldCapacity >= MAXIMUM_CAPACITY)

                return;

 

 

            HashEntry[] newTable = new HashEntry[oldCapacity << 1];

            threshold = (int)(newTable.length * loadFactor);

            int sizeMask = newTable.length - 1;

            for (int i = 0; i < oldCapacity ; i++) {

                // We need to guarantee that any existing reads of old Map can

                //  proceed. So we cannot yet null out each bin.

                HashEntry<K,V> e = (HashEntry<K,V>)oldTable[i];

 

                if (e != null) {

                    HashEntry<K,V> next = e.next;

                    int idx = e.hash & sizeMask;

 

                    //  Single node on list

                    if (next == null)

                        newTable[idx] = e;

 

                    else {

                        // Reuse trailing 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;

                       /**

                      lastRun所代表的HashEntry开始,链表其后的元素的在新table中的索引

                    都与lastRun相同,所以 newTable[lastIdx] = lastRun;lastIdx等于i

                      i+oldCapacity此式均移动了从lastRun(包括)开始的一系列HashEntry

                   无论lastRun后面跟了多少HashEntry,这种写法均最大可能地减少了HashEntry

                          的创建

                      **/

                        // Clone all remaining nodes

                        for (HashEntry<K,V> p = e; p != lastRun; p = p.next) {

                            int k = p.hash & sizeMask;

                            HashEntry<K,V> n = (HashEntry<K,V>)newTable[k];

                            newTable[k] = new HashEntry<K,V>(p.key, p.hash,

                                                             n, p.value);

                        }

                    }

                }

            }

            table = newTable;

        }

 

 

 

  HashEntry 对象的不变性来降低读操作对加锁的需求

在代码清单“HashEntry 类的定义中我们可以看到,HashEntry 中的 keyhashnext 都声明为 final 型。这意味着,不能把节点添加到链接的中间和尾部,也不能在链接的中间和尾部删除节点。这个特性可以保证:在访问某个节点时,这个节点之后的链接不会被改变。这个特性可以大大降低处理链表时的复杂性。

同时,HashEntry 类的 value 域被声明为 Volatile 型,Java 的内存模型可以保证:某个写线程对 value 域的写入马上可以被后续的某个读线程“看”到。在 ConcurrentHashMap 中,不允许用 unll 作为键和值,当读线程读到某个 HashEntry 的 value 域的值为 null 时,便知道产生了冲突——发生了重排序现象,需要加锁后重新读入这个 value 值。这些特性互相配合,使得读线程即使在不加锁状态下,也能正确访问 ConcurrentHashMap。

下面我们分别来分析线程写入的两种情形:对散列表做非结构性修改的操作和对散列表做结构性修改的操作。

非结构性修改操作只是更改某个 HashEntry value 域的值。由于对 Volatile 变量的写入操作将与随后对这个变量的读操作进行同步。当一个写线程修改了某个 HashEntry value 域后,另一个读线程读这个值域,Java 内存模型能够保证读线程读取的一定是更新后的值。所以,写线程对链表的非结构性修改能够被后续不加锁的读线程看到

ConcurrentHashMap 做结构性修改,实质上是对某个桶指向的链表做结构性修改。如果能够确保:在读线程遍历一个链表期间,写线程对这个链表所做的结构性修改不影响读线程继续正常遍历这个链表。那么读 / 写线程之间就可以安全并发访问这个 ConcurrentHashMap

结构性修改操作包括 putremoveclear。下面我们分别分析这三个操作。

clear 操作只是把 ConcurrentHashMap 中所有的桶置空,每个桶之前引用的链表依然存在,只是桶不再引用到这些链表(所有链表的结构并没有被修改)。正在遍历某个链表的读线程依然可以正常执行对该链表的遍历。

从上面的代码清单 Segment 中执行具体的 put 操作中,我们可以看出:put 操作如果需要插入一个新节点到链表中时 , 会在链表头部插入这个新节点。此时,链表中的原有节点的链接并没有被修改。也就是说:插入新健 / 值对到链表中的操作不会影响读线程正常遍历这个链表。

下面来分析 remove 操作,先让我们来看看 remove 操作的源代码实现。

 remove

       /**

         * 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[] tab = table;

                int index = hash & (tab.length - 1);

                HashEntry<K,V> first = (HashEntry<K,V>)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;

                        // All entries following removed node can stay

                        // in list, but all preceding ones need to be

                        // cloned.

                        ++modCount;

                        HashEntry<K,V> newFirst = e.next;

                        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();

            }

        }

get 操作一样,首先根据散列码找到具体的链表;然后遍历这个链表找到要删除的节点;最后把待删除节点之后的所有节点原样保留在新链表中,把待删除节点之前的每个节点克隆到新链表中。下面通过图例来说明 remove 操作。假设写线程执行 remove 操作,要删除链表的 C 节点,另一个读线程同时正在遍历这个链表。

4. 执行删除之前的原链表:

5. 执行删除之后的新链表

从上图可以看出,删除节点 C 之后的所有节点原样保留到新链表中;删除节点 C 之前的每个节点被克隆到新链表中,注意:它们在新链表中的链接顺序被反转了

在执行 remove 操作时,原始链表并没有被修改,也就是说:读线程不会受同时执行 remove 操作的并发写线程的干扰。

综合上面的分析我们可以看出,写线程对某个链表的结构性修改不会影响其他的并发读线程对这个链表的遍历访问。

 

  Volatile 变量协调读写线程间的内存可见性

虽然线程 N 是在未加锁的情况下访问链表。Java 的内存模型可以保证:只要之前对链表做结构性修改操作的写线程 M 在退出写方法前写 volatile 型变量 count,读线程 N 在读取这个 volatile 型变量 count 后,就一定能“看到”这些修改。

所有不加锁读方法,在进入读方法时,首先都会去读这个 count 变量。比如下面的 get 方法:

ConcurrentHashMap 中,所有执行写操作的方法(put, remove, clear),在对链表做结构性修改之后,在退出写方法前都会去写这个 count 变量。所有未加锁的读操作(get, contains, containsKey)在读方法中,都会首先去读取这个 count 变量。

根据 Java 内存模型,对 同一个 volatile 变量的写 / 读操作可以确保:写线程写入的值,能够被之后未加锁的读线程看到

这个特性和前面介绍的 HashEntry 对象的不变性相结合,使得在 ConcurrentHashMap 中,读线程在读取散列表时,基本不需要加锁就能成功获得需要的值。这两个特性相配合,不仅减少了请求同一个锁的频率(读操作一般不需要加锁就能够成功获得值),也减少了持有同一个锁的时间(只有读到 value 域的值为 null , 读线程才需要加锁后重读)。

 

 readValueUnderLock

HashEntry构造函数中对value的赋值以及对tab[index]的赋值可能被重新排序

tab[index] = new HashEntry<K,V>(key, hash, first, value);  

value 虽然是volatile的,但tab[index] 并不是volatile

AtomicReferenceArray 可以保证

参见 concurrent programming in java 中的Safe publication

 

 clear方法

   public void clear() {

        for (int i = 0; i < segments.length; ++i)

            segments[i].clear();

    }

 

  void clear() {

            if (count != 0) {

                lock();

                try {

                    HashEntry<K,V>[] tab = table;

                    for (int i = 0; i < tab.length ; i++)

                        tab[i] = null;

                    ++modCount;

                    count = 0; // write-volatile

                } finally {

                    unlock();

                }

            }

        }

 

 size方法

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) {

                        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;

    }

 

 遍历

 /**

     * Returns a {@link Set} view of the mappings contained in this map.

     * The set is backed by the map, so changes to the map are

     * reflected in the set, and vice-versa.  The set supports element

     * removal, which removes the corresponding mapping from the map,

     * via the <tt>Iterator.remove</tt>, <tt>Set.remove</tt>,

     * <tt>removeAll</tt>, <tt>retainAll</tt>, and <tt>clear</tt>

     * operations.  It does not support the <tt>add</tt> or

     * <tt>addAll</tt> operations.

     *

     * <p>The view's <tt>iterator</tt> is a "weakly consistent" iterator

     * that will never throw {@link ConcurrentModificationException},

     * and guarantees to traverse elements as they existed upon

     * construction of the iterator, and may (but is not guaranteed to)

     * reflect any modifications subsequent to construction.

     */

    public Set<Map.Entry<K,V>> entrySet() {

        Set<Map.Entry<K,V>> es = entrySet;

        return (es != null) ? es : (entrySet = new EntrySet());

    }

 

   public Enumeration<K> keys() {

        return new KeyIterator();

    }

 

   public Set<K> keySet() {

        Set<K> ks = keySet;

        return (ks != null) ? ks : (keySet = new KeySet());

    }

 

    final class KeySet extends AbstractSet<K> {

        public Iterator<K> iterator() {

            return new KeyIterator();

        }

        public int size() {

            return ConcurrentHashMap.this.size();

        }

        public boolean contains(Object o) {

            return ConcurrentHashMap.this.containsKey(o);

        }

        public boolean remove(Object o) {

            return ConcurrentHashMap.this.remove(o) != null;

        }

        public void clear() {

            ConcurrentHashMap.this.clear();

        }

    }

 

 jdk1.8中的ConcurrentHashMap

jdk7ConcurrentHashmap中,当长度过长碰撞会很频繁,链表的增改删查操作都会消耗很长的时间,影响性能,所以jdk8 中完全重写了concurrentHashmap,代码量从原来的1000多行变成了 6000多 行,实现上也和原来的分段式存储有很大的区别。

 

主要设计上的变化有以下几点:

不采用segment而采用node,锁住node来实现减小锁粒度。

设计了MOVED状态 当resize的中过程中 线程2还在put数据,线程2会帮助resize

使用3CAS操作来确保node的一些操作的原子性,这种方式代替了锁。

sizeCtl的不同值来代表不同含义,起到了控制的作用。

至于为什么JDK8中使用synchronized而不是ReentrantLock,我猜是因为JDK8中对synchronized有了足够的优化吧。

ConcurrentHashMap在JDK8中进行了巨大改动,很需要通过源码来再次学习下Doug Lea的实现方法。

它摒弃了Segment(锁段)的概念,而是启用了一种全新的方式实现,利用CAS算法。它沿用了与它同时期的HashMap版本的思想,底层依然由“数组”+链表+红黑树的方式思想(JDK7与JDK8中HashMap的实现),但是为了做到并发,又增加了很多辅助的类,例如TreeBin,Traverser等对象内部类。

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值