学习笔记 1.高性能编程 1.3.3 并发容器类HashMap Concurrenthashmap

在日常工作中我们经常用到的容器有许多,其中就包括了map类,而其中最最常用到的就非HashMap,那么HashMap到底是什么的呢?

1.什么是HashMap

HashMap是一个散列桶(数组和链表),它存储的内容是键值对(key-value)映射。

HashMap继承于AbstractMap,实现了Map,Cloneable,Java.io.Serializable接口

HashMap采用了数组和链表的数据结构,在查询和修改方面继承了数组的线性查找和链表的寻址修改

HashMap的实现是不同步的,是非synchronized,所以它不是线程安全的,但是它的速度很快。

HashMap的key,value都可以为null,而HashTable则不能(原因是equlas()方法需要对象,因为HashMap是后出的API经过处理才可以),且HashMap的映射不是有序的。

HashMap的实例有两个参数影响其性能:初始容量和加载因子。容量是哈希表中桶的数量,初始容量只是哈希表在创建时的容量。加载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行rehash操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数。

通常默认加载因子是0.75,这都是在时间和空间成本上寻求的一种折中。加载因子过高虽然减小了空间开销,但是同时也增加了查询成本(在大多数HashMap类的操作中,包括get和put操作,都反映了这一点)。在设置初始容量时应该考虑到映射中所需要的条目数及其加载因子,以便最大限度地减少rehash操作次数。如果初始容量大于最大条目数除以加载因子,则不会发生rehash操作。     

上面那些都可以在源码中一一找出

首先HashMap在JDK1.7中是用数组和链表的方式进行存储的,在JDK1.8之后HashMap加入红黑树,提升了自己的查询效率,首先在HashMap的源码中我们可以明确的看到HashMap的成员变量都有什么K,V学习笔记 1.高性能编程 1.3.3 并发容器类HashMap Concurrenthashmap_第1张图片

其中可以用来存储数据的有table和entrySet,一个是Node,一个是Entry,通过查看我们通常使用的put方法或者HashMap的构造方法,得到HashMap通过数组存储数据同样的通过HashMap的存储过程也可以了解HashMap存储的原理,

    public V put(K key, V value) {
       //来自父类的put的方法,在JDK1.7中是直接实现了put方法
       //JDk1.7之后HashMap的实现做了改变
       //此时通过Key值运用Hash算法先取出散列值
       return putVal(hash(key), key, value, false, true);
    }


    /**
     * Implements Map.put and related methods.
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @param onlyIfAbsent if true, don't change existing value
     * @param evict if false, the table is in creation mode.
     * @return previous value, or null if none
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node[] tab; Node p; int n, i;
        //若此时数组为空或长度为0 也就是说我们没有初始化或者个HashMap指定长度
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //如果通过对Hash来的值取模,算出p所在数组下标,若在这个下标没有值为空就将值填入
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node e; K k;
            //如果Hash值相等,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) {
                    //如果p.next为空则将新值插入,此处可以看出引入了链表
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        //如果链表长度大于TREEIFY_THRESHOLD -1,就会从链表转为树
                        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;
                }
            }
            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;
    }   

通过对源码的简单学习可以将上面锁总结的几点一一证实,虽然通过链表的方式在一定程度上扩大了存储空间,但是随着链表长度的不断增加,我们查找的时间也变得越来越长,所以HashMap也需要扩容,那么它是怎么扩容的呢?在HashMap初始化的时候若不设置长度,会自动给与一个默认值,那么什么情况下会进行扩容呢?只有在空间不够的时候HashMap会进行扩容,换句话说当我们添加一个元素的时候,发现空间不够了,就会进行扩容,所以我们可以在添加元素的方法中找到扩容的步骤,下面是Hash Map中一些参数的含义

     /**
     * 默认值
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    /**
     * 最大值
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;

    /**
     * 默认加载因子。当键值对的数量大于 CAPACITY * 0.75 时,就会触发扩容
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    /**
     * 计数阈值。链表的元素大于8的时候,链表将转化为树
     */
    static final int TREEIFY_THRESHOLD = 8;

    /**
     * 计数阈值。resize操作时,红黑树的节点数量小于6时使用链表来代替树
     */
    static final int UNTREEIFY_THRESHOLD = 6;

    /**
     * 在转变成树之前,还会有一次判断,只有键值对数量大于 64 才会发生转换。
     * 这是为了避免在哈希表建立初期,多个键值对恰好被放入了同一个链表中而导致不必要的转化
     */
    static final int MIN_TREEIFY_CAPACITY = 64;
    
    /**
     * 调整大小的下一个大小值(容量*加载因子)。
     * @serial
     */
    // 此外,如果尚未分配表数组,则此//字段保持初始数组容量,或者表示
    // DEFAULT_INITIAL_CAPACITY为零。
    int threshold;

    /**
     * 此HashMap经过结构修改的次数*结构修改是指更改HashMap中的映射数或以
     * 其他方式修改其内部结构(例如,* rehash)的修改。
     * 该字段用于在* HashMap的Collection-views上快速生成迭代器。
     */
    transient int modCount;

 在上面的put方法中按我们在最后可以看到,有一个判断if (++size > threshold)此时HashMap中在添加一个新元素,就会超过约定好的扩容临界值从而触发扩容方法resize()

