当前位置: 萬仟网 > IT编程>开发语言>Java > JDK源码分析-HashMap

JDK源码分析-HashMap

2019年12月28日  | 萬仟网IT编程  | 我要评论
一.HashMap的内部属性 1.1 成员变量 HashMap包含的KV键值对的数量,也就是我们通常调用Map.size()方法的返回值 HashMap的结构被修改的次数(包括KV映射数量和内部结构rehash次数),用于判断迭代器梳理中不一致的快速失败。 下一次扩容时的阈值,达到阈值便会触发扩容机 ...

一.hashmap的内部属性

1.1 成员变量

1.1.1 size:

hashmap包含的kv键值对的数量,也就是我们通常调用map.size()方法的返回值

    public int size() {
        return size;
    }
1.1.2 modcount

hashmap的结构被修改的次数(包括kv映射数量和内部结构rehash次数),用于判断迭代器梳理中不一致的快速失败。

abstract class hashiterator {
...
  final node<k,v> nextnode() {
            node<k,v>[] t;
            node<k,v> e = next;
            if (modcount != expectedmodcount)
                throw new concurrentmodificationexception();
            if (e == null)
                throw new nosuchelementexception();
            if ((next = (current = e).next) == null && (t = table) != null) {
                do {} while (index < t.length && (next = t[index++]) == null);
            }
            return e;
        }
...
}
1.1.3 threshold

下一次扩容时的阈值,达到阈值便会触发扩容机制resize(阈值 threshold = 容器容量 capacity * 负载因子 load factor)。也就是说,在容器定义好容量之后,负载因子越大,所能容纳的键值对元素个数就越多。计算方法如下:

 static final int tablesizefor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= maximum_capacity) ? maximum_capacity : n + 1;
    }
1.1.4 loadfactor

负载因子,默认是0.75

1.1.5 node<k,v>[] table

底层数组,充当哈希表的作用,用于存储对应hash位置的元素,数组长度总是2的n次幂

1.2 内部类

1.2.1 node<k,v>
/**
     * 定义hashmap存储元素结点的底层实现
     */
    static class node<k,v> implements map.entry<k,v> {
        final int hash;//元素的哈希值 由final修饰可知,当hash的值确定后,就不能再修改
        final k key;// 键,由final修饰可知,当key的值确定后,就不能再修改
        v value; // 值
        node<k,v> next; // 记录下一个元素结点(单链表结构,用于解决hash冲突)

        
        /**
         * node结点构造方法
         */
        node(int hash, k key, v value, node<k,v> next) {
            this.hash = hash;//元素的哈希值
            this.key = key;// 键
            this.value = value; // 值
            this.next = next;// 记录下一个元素结点
        }

        public final k getkey()        { return key; }
        public final v getvalue()      { return value; }
        public final string tostring() { return key + "=" + value; }

        /**
         * 为node重写hashcode方法,值为:key的hashcode 异或 value的hashcode 
         * 运算作用就是将2个hashcode的二进制中,同一位置相同的值为0,不同的为1。
         */
        public final int hashcode() {
            return objects.hashcode(key) ^ objects.hashcode(value);
        }

        /**
         * 修改某一元素的值
         */
        public final v setvalue(v newvalue) {
            v oldvalue = value;
            value = newvalue;
            return oldvalue;
        }

        /**
         * 为node重写equals方法
         */
        public final boolean equals(object o) {
            if (o == this)
                return true;
            if (o instanceof map.entry) {
                map.entry<?,?> e = (map.entry<?,?>)o;
                if (objects.equals(key, e.getkey()) &&
                    objects.equals(value, e.getvalue()))
                    return true;
            }
            return false;
        }
    }
1.2.2 treenode<k,v>
 static final class treenode<k,v> extends linkedhashmap.entry<k,v> {
        //与left、right联合使用实现树结构   
        treenode<k,v> parent;  
        treenode<k,v> left;
        treenode<k,v> right;
        // needed to unlink next upon deletion
        treenode<k,v> prev;   
        //记录树节点颜色 
        boolean red;

     /**
     * 操作方法
     * 包括:树化、链栈化、增删查节点、根节点变更、树旋转、插入/删除节点后平衡红黑树
     */
     ...
}

1.3 key的hash算法

key的hash算法源码如下:

  static final int hash(object key) {
        int h;
         ///key.hashcode()为哈希算法,返回初始哈希值
        return (key == null) ? 0 : (h = key.hashcode()) ^ (h >>> 16);
    } 

