Java1.8-HashMap源码分析入门(下)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u013821237/article/details/82943445

引入上篇说到有三个待解答的问题:

  1. HashMap 为什么默认数组长度是16?
  2. 16个单位真的够用吗?如果不够用该怎么办?
  3. 如果多线程执行put数据,get数据,是否是安全的?如何解决安全的问题?

我们先来看第一个:HashMap 为什么默认数组长度是16?

上篇源码分析中我们看到,如果构造HashMap的时候没有指定为数组的长度,那么,数组的长度是默认的:

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

分析之前,我们将上一个问题拆分为两个问题:

  1. 为什么hashmap的容量约定是the power of 2 size呢(2的幂次方)?
  2. 基于问题1的前提下,为什么不是32,或者8呢

首先看第一个:

上篇文章介绍了将元素放到指定位置(桶)的规则:key的hash值对16取余(实际是位运算)。

如果这个分配算法不合理,就会出现某个桶(数组的某一项)大概率被选中放入数据:

就像宋小宝的一个小品台词说的那样,后宫佳丽三千,皇上独爱嫔妾,我就和皇上说呀,要雨~露~均~沾~,可皇上偏不听,就宠我,就宠我!很明显这样是不合理的,老是被翻牌子,身体也受不了。长此以往的累积下去,桶的负载就失衡了,同时效率也急剧下降且不可控。

我们通过下图看一下2的幂次方的合理性:

也就是说要想保证每个值都可以被取到,一定要是1 、11、 111、 1111等才可以,也就是2的幂次方。

再来看第二个问题,为什么是16 而不是8、32或者其他的2的幂次方呢?

引用官方的一句话就是:

Maybe a tradeoff between speed, utility, and quality of bit-spreading.

可能是速度、效用和比特扩展质量之间的权衡。

换言之也就是说,16刚刚好,多了浪费,少了不够用,这是一个折中的考虑。

我们再来看看第二个大问题:16个单位真的够用吗?如果不够用该怎么办?

这一点,源码作者肯定考虑到了:

    /**
     * The load factor used when none specified in constructor.
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

加载因子,即在已被使用了多少比例的时候进行扩容,默认数组长度是16也就是说,当用掉了12(16*0.75)个桶的时候,容量就进行了一次翻倍处理:数组长度由16变为了32。这样就保证了数组容量总是提前被扩展的。

 /**
     * Constructs an empty <tt>HashMap</tt> with the specified initial
     * capacity and load factor.
     *
     * @param  initialCapacity the initial capacity
     * @param  loadFactor      the load factor
     * @throws IllegalArgumentException if the initial capacity is negative
     *         or the load factor is nonpositive
     */
    public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }

我们在构造函数里可以自定义加载因子的大小,在每次put和remove的时候都进行长度的校验,当put后超过加载因子,就执行扩容,当remove后低于加载因子就缩容。

JDK1.8中,除了链表节点外,还新增了一个TreeNode:

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
    TreeNode<K,V> parent;  // red-black tree links
    TreeNode<K,V> left;
    TreeNode<K,V> right;
    TreeNode<K,V> prev;    // needed to unlink next upon deletion
    boolean red;
}

可以看到就是个红黑树节点,有父亲、左右孩子、前一个元素的节点,还有个颜色值。

另外由于它继承自 LinkedHashMap.Entry ,而 LinkedHashMap.Entry 继承自 HashMap.Node ,因此还有额外的 6 个属性:

//继承 LinkedHashMap.Entry 的
Entry<K,V> before, after;

//HashMap.Node 的
final int hash;
final K key;
V value;
Node<K,V> next;

这个红黑树是干什么的呢?:我们假想,当我们王hashMap忠put了很多的数据,每一个桶中都拥有大量的节点,此时,链表的效率已经逐渐降低了,考虑到这点,JDK1.8推出了红黑树节点,如果一个桶中的元素个数超过 TREEIFY_THRESHOLD(默认是 8 ),就使用红黑树来替换链表,从而提高速度。执行链表替换为红黑树的操作是:treeifyBin() 即树形化方法:

//将桶内所有的 链表节点 替换成 红黑树节点
final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    //如果当前哈希表为空,或者哈希表中元素的个数小于 进行树形化的阈值(默认为 64),就去新建/扩容
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        //如果哈希表中的元素个数超过了 树形化阈值,进行树形化
        // e 是哈希表中指定位置桶里的链表节点,从第一个开始
        TreeNode<K,V> hd = null, tl = null; //红黑树的头、尾节点
        do {
            //新建一个树形节点,内容和当前链表节点 e 一致
            TreeNode<K,V> p = replacementTreeNode(e, null);
            if (tl == null) //确定树头节点
                hd = p;
            else {
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);  
        //让桶的第一个元素指向新建的红黑树头结点,以后这个桶里的元素就是红黑树而不是链表了
        if ((tab[index] = hd) != null)
            hd.treeify(tab);
    }
}


    TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
    return new TreeNode<>(p.hash, p.key, p.value, next);
}

这样,在HashMap的扩展过程中,一些可预见的问题得到了解决。

我们再来看看第三个问题:如果多线程执行put数据,get数据,是否是安全的?如何解决安全的问题?

我们可能刷面试题的时候,看到过,HashMap是线程不安全的,HashTable是线程安全的。那么他们的区别在哪里呢?也很简单,我们知道线程是程序执行的最小单位,每时每刻,线程都在抢夺着CPU的时间片,如果我用a线程修改HashMap里的数据,b线程从HashMap里去数据,我们会发现,取到的数据并不是对应的,也就是并不是同步的,而使用HashTable则会打印出来对应顺序的日志。原因就在源码里:

以下代码及注释来自java.util.HashTable
 
public synchronized V put(K key, V value) {
 
    // 如果value为null,抛出NullPointerException
    if (value == null) {
        throw new NullPointerException();
    }
 
    // 如果key为null,在调用key.hashCode()时抛出NullPointerException
 
    // ...
}
 
 
以下代码及注释来自java.util.HasMap
 
public V put(K key, V value) {
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);
    }
    // 当key为null时,调用putForNullKey特殊处理
    if (key == null)
        return putForNullKey(value);
    // ...
}

比较发现,HasnTable的方法有synchronized 关键字,这也就是为什么hashTable可以保证存取的一致性的原因了。

但是当我们的并发操作非常频繁时,HashTable的弊端就展露出来了,因为,他是对整个方法同步,这会导致调用该方法的其他操作被阻塞。如何解决这个问题呢?这个问题出现的原因是同步的粒度太大,如果让同步只在被操作的Node上,就不会导致整个方法被阻塞了,我们再看下ConcurrentHashMap的部分源码:

 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;
    }

我们看到,这时同步关键字放在了 f 上, f 是谁?往上看就可以看到 f 正是操作的节点!

综上:hashMap不是线程安全的,但是速度更快,hashTable作为低频率的同步可用,高频率的同步操作,选择ConrrentHashMap更好一点。

到此,我们三个问题都分析完了,HashMap是必用,必考,必问,必知的问题,源码简单但很经典,谢谢大家赏光。欢迎交流。

猜你喜欢

转载自blog.csdn.net/u013821237/article/details/82943445