源码分析之ConcurrentHashMap

作者: 特立独行的猪手 | 来源:发表于2017-04-27 15:42 被阅读348次

    Doug Lea大神在j.u.c包下给我们提供了一个适用于多线程并发环境使用的集合类ConcurrentHashMap。而如果在多线程环境,不考虑任何线程安全的防范的话,使用HashMap会带来诸多问题。

    HashMap的并发问题

    HashMap集合是非线程安全,在多线程环境下容易出现问题。HashMap在数据更新的时候,会带来很多问题:

    • 数据丢失

    多线程环境下,如两个线程同时在一个bucketput元素,有可能会造成元素写入被覆盖,从而丢失数据。

    • fail-fast机制

    当多个线程对同一个集合进行操作时候,就会触发fail-fast机制并抛出ConcurrentModificationException

    • 死循环

    HashMaptable的大小不够时,如果两个线程同时进行resize的操作。如果某一bucket下元素是用链表记录,在resize过程中,链表在多线程的环境下有可能会形成闭合回链,get请求就会造成死循环,使得CPU飙升。详细可以看看这篇博文:疫苗:JAVA HASHMAP的死循环。不过JDK1.8中已经改写resize方法,应该不会出现这种问题,但这并不是我们可以在多线程环境下使用HashMap理由。

    ...

    线程安全类 HashTable

    HashTable是线程安全的,底层是通过synchronized来保证线程安全。当多线程竞争激烈的时候,没有获得锁的线程都将会阻塞。synchronized修饰所有针对HashTable集合的操作。这样一旦有线程获得锁,其他的线程都只能等待锁的释放,然后再去竞争锁,这样一来HashTable的效率必定会受到影响。

    ConcurrentHashMap 锁机制

    相对于低效的HashTableConcurrentHashMap在锁机制层面上做了优化。锁优化的思路一般有一下几种:

    • 减少锁持有时间
    • 减小锁粒度
    • 锁分离
    • 锁粗化
    • 锁消除

    JDK1.6 ConcurrentHashMap

    ConcurrentHashMap中存储的元素是通过静态内部类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; 
            } 
     }
    

    其中value 字段被声明为 volatile 型,保证其在内存中可见性。keyhashnext 都被声明为 final 型。ConcurrentHashMap存储数据根据keyhash值将数据元素散列到哈希表中每一个bucket中。当发生哈希碰撞时候,会将元素封装成HashEntry构成链表。由于nextfinal类型的,链表中添加元素都将从表头添加。

    减小锁粒度-分段锁

    JDK1.7中,ConcurrentHashMap在锁优化过程通过减小锁粒度的实现了对集合的高效并发操作。ConcurrentHashMap包含一个静态内部类Segment,是用来充当锁的角色。

    每个Segment将维护若干个bucket。而锁只针对Segment而不是整张表,从而实现减小锁的粒度。

    Segment类
    static final class Segment<K,V> extends ReentrantLock implements Serializable { 
            // 包含HashEntry的个数
            transient volatile int count; 
            // table 更新次数
            transient int modCount; 
            // table resize 阈值
            transient int threshold; 
            // HashEntry数组用于存储元素
            transient volatile HashEntry<K,V>[] table; 
            // 加载因子
            final float loadFactor; 
            // 构造函数
            Segment(int initialCapacity, float lf) { 
                loadFactor = lf; 
                setTable(HashEntry.<K,V>newArray(initialCapacity)); 
            } 
    
            void setTable(HashEntry<K,V>[] newTable) { 
                threshold = (int)(newTable.length * loadFactor); 
                table = newTable; 
            } 
    
            HashEntry<K,V> getFirst(int hash) { 
                HashEntry<K,V>[] tab = table; 
                return tab[hash & (tab.length - 1)]; 
            } 
     }
    
    ConcurrentHashMap 初始化
      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; 
                
            int sshift = 0; 
            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; 
            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); 
            } 
    

    ConcurrentHashMap在初始化的过程中,创建了segments数组。ConcurrentHashMap 的结构示意图如下所示:

    put 方法
     public V put(K key, V value) { 
            // 不允许value为null
            if (value == null)        
                throw new NullPointerException(); 
            int hash = hash(key.hashCode());        
            // 根据哈希值找到在segments数组中对一个的segment
            return segmentFor(hash).put(key, hash, value, false); 
     }
     
     
     V put(K key, int hash, V value, boolean onlyIfAbsent) { 
                // segment.put 加锁,锁对象是segment而非整个table
                lock();  
                try { 
                    int c = count; 
                    // 动态扩容
                    if (c++ > threshold)
                        rehash();              
                    
                    // 找出table中key index处的元素
                    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; 
                    if (e != null) {
                        oldValue = e.value; 
                        // key处已有值,根据onlyIfAbsent觉得是否需要覆盖
                        if (!onlyIfAbsent) {
                            e.value = value;                   
                        } 
                    else {              
                        // 元素封装成 HashEntry,添加至表头
                        oldValue = null; 
                        ++modCount; 
                        tab[index] = new HashEntry<K,V>(key, hash, first, value); 
                        count = c;               
                    } 
                    return oldValue; 
                } finally { 
                    // 释放锁
                    unlock();   
                } 
            }
    

    segment是继承ReentrantLockput操作在开始之前会通过调用lock获取锁,添加元素完毕后,调用unlock释放锁。从此可以看出,ConcurrentHashMap在锁方面优化点之一,引入segment,将锁分成N段,每次操作集合,只会锁住对应的segment而非整张表,减小锁粒度,支持一定数量并发写入,提升了并发效率。

    读写锁分离-完全并发读
    get 方法

    ConcurrentHashMap中的读操作如get方法是没有加锁的。在更新操作中,最后都会更新count变量。countvolatile类型,在不加锁的前提下,也可以保证被准确读取。而在读的时候也会去首先判断count的值。如果写入过程读取值,就要加锁等待其他操作释放锁之后再去读取。

    V get(Object key, int hash) { 
                if(count != 0) {
                    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); 
                        } 
                        e = e.next; 
                    } 
                } 
                return null; 
            }
    

    ConcurrentHashMap中,在不加锁的前提下可以成功读取值,这种读写分离锁的实现,减少了请求获取锁的频次,使得并发效率进一步提高。

    JDK1.8 ConcurrentHashMap

    JDK1.8中,ConcurrentHashMap的实现不再使用Segment做锁分段方法。新版本中ConcurrentHashMap采用底层的CPUCAS指令和synchronized来实现锁机制。数据存储和HashMap一致,采用数组、链表和红黑树实现。

    sizeCtl 变量
    private transient volatile int sizeCtl;
    

    sizeCtl是控制标识符,不同的值代表不同的意义。

    • -1 表示正在初始化
    • -N 表示N-1个线程正在进行扩容
    • 0 代表尚未初始化
    • >0 扩容阈值
    table 初始化

    table初始化是在put操作过程中进行的。可以从源码角度看一下initTable是如何保证在多线程环境下,只会初始化一次。

      private final Node<K,V>[] initTable() {
            Node<K,V>[] tab; int sc;
            while ((tab = table) == null || tab.length == 0) {
                if ((sc = sizeCtl) < 0)
                    // sizeCtl 小于0表明有其他线程正在操作table 初始化或者扩容,当前线程让出CPU
                    Thread.yield(); // lost initialization race; just spin
                else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                    // 通过CAS机制讲更新sizeCtl为-1,保证线程安全。
                    try {
                        if ((tab = table) == null || tab.length == 0) {
                            // table 初始化
                            int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                            @SuppressWarnings("unchecked")
                            Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                            table = tab = nt;
                            sc = n - (n >>> 2);
                        }
                    } finally {
                        sizeCtl = sc;
                    }
                    break;
                }
            }
            return tab;
        }
    

    CASCompare and Swap),从字面意思来看就是比较并交换。CAS有3个操作数,原始值V,预期值A,要修改的值B,当且仅当原始值V等于预期值A的时候,才会将V修改为BJava中通过sun.misc.Unsafe类调用JNI代码来实现CPUCAS指令。

    这里通过借助CAS实现了区别于内部悲观独占锁synchronized的乐观锁来实现ConcurrentHashMap的并发安全。

    put 方法
     public V put(K key, V value) {
            return putVal(key, value, false);
        }
    
        /** Implementation for put and putIfAbsent */
        final V putVal(K key, V value, boolean onlyIfAbsent) {
            if (key == null || value == null) throw new NullPointerException();
            int hash = spread(key.hashCode());
            int binCount = 0;
            for (Node<K,V>[] tab = table;;) {
                Node<K,V> f; int n, i, fh;
                if (tab == null || (n = tab.length) == 0)
                    tab = initTable();
                else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                    if (casTabAt(tab, i, null,
                                 new Node<K,V>(hash, key, value, null)))
                        break;                   // no lock when adding to empty bin
                }
                else if ((fh = f.hash) == MOVED)
                    tab = helpTransfer(tab, f);
                else {
                    V oldVal = null;
                    synchronized (f) {
                        if (tabAt(tab, i) == f) {
                            if (fh >= 0) {
                                binCount = 1;
                                for (Node<K,V> e = f;; ++binCount) {
                                    K ek;
                                    if (e.hash == hash &&
                                        ((ek = e.key) == key ||
                                         (ek != null && key.equals(ek)))) {
                                        oldVal = e.val;
                                        if (!onlyIfAbsent)
                                            e.val = value;
                                        break;
                                    }
                                    Node<K,V> pred = e;
                                    if ((e = e.next) == null) {
                                        pred.next = new Node<K,V>(hash, key,
                                                                  value, null);
                                        break;
                                    }
                                }
                            }
                            else if (f instanceof TreeBin) {
                                Node<K,V> p;
                                binCount = 2;
                                if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                               value)) != null) {
                                    oldVal = p.val;
                                    if (!onlyIfAbsent)
                                        p.val = value;
                                }
                            }
                        }
                    }
                    if (binCount != 0) {
                        if (binCount >= TREEIFY_THRESHOLD)
                            treeifyBin(tab, i);
                        if (oldVal != null)
                            return oldVal;
                        break;
                    }
                }
            }
            addCount(1L, binCount);
            return null;
        }
    
    • keyvalue为空,抛出NP异常,表明ConcurrentHashMap不允许keyvalue为空
    • 调用spread方法计算出key的哈希值
    • 遍历table
      • 如果table为空,进行初始化工作
      • 当前index没有其他元素,调用casTabAt通过CAS更新元素值
      • 检测到其他线程正在扩容,会调用helpTransfer方法协助其调用
      • 当发生哈希碰撞,无论是链表还是红黑树,添加元素的操作都需要上锁synchronized
    get 方法

    ConcurrentHashMapget方法,没有上锁,表明ConcurrentHashMap在读操作上是支持完全并发的。效率层面不受加锁机制的影响。

        public V get(Object key) {
            Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
            int h = spread(key.hashCode());
            if ((tab = table) != null && (n = tab.length) > 0 &&
                (e = tabAt(tab, (n - 1) & h)) != null) {
                if ((eh = e.hash) == h) {
                    if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                        return e.val;
                }
                else if (eh < 0)
                    return (p = e.find(h, key)) != null ? p.val : null;
                while ((e = e.next) != null) {
                    if (e.hash == h &&
                        ((ek = e.key) == key || (ek != null && key.equals(ek))))
                        return e.val;
                }
            }
            return null;
        }
    

    总结

    在多线程并发环境下,HashMap是肯定不能用。我们要选择适用于多线程高并发场景的集合类。

    ConcurrentHashMap支持完全并发读操作,从效率上来说是优于HashTable,但由于ConcurrentHashMap在读操作中存在弱一致性,所以还是需要结合场景来决定是否用ConcurrentHashMap替代HashTable

    参考

    探索 ConcurrentHashMap 高并发性的实现机制

    相关文章

      网友评论

        本文标题:源码分析之ConcurrentHashMap

        本文链接:https://www.haomeiwen.com/subject/bffnzttx.html