因为hashmap中是允许key 为null的键值对,所以先判断了key == null。当key 不为null的时候,hash算法是先通过key.hashcode()计算出一个hash值再与改hash值的高16位做异或运算(有关异或运算请移步:) 上面的key.hashcode()已经计算出来了一个hash散列值,可以直接拿来用了,为何还要做一个异或运算? 是为了对key的hashcode进行扰动计算(),防止不同hashcode的高位不同但低位相同导致的hash冲突。简单点说,就是为了把高位的特征和低位的特征组合起来,降低哈希冲突的概率,也就是说,尽量做到任何一位的变化都能对最终得到的结果产生影响

二. hashmap的初始化

hashmap的初始化有以下四种方法:

  1. hashmap()
  2. hashmap(int initialcapacity)
  3. hashmap(int initialcapacity, float loadfactor)
  4. hashmap(map<? extends k, ? extends v> m)

方法1的源码如下:

   public hashmap() {
        //使用默认的default_load_factor = 0.75f
        this.loadfactor = default_load_factor; // all other fields defaulted
    }

  其中的方法2本质上都是调用了方法3。initialcapacity是初始化hashmap的容量,loadfactor是在1.1.4中提到的负载因子。 方法3的源码注释如下:

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

方法4源码注释如下:

public hashmap(map<? extends k, ? extends v> m) {
        this.loadfactor = default_load_factor;
        putmapentries(m, false);
    }

    /**
     * implements map.putall and map constructor
     *
     * @param m 要初始化的map
     * @param evict 初始化构造map时为false,其他情况为true
     */
    final void putmapentries(map<? extends k, ? extends v> m, boolean evict) {
        int s = m.size();
        //判断当前m容量
        if (s > 0) {
            // 初始化
            if (table == null) { 
                //ft按照默认加载因子计算ft=s/0.75 +1计算出来
                float ft = ((float)s / loadfactor) + 1.0f;
                int t = ((ft < (float)maximum_capacity) ?
                         (int)ft : maximum_capacity);
                if (t > threshold)
                    threshold = tablesizefor(t);
            }
            else if (s > threshold)
                //s大于threshlod,需要扩容
                resize();
            //遍历m,并通过putval初始化数据
            for (map.entry<? extends k, ? extends v> e : m.entryset()) {
                k key = e.getkey();
                v value = e.getvalue();
                putval(hash(key), key, value, false, evict);
            }
        }
    }

  

三. put过程

3.1 put的正常调用过程

put方法是hashmap的增加kv对的入口,putval方法是具体实现,整个过程的大致流程如下:

  1. 对key的hashcode()做hash,然后再计算index;
  2. 如果没碰撞直接放到bucket里;
  3. 如果碰撞了,以链表的形式存在buckets后;
  4. 如果碰撞导致链表过长(大于等于treeify_threshold),就把链表转换成红黑树;
  5. 如果节点已经存在就替换old value(保证key的唯一性)
  6. 如果bucket满了(超过load factor*current capacity),就要resize
   public v put(k key, v value) {
        return putval(hash(key), key, value, false, true);
    } 

3.2 put过程剖析

putval方法的源码解析如下:

/**
     * implements map.put and related methods
     *
     * @param hash key的hash值
     * @param key the key
     * @param value the value to put
     * @param onlyifabsent 为true不修改已经存在的值
     * @param evict 为false表示创建
     * @return previous value, or null if none
     */
    final v putval(int hash, k key, v value, boolean onlyifabsent,
                   boolean evict) {
        node<k,v>[] tab; node<k,v> p; int n, i;
        //table为空则创建
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //根据hash值计算出index,并校验当前tab中index的值是否存在
        if ((p = tab[i = (n - 1) & hash]) == null)
            //当前tab中index的值为空,则直接插入到tab中
            tab[i] = newnode(hash, key, value, null);
        else {
            //当前tab节点已经存在hash相同的值
            node<k,v> e; k k;
            //分别比较hash值和key值相等,就直接替换现有的节点
            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;
                }
            }
            //把初始化的节点写入
            if (e != null) { // existing mapping for key
                v oldvalue = e.value;
                if (!onlyifabsent || oldvalue == null)
                    e.value = value;
                afternodeaccess(e);
                return oldvalue;
            }
        }
        ++modcount;
        //判断是否需要resize扩容
        if (++size > threshold)
            resize();
        afternodeinsertion(evict);
        return null;
    }

  

 

 

四. 扩容

