美文网首页
HashMap与ConcurrentHashMap

HashMap与ConcurrentHashMap

作者: 菠萝丶丶 | 来源:发表于2020-08-05 23:13 被阅读0次

    在Java编程中使用到集合是经常会用到List,Set,Map这三大集合接口,而Map作为集合的一种也是经常广泛的被使用,而Map的最常用到的一个实现类就要说到HashMap了,而HashMap并不是线程安全的,下面我们将会带着大家来一起研究HashMap的线程安全问题以及线程安全的Map。

    HashMap的实现分析

    此处主要从两个方面分析:

    • put方法
    • get方法

    put方法
    下面是put方法的源码:

        public V put(K key, V value) {
            return putVal(hash(key), key, value, false, true);
        }
    
        /**
         * @param hash key的hash值
         * @param key 键
         * @param value 值
         * @param onlyIfAbsent 设为true表示如果键不存在,才会写入值。
         * @param evict 
         * @return 返回value
         */
        final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                       boolean evict) {
            Node<K,V>[] tab; Node<K,V> p; int n, i;
            // 如果当前Map的元素数组为空 或者 数组长度为0,那么需要初始化元素数组,同时该方法也是扩容方法
            if ((tab = table) == null || (n = tab.length) == 0)
                n = (tab = resize()).length;
            // 根据hash值和数组长度取摸计算出数组下标
            if ((p = tab[i = (n - 1) & hash]) == null)
                tab[i] = newNode(hash, key, value, null);
            else {
                Node<K,V> e; K k;
                if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k))))
                    e = p;
                // 红黑树解析
                else if (p instanceof TreeNode)
                    e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
                else {
                    for (int binCount = 0; ; ++binCount) {
                        if ((e = p.next) == null) {
                            p.next = newNode(hash, key, value, null);
                            //  如果链表上元素的个数已经达到了阀值(可以改变存储结构的临界值)
                            if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                                treeifyBin(tab, hash);
                            break;
                        }
                        if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                            break;
                        p = e;
                    }
                }
                // 说明找了和要写入的key对应的元素,根据情况来决定是否覆盖值
                if (e != null) { // existing mapping for key
                    V oldValue = e.value;
                    if (!onlyIfAbsent || oldValue == null)
                        e.value = value;
                    afterNodeAccess(e);
                    return oldValue;
                }
            }
            ++modCount;
            if (++size > threshold)
                resize();
            afterNodeInsertion(evict);
            return null;
        }
        static final int hash(Object key) {
            int h;
            return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
        }
    

    逐步分析put方法的实现大致可以分为以下几个步骤:
    1、hash(key),该方法获取到key的hashCode,然后通过位运算 异或(^) 重新计算hash(为了将高位参与到后续计算中,避免重复发生hash碰撞的几率)
    2、判断是否需要扩容(初始化)
    3、i = (n - 1) & hash通过容量大小 与运算(&) hash得出一个要放置数据的下标,判断该位置是否已存在元素,如果不存在则创建一个node放置到该位置
    4、如果已经存在元素则比较key是否相等或者key的hashCode方法返回值是否相等,如果相等则替换并返回替换前的值
    5、如果key不相等并且hashCode也不相等,则再判断原节点类型是否是TreeNode(红黑树),再调用红黑树的putTreeVal方法
    6、如果上述两个条件都不是则说明是使用链表存储的,通过链表的方式查找是否有原数据或者是新创建数据
    7、判断当前链表上元素是否超过阈值TREEIFY_THRESHOLD(8),如果超过则转换为红黑树进行存储
    8、如果没超过则按照正常的链表增加元素,并从putVal方法返回

    get方法
    get方法的实现相对较为简单,下面是get方法的具体源码:

        public V get(Object key) {
            Node<K,V> e;
            return (e = getNode(hash(key), key)) == null ? null : e.value;
        }
    
        /**
        * 该方法是Map.get方法的具体实现
        * 接收两个参数
        * @param hash key的hash值,根据hash值在节点数组中寻址,该hash值是通过hash(key)得到的,可参见:hash方法解析
        * @param key key对象,当存在hash碰撞时,要逐个比对是否相等
        * @return 查找到则返回键值对节点对象,否则返回null
        */
        final Node<K,V> getNode(int hash, Object key) {
            Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
            if ((tab = table) != null && (n = tab.length) > 0 &&
                (first = tab[(n - 1) & hash]) != null) {
                if (first.hash == hash && // always check first node
                    ((k = first.key) == key || (key != null && key.equals(k))))
                    return first;
                if ((e = first.next) != null) {
                    if (first instanceof TreeNode)
                        return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                    do {
                        if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                            return e;
                    } while ((e = e.next) != null);
                }
            }
            return null;
        }
    

    get方法的实现分析可以分为以下几个步骤:
    1、与put时一致,调用hash方法重新计算hash
    2、通过计算集合长度 与运算(&) hash的结果来得出放置元素时的下标,并且判断元素不为空
    3、继续判断key是否与存储节点的key相等或者equals结果是否相等,如果相等则直接返回该节点
    4、如果不相等并且该节点有下一个节点,此时可能是红黑树结构或者是链表结构
    5、如果是红黑树,则调用红黑树的getTreeNode方法返回节点
    6、否则肯定是链表,遍历链表直到获取到匹配的key的节点,或者直到节点遍历完还没找到,则返回null

    HashMap线程不安全
    看过了HashMap的get和put方法的实现之后,该思考为什么HashMap会有线程不安全的问题了呢?

    首先我们要先看java1.7中的HashMap的线程安全问题,可能会造成死循环和数据丢失,由于Java1.7中的HashMap是使用头插法,在put的时候可能造成两个entry节点的循环引用,从而造成下一次get时死循环问题,主要问题的源码如下:

        /**对HashMap进行容量扩充
         * Transfers all entries from current table to newTable.
         */
        void transfer(Entry[] newTable) {
            Entry[] src = table;
            int newCapacity = newTable.length;
            for (int j = 0; j < src.length; j++) {//遍历原table中的所有表头
                Entry<K,V> e = src[j];
                if (e != null) {
                    src[j] = null;
                    do {//依次将链表中的元素,重新添加到新的table中
                        Entry<K,V> next = e.next;//          代码 1
                        int i = indexFor(e.hash, newCapacity);
                        e.next = newTable[i];
                        newTable[i] = e;
                        e = next;
                    } while (e != null);
                }
            }
        }
    

    此处由于篇幅较多,不再仔细分析循环引用出现的具体步骤,有兴趣的话可以参考https://blog.csdn.net/swpu_ocean/article/details/88917958

    那么Java1.8中的HashMap已经改为尾插法,为什么还会有线程不安全问题呢?
    Java1.8中的HashMap已经修改解决了死循环和数据丢失,但是依然可能造成数据覆盖的问题。

    我们现在重新回去看putVal方法代码块的第20行,此处判断了是否发生了hash碰撞,如果没有则直接插入,如果有则转为链表或红黑树存储。假设有A和B两个线程同时进入了此处判断条件,A判断该位置为空,此时CPU调度切换为B线程,线程判断此处位置依然为空,即执行插入并且结束,回到A线程由于已经判断过为空,则将原位置上的元素直接替换为A线程的value,此时原来B线程的数据被直接覆盖。

    相关文章

      网友评论

          本文标题:HashMap与ConcurrentHashMap

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