欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

HashMap深度分析(基于jdk1.8)

程序员文章站 2022-06-04 19:23:16
...

 

HashMap 的简介

  • Hashmap是一个散列桶(bucket ),存储的一对一对的数据,形式是:key---value。
  • Hashmap采用的是数组和链表的数据结构,能在查询和修改方便继承了数组的线性查找和链表的寻址修改。
  • HashMap 线程不安全,所以 HashMap 很快。
  • HashMap 可以接受 null 键和值,而 Hashtable 则不能。

HashMap 的工作原理

在使用put(key,value)方法向map中插入值得时候,会优先调用hash()方法,参数为key,意思就是先获取key得hashcode,计算需要将该key放入的位置。这里关键点在于指出,HashMap 是在 bucket 中储存键对象和值对象,作为Map.Node 。下面可以了解一下Hashmap的实现:

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;

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

        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

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

解释:Node是Hashmap类的一个静态内部类,实现了Map.Entry<K,V>接口。里面存放的key,value,hash这几个参数共同组成了hashmap的数组需要的参数,而next是组成链表的重要成员。他们共同组成了NODE这个对象,NODE对象共同组成了Hashmap 的主要结构。

HashMap 的put方法分析

  1、先对key进行hash计算,然后计算存放的位置。如下源码:

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

 

  2、如果位置没有冲突,那么直接放入到bucket之中。

  3、如果位置有冲突,那么需要调用equals方法(hashcode一样,key内容不一定相同),如果equals返回的 值为false,那么需要放到这个位置的链表之中。

  4、当链表的长度大于8的时候,那么链表会转为红黑树的数据结构。如下源码:

if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // TREEIFY_THRESHOLD 默认为8
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;

  5、当链表的长度小于6的时候,那么红黑树会转为链表的数据结构。如下源码:

if (lc <= UNTREEIFY_THRESHOLD) // UNTREEIFY_THRESHOLD默认为6
                    tab[index] = loHead.untreeify(map);
                else {
                    tab[index] = loHead;
                    if (hiHead != null) // (else is already treeified)
                        loHead.treeify(tab);
                }

   6、如果hash计算后的值已经存在,那么需要调用equals方法(hashcode一样,key内容不一定相同),如果equals返回的 值为true,那么覆盖原有的value。

   7、如果桶满了(容量16*加载因子0.75),就需要 resize(扩容2倍后重排)

HashMap 的get方法分析

  1、调用get方法的时候,也会先调用hash()方法,获取bucket 位置。如下源码:

 public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

  2、获取bucket位置之后,需要调用equals方法(hashcode一样,key内容不一定相同)去匹配需要查找的key,然后获取对应的value。

HashMap 中 hash 函数分析

前面说过,hashmap 的数据结构是数组和链表的结合,所以我们当然希望这个 hashmap 里面的元素位置尽量的分布均匀些,尽量使得每个位置上的元素数量只有一个。那么当我们用 hash 算法求得这个位置的时候,马上就可以知道对应位置的元素就是我们要的,而不用再去遍历链表。 所以,我们首先想到的就是把 hashcode 对数组长度取模运算。这样一来,元素的分布相对来说是比较均匀的。如下源码:

static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

  解释:^ :按位异或。>>>:无符号右移,忽略符号位,空位都以0补齐

 

  为什么使用红黑树

 之所以选择红黑树,是为了解决二叉树在某一些特殊情况下会变成线性二叉树的时候,那样反而导致查询的层次很深,这样的查询会和直接的链表查询没有区别。而红黑树可以通过左旋、右旋、变色这些操作来保持平衡。引入红黑树就是为了查询数据快,解决链表查询深度的问题。我们知道红黑树属于平衡二叉树,为了保持“平衡”是需要付出代价的,但是该代价所损耗的资源要比遍历线性链表要少。所以当长度大于8的时候,会使用红黑树;如果链表长度很短的话,根本不需要引入红黑树,引入反而会慢。

 