4.1 什么条件下会扩容

当向容器添加元素的时候,会判断当前容器的元素个数,如果大于等于threshold阈值(即当前数组的长度乘以加载因子的值的时候),就要自动扩容了。

4.2 如何扩容

hashmap的扩容是调用了resize方法(初始化的时候也会调用),扩容是按照两倍的大小进行的,源码如下:

final node<k,v>[] resize() {
        node<k,v>[] oldtab = table;
        //取出tabble的大小
        int oldcap = (oldtab == null) ? 0 : oldtab.length;
        int oldthr = threshold;
        int newcap, newthr = 0;
        //当map不为空的时候
        if (oldcap > 0) {
            //map已经大于最大maximum_capacity = 1 << 30
            if (oldcap >= maximum_capacity) {
                threshold = integer.max_value;
                return oldtab;
            }
            else if ((newcap = oldcap << 1) < maximum_capacity &&
                     oldcap >= default_initial_capacity)
                //向左位移1,扩大两倍
                newthr = oldthr << 1; // double threshold
        }
        //也就是hashmap初始化是调用了hashmap(initialcapacity)或者hashmap(initialcapacity,loadfactor)构造方法
        else if (oldthr > 0) // initial capacity was placed in threshold
            newcap = oldthr;
        //使用的是hashmap()构造方法
        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) {
            //当map不为空,需要赋值原有map中的数据到新table中
            ...
        }
        return newtab;
    }

  

从源码中可以看出,resize扩容是一个非常消耗性能的操作,所以在我们可以预知hashmap大小的情况下,预设的大小能够避免resize,也就能有效的提高hashmap的性能。

五. 树化与链表化

5.1 什么条件下会树化

当bincount达到阈值treeify_threshold - 1的时候就会发生树化(treeify_threshold = 8),也就是bincount>=7的时候就会进入到treeifybin方法,但只有当大于min_treeify_capacity(= 64)才会触发treeify树化

 if (bincount >= treeify_threshold - 1) // -1 for 1st
      treeifybin(tab, hash);

5.2 树化算法

算法

final void treeifybin(node<k,v>[] tab, int hash) {
        int n, index; node<k,v> e;
        if (tab == null || (n = tab.length) < min_treeify_capacity)
            resize();
        // 通过hash求出bucket的位置    
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            treenode<k,v> hd = null, tl = null;
            do {
                // 将node节点包装成treenode
                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)
                // 对treenode链表进行树化
                hd.treeify(tab);
        }
    }

        final void treeify(node<k,v>[] tab) {
            treenode<k,v> root = null;
            //遍历treenode
            for (treenode<k,v> x = this, next; x != null; x = next) {
                //next向前
                next = (treenode<k,v>)x.next;
                x.left = x.right = null;
                //当根节点为空,就赋值
                if (root == null) {
                    x.parent = null;
                    x.red = false;
                    root = x;
                }
                else {
                   //root存在,就自顶向下遍历
                    ...
                  
            }
            moveroottofront(tab, root);
        } 

六. get过程

get方法相对于put要简单一些,源码如下:

public v get(object key) {
        node<k,v> e;
        //根据key取hash,算法与put中一样
        return (e = getnode(hash(key), key)) == null ? null : e.value;
    }

    final node<k,v> getnode(int hash, object key) {
        node<k,v>[] tab; node<k,v> first, e; int n; k k;
        //1. 判断table不为空
        //2. table长度大于0
        //3. 与put方法一样计算tab的索引,并判断是否为空
        if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
            //比较第一个节点的hash和key是都都相等
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            if ((e = first.next) != null) {
                //红黑树:直接调用gettreenode()
                if (first instanceof treenode)
                    return ((treenode<k,v>)first).gettreenode(hash, key);
                do {
		    //链表:通过.next() 循环获取
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

  

 

 

六. 常见问题

5.1 并发常见下cpu100%问题

hash并非是线程安全的,在并发场景下,错误的使用hashmap可能会出现cpu100%的问题 曾今有人在jdk1.4版本中的hashmap中提出过这样一个bug,官方也给出了答复“并非java或jvm的bug,而是使用不当”,当时所提出的地址是:jdk-6423457 : (coll) high cpu usage in hashmap.get() 左耳朵耗子前辈也做过分享:疫苗:java hashmap的死循环

5.2 concurrentmodificationexception

 

 

 

如您对本文有疑问或者有任何想说的,请点击进行留言回复,万千网友为您解惑!

相关文章:

验证码: