HashMap与ConcurrentHashMap

在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[] tab; Node 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 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)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 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 getNode(int hash, Object key) {
        Node[] tab; Node 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)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 e = src[j];
            if (e != null) {
                src[j] = null;
                do {//依次将链表中的元素,重新添加到新的table中
                    Entry 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)