红黑树的特征

  1. 节点是红色或者黑色。
  2. 根节点是黑色。
  3. 红色节点的子节点是黑色。
  4. 每个子节点是黑色。
  5. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。

HashMap的resize分析

HashMap 默认的负载因子大小为0.75。也就是说,当一个 Map 填满了75%的 bucket 时候,和其它集合类一样(如 ArrayList 等),将会创建原来 HashMap 大小的两倍的 bucket 数组来重新调整 Map 大小,并将原来的对象放入新的 bucket 数组中。

重新调整 HashMap 大小存在的问题

在重新调整hashmap的时候,会存在一定的问题
在多个线程都检测到hashmap需要扩容的时候,那么这些线程就会去试着扩容。在扩容的过程中,存储在链表中的元素次序会反过来。因为移动到新的bucket的时候,hashmap并不会将元素放在尾部,而是放在头部。如果竞争发生了,那么就会出现死循环。

CocurrentHashMap浅析

在concurrenhashmap抛弃了原有的segment分段锁,采用了cas+synchronized保证并发安全。最大特点是引入了 CAS.借助 Unsafe 来实现 native code。CAS有3个操作数,内存值 V、旧的预期值 A、要修改的新值 B。当且仅当预期值 A 和内存值 V 相同时,将内存值V修改为 B,否则什么都不做。Unsafe 借助 CPU 指令 cmpxchg 来实现。如下源码:

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        volatile V val;
        volatile Node<K,V> next;

        Node(int hash, K key, V val, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.val = val;
            this.next = next;
        }

        public final K getKey()       { return key; }
        public final V getValue()     { return val; }
        public final int hashCode()   { return key.hashCode() ^ val.hashCode(); }
        public final String toString(){ return key + "=" + val; }
        public final V setValue(V value) {
            throw new UnsupportedOperationException();
        }

        public final boolean equals(Object o) {
            Object k, v, u; Map.Entry<?,?> e;
            return ((o instanceof Map.Entry) &&
                    (k = (e = (Map.Entry<?,?>)o).getKey()) != null &&
                    (v = e.getValue()) != null &&
                    (k == key || k.equals(key)) &&
                    (v == (u = val) || v.equals(u)));
        }

        /**
         * Virtualized support for map.get(); overridden in subclasses.
         */
        Node<K,V> find(int h, Object k) {
            Node<K,V> e = this;
            if (k != null) {
                do {
                    K ek;
                    if (e.hash == h &&
                        ((ek = e.key) == k || (ek != null && k.equals(ek))))
                        return e;
                } while ((e = e.next) != null);
            }
            return null;
        }
    }

put 过程

  1. 取出key的hashcode。
  2. 判断是否需要进行初始化。
  3. 通过key定位出node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。
  4. 如果当前位置的 hashcode == MOVED == -1,则需要进行扩容。
  5. 如果都不满足,则利用 synchronized 锁写入数据。
  6. 如果数量大于 TREEIFY_THRESHOLD 则要转换为红黑树。

源码如下:

public V put(K key, V value) {
        return putVal(key, value, false);
}
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;
    }

get 过程

  1. 获取key的hashcode来寻址,如果就在桶上,就直接返回。
  2. 如果是红黑树,就按照树的方式获取值。
  3. 不满足就按照链表的方式获取值。

源码如下:

public V get(Object key) {
        Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
        int h = spread(key.hashCode());
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (e = tabAt(tab, (n - 1) & h)) != null) {
            if ((eh = e.hash) == h) {
                if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                    return e.val;
            }
            else if (eh < 0)
                return (p = e.find(h, key)) != null ? p.val : null;
            while ((e = e.next) != null) {
                if (e.hash == h &&
                    ((ek = e.key) == key || (ek != null && key.equals(ek))))
                    return e.val;
            }
        }
        return null;
    }
相关标签: Hashmap