final Node[] resize() {
        //新建一个存储空间
        Node[] oldTab = table;
        //若是新的则为0 否则为需要扩容的长度
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        //一般为长度*加载因子(0.75)
        int oldThr = threshold;
        //初始化新长度和扩容的阀值
        int newCap, newThr = 0;
        if (oldCap > 0) {
            //若长度已经是最大值,无法继续扩容,则把扩容的阀值设置为最大值
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //若新长度小于最大值(旧长度的两倍)且旧长度大于默认值
            //新长度为旧长度的2倍,新阀值为旧阀值的2倍
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        //旧长度为0,且阀值不为0,新长度就等于阀值
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        //需要扩容的HashMap未初始化,此时初始化HashMap ,长度为默认值,阀值为长度* 0.75
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        //若新阀值为0 
        if (newThr == 0) {
            //新长度*加载因子
            float ft = (float)newCap * loadFactor;
            //新长度若小于最大值且阀值小于最大值,则等于ft,否则为最大值
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        //更新阀值
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
        //初始化容器
        Node[] newTab = (Node[])new Node[newCap];
        table = newTab;
        //若旧HashMap不为空,需要将里面的值进行重新排列
        if (oldTab != null) {
            //遍历旧HashMap
            for (int j = 0; j < oldCap; ++j) {
                Node 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)e).split(this, newTab, j, oldCap);
                    else { //遍历链表
                        Node loHead = null, loTail = null;
                        Node hiHead = null, hiTail = null;
                        Node next;
                        do {
                            next = e.next;
                            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;
    }

通过上面的代码和注释不难看出HashMap每次扩容都为原来的两倍,并且在不为空的时候需要重新遍历HashMap,若创建的时候没有预估需要存放多少元素进入,HashMap的扩容会降低代码的效率。所以在阿里巴巴规范中会要求初始化HashMap的时候需要加上容量,同时也发现了,在HashMap中没有加锁和任何关键字来保证HashMap在多线程中的安全,所以说HashMap是线程不安全的。ConcurrentHashMap是线程安全的,在JDK1.7中ConcurrentHashMap运用segments,分段加锁,在数组中存放的是HashTable,相当于每一个坐标都有一个自己的锁,从而实现了线程安全,且并发数量是segments的长度,但是并发级别是不会变化的,一旦确认就无法改变,但是segment的长度依然可以改变,逻辑和HashMap类似

学习笔记 1.高性能编程 1.3.3 并发容器类HashMap Concurrenthashmap_第2张图片

而在JDK1.8之后ConcurrentHashMap又变为了数组加链表的方式,使用CAS操作和synchronize关键字实现线程安全,同时在链表头部添加特殊字段如forwarding,来实现扩容时的线程安全,

学习笔记 1.高性能编程 1.3.3 并发容器类HashMap Concurrenthashmap_第3张图片

除了上面两个容器,下面不得不说的另外一个容器是ConcurrentSkipListMap,理解ConcurrentSkipListMap的时候需要先理解一下跳表,我们说HashMap里面存储的是数组和链表,ConcurrentSkipListMap在Node中添加了index其中包含了级别Leven和right,Leven将链表分层了,同一级别的Node虽然不一定通过next连接,也可以通过right做关联,这样可以跳过中间的node,减少了next的使用,提升了查询效率,当然同一个Node是可以有多个index这样可以去对应多个级别,跳的也可以更远一点,当进行删除的时候index也会被删除,但是right不会中断,除非被删除的Node的right为null

学习笔记 1.高性能编程 1.3.3 并发容器类HashMap Concurrenthashmap_第4张图片

 

 

你可能感兴趣的:(学习笔记 1.高性能编程 1.3.3 并发容器类HashMap Concurrenthashmap)