HashMap底层原理

HashMap底层原理

参考视频Java 7/8 HashMap源码详解
​ 近期由于学校的生产实习,有一段时间没有更新博客了。懈怠了,不过在这次实习中也学到了不少 , 下次博客将其中遇到的一些难题和知识点总结总结。我的博客会同步到http://www.czpcoding.club/这个是我根据solo搭建的一个个人博客,有兴趣的话可以去看看。话不多说,进入今天的主题:

HashMap是我们在工作中经常会用到的一个数据类型,那么它到底是如何实现的呢?

让我们先来探究一下它的数据结构

哈希表

  • 核心是基于哈希值的桶和链表
  • O(1)的平均查找,插入,删除时间
  • 指明的缺陷是哈希值的碰撞

在JDK1.7及其以前,hashMap是由数组+链表构成,而在JDK1.8以后采用了数组+链表+红黑树

java7

HashMap在创建时,并不会直接将桶开辟出来,懒加载

在创建hashMap时可以传入初始的桶的数量,但是桶的数量必须是2的整数次幂,如果不是在第一次被加载时被调整到临近向上取整的2次幂

  public HashMap(int initialCapacity) {
     
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

问题: 为什么数组大小一定要是2的幂?

//hashMap put方法
public V put(K key, V value) {
     
        if (table == EMPTY_TABLE) {
     
            inflateTable(threshold);
        }
        if (key == null)
            return putForNullKey(value);
    	//在获取到键的hash值之后
        int hash = hash(key);
    	//通过indexFor方法获取要存放的数组下标
        int i = indexFor(hash, table.length);
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
     
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
     
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }

//indexFor方法
static int indexFor(int h, int length) {
     
        // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
        return h & (length-1);
}
//如果数组是2的n幂 使用length-1 可以得到一个全为1的二进制数,然后和hash进行或运算可以快速获得数组下标
//如果采用模运算 %, haah如果是负数则模为负值,切运算过慢
//addEntry方法, 如果存放的键值对超过阈值,就需要进行扩容和rehash
  void addEntry(int hash, K key, V value, int bucketIndex) {
     
        if ((size >= threshold) && (null != table[bucketIndex])) {
     
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }

        createEntry(hash, key, value, bucketIndex);
    }

//没有超过阈值添加entry
  void createEntry(int hash, K key, V value, int bucketIndex) {
     
        Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
  }
//而entry的构造方法如下, 由此可见jdk1.7采用的是头插法
 Entry(int h, K k, V v, Entry<K,V> n) {
     
            value = v;
            next = n;
            key = k;
            hash = h;
 }

//如果超过阈值调用resize方法
 void resize(int newCapacity) {
     
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
     
            threshold = Integer.MAX_VALUE;
            return;
        }

        Entry[] newTable = new Entry[newCapacity];
        transfer(newTable, initHashSeedAsNeeded(newCapacity));
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

//resize调用transfer方法
void transfer(Entry[] newTable, boolean rehash) {
     
        int newCapacity = newTable.length;
        for (Entry<K,V> e : table) {
     
            while(null != e) {
     
                Entry<K,V> next = e.next;
                if (rehash) {
     
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
}

java7中的问题

  • 容易出现死锁

    先来看map是如何取值的

    //从这里可以看出主要是通过getEntry获取值
    public V get(Object key) {
           
            if (key == null)
                return getForNullKey();
            Entry<K,V> entry = getEntry(key);
    
            return null == entry ? null : entry.getValue();
    }
    //先通过key获取其hash值然后调用indexfor方法获取桶的序号,遍历桶获取
    final Entry<K,V> getEntry(Object key) {
           
            if (size == 0) {
           
                return null;
            }
    
            int hash = (key == null) ? 0 : hash(key);
            for (Entry<K,V> e = table[indexFor(hash, table.length)];
                 
                 e != null;
                 e = e.next) {
           
                Object k;
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            }
            return null;
    }
    
    • 怎样会出现死锁?(借鉴 coolshell hashmap)

      • 假设有两个线程在操作同一个map,在某一时刻一个线程(线程一)在获取调用到getEntry()方法时被挂起了,此时另一个线程(线程二)将map进行扩容了(resize),此时就会重新进行hash和计算k-v存放的位置(由于采用的是头插法)当线程二将下图中的key:7 插入到桶中时(此时,线程一被唤醒 key3的next指向的是key:7 而key:7的next指向的却是key:3 此时就会出现死循环 ),产生死锁问题

HashMap底层原理_第1张图片

  • 潜在的安全隐患

    • CVE-2011-4858(可以通过精心构造的恶意请求引发Dos)
      • ApacheTomcat5.5.35之前版本,6.0.35之前的6.x版本以及7.0.23之前的7.x版本中存在漏洞,该漏洞源于在没有限制触发预测哈希冲突的情况下为形式参数计算哈希值。远程攻击者可以通过发送多个特制参数导致拒绝服务。
    • Tomcat邮件组的讨论

Java8 hashMap的改进

  • 数据结构: 数组+链表+红黑树
  • 扩容时插入顺序的改进
  • 函数方法
    • forEach
    • compute系列
  • Map新的API
    • merge
    • replace

改进方法

  • hash算法
//从这里可以看出hash的方法实现十分简单(意思是将哈市的高16位和低16位相异或得出一个结果) int能存放32位二进制数 这样做的原因是将hash的高16位也参与进来,进行运算 
//为什么使用的是异或而不是与和或呢?
//与和或都会让结果都偏向0或者偏向1,而异或相同为0 相异为真,比较均匀
static final int hash(Object key) {
     
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
  • 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为原来的链表,通过和原来的大小进行与操作(0和1想与为0,为0说明扩容所在的2进制位为0)
                            //实现了尾插法,没有改变其顺序
                            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;
}

你可能感兴趣的:(java,java)