分析

上一篇文章我们详细分析了一遍hashMap的数据结构,但是我们知道,在多线程情况下。hashmap是不安全的,这是问什么呢?我们首先分析一下:
多线程在对HashMap进行put的时候,邹游一个时候,hashCode会超过负载因子(默认0.75),超过以后,HashMap内部就会对数组扩容,扩容的时候会调用resize().下面我们看一下resize()方法:

 final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    if (oldCap > 0) {
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next; //这一步,获取了e的下一个节点
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

我们假设有这样的一个情况存在:
图片描述

此时,有线程a,b分别往HashMap里面put,a已经得到A的next是B,刚好到此时,Enty数组长度达到了12,需要扩容。而扩容后,需要从新计算hashcode,以便让hash值更加均匀的分布在hash桶上。而刚好,b计算出来了B的next是A,而此时便会出现一个环形,导致死循环!

而ConcurrentHashMap使用的是分段锁技术,ConcurrentHashMap 是一个 Segment 数组,Segment 通过继承 ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全。如下
clipboard.png..]

Segment 内部是由 数组+链表 组成的。

简单分析一下java 8 中的ConcurrentHashMap的put方法:

java 8 的ConcurrentHashMap采用的是懒加载的方式,只有当调用put方法的,map没有被实例化的时候才会将map实例化。然后通过key.hash计算出桶存在的位置,采用CAS的方法将数据put上去。
解释一下CAS : 
   V:需要更新的值
   E:期望的值
   N:最后要改成的值
当V=E则将N赋值给V,否则说明数据被更改过了,放弃操作

下面这张图是java 8 的 ConcurrentHashMap 数据结构。可以看到,table[] 里面的数据结构不仅仅是java 7 的entry了,有node,treeNode,ForwardingNode,ReservationNode等...node可以理解成以前的entry,而后面几个实际上都继承自node,但是都有各自的含义,比如treeNode表示这是一个链表或者是红黑树(ps:之所以将链表转换成红黑树,是因为链表的时间复杂度是O(n),而红黑树是一个平衡二叉树,时间复杂度是O(logn))
https://upload-images.jianshu...


crawler
327 声望79 粉丝

专注技术多年,曾任职京东,汉得等公司主研


引用和评论

0 条评论