hashMap1.7和1.8的实现原理以及扩容机制

1 hashMap1.7底层实现原理及扩容

   一、数据结构

hashMap1.7和1.8的实现原理以及扩容机制_第1张图片
二 hash算法
hash算法,不是简单的对key值进hash运算,而是是下面这个方法,是先将key进行hashcode后的一个hash值与数组长度-1后进行与运算得到的值,为啥这样操作:
进行与使不同的hash值发生的碰撞的概率比较小。这样会使得数据在table数组中分布较均匀,查询速度也快
我们可以看到在HashMap中要找到某个元素,需要根据key的hash值来求得对应数组中的位置。如何计算这个位置就是hash算法。前面说过HashMap的数据结构是数组和链表的结合,所以我们当然希望这个HashMap里面的元素位置尽量的分布均匀些,尽量使得每个位置上的元素数量只有一个,那么当我们用hash算法求得这个位置的时候,马上就可以知道对应位置的元素就是我们要的,而不用再去遍历链表。

源码分析:

/**

  • Returns index for hash code h.
    */
    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);
    }
    三 扩容
    put 方法
public V put(K key, V value) {
    //判断当前Hashmap(底层是Entry数组)是否存值(是否为空数组)
    if (table == EMPTY_TABLE) {
      inflateTable(threshold);//如果为空,则初始化
    }
    
    //判断key是否为空
    if (key == null)
      return putForNullKey(value);//hashmap允许key为空
    
    //计算当前key的哈希值    
    int hash = hash(key);
    //通过哈希值和当前数据长度,算出当前key值对应在数组中的存放位置
    int i = indexFor(hash, table.length);
    for (Entry e = table[i]; e != null; e = e.next) {
      Object k;
      //如果计算的哈希位置有值(及hash冲突),且key值一样,则覆盖原值value,并返回原值value
      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;
  }

具体put是否扩容需要两个条件
1、 存放新值的时候当前已有元素的个数必须大于等于阈值
2、 存放新值的时候当前存放数据发生hash碰撞(当前key计算的hash值换算出来的数组下标位置已经存在值)
扩容方法是在addEntry方法中

void addEntry(int hash, K key, V value, int bucketIndex) {
    //1、判断当前个数是否大于等于阈值
    //2、当前存放是否发生哈希碰撞
    //如果上面两个条件否发生,那么就扩容
    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);
  }

扩容过程中会将原来的数据,放入到新的数组中,但是会重新计算hash值进行分配

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()方法把原数组中的值放到新数组中
    transfer(newTable, initHashSeedAsNeeded(newCapacity));
    //设置hashmap扩容后为新的数组引用
    table = newTable;
    //设置hashmap扩容新的阈值
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
  }
 

transfer()在实际扩容时候把原来数组中的元素放入新的数组中

void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    for (Entry e : table) {
      while(null != e) {
        Entry next = e.next;
        if (rehash) {
          e.hash = null == e.key ? 0 : hash(e.key);
        }
        //通过key值的hash值和新数组的大小算出在当前数组中的存放位置
        int i = indexFor(e.hash, newCapacity);
        e.next = newTable[i];
        newTable[i] = e;
        e = next;
      }
    }
  }

扩容问题:
数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这个操作是极其消耗性能的。所以如果我们已经预知HashMap中元素的个数,那么预设初始容量能够有效的提高HashMap的性能。

重新调整HashMap大小,当多线程的情况下可能产生条件竞争。因为如果两个线程都发现HashMap需要重新调整大小了,它们会同时试着调整大小。在调整大小的过程中,存储在链表中的元素的次序会反过来,因为移动到新的bucket位置的时候,HashMap并不会将元素放在链表的尾部,而是放在头部,这是为了避免尾部遍历(tail traversing)。如果条件竞争发生了,那么就死循环了。
四 线程安全
HashMap是线程不安全的,在多线程情况下直接使用HashMap会出现一些莫名其妙不可预知的问题。在多线程下使用HashMap,有几种方案:

A.在外部包装HashMap,实现同步机制

B.使用Map m = Collections.synchronizedMap(new HashMap(…));实现同步(官方参考方案,但不建议使用,使用迭代器遍历的时候修改映射结构容易出错)

D.使用java.util.HashTable,效率最低(几乎被淘汰了)

E.使用java.util.concurrent.ConcurrentHashMap,相对安全,效率高(建议使用)

注意一个小问题,HashMap所有集合类视图所返回迭代器都是快速失败的(fail-fast),在迭代器创建之后,如果从结构上对映射进行修改,除非通过迭代器自身的 remove 或 add 方法,其他任何时间任何方式的修改,迭代器都将抛出 ConcurrentModificationException。。因此,面对并发的修改,迭代器很快就会完全失败。

2 hashMap1.8底层实现原理及扩容

一数据结构
hashMap1.7和1.8的实现原理以及扩容机制_第2张图片
2扩容-
主要是hash值的计算以及树的转换,1.8相对1.7进行了大量优化,性能提高很多
源码

 /**
     * Initializes or doubles table size.  If null, allocates in
     * accord with initial capacity target held in field threshold.
     * Otherwise, because we are using power-of-two expansion, the
     * elements from each bin must either stay at same index, or move
     * with a power of two offset in the new table.
     *  @return the table
     *
     *
     *
     * 初始化或者翻倍表大小。
     * 如果表为null,则根据存放在threshold变量中的初始化capacity的值来分配table内存
     * (这个注释说的很清楚,在实例化HashMap时,capacity其实是存放在了成员变量threshold中,
     * 注意,HashMap中没有capacity这个成员变量)
     * 。如果表不为null,由于我们使用2的幂来扩容,
     * 则每个bin元素要么还是在原来的bucket中,要么在2的幂中
     *
     * 此方法功能:初始化或扩容
     */
    final Node[] resize() {
        Node[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        //新的容量值,新的扩容阀界值
        int newCap, newThr = 0;
        //oldTab!=null,则oldCap>0
        if (oldCap > 0) {
            //如果此时oldCap>=MAXIMUM_CAPACITY(1 << 30),表示已经到了最大容量,这时还要往map中放数据,则阈值设置为整数的最大值 Integer.MAX_VALUE,直接返回这个oldTab的内存地址。
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //如果(当前容量*2<最大容量&&当前容量>=默认初始化容量(16))
            //并将将原容量值<<1(相当于*2)赋值给 newCap
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                //如果能进来证明此map是扩容而不是初始化
                //操作:将原扩容阀界值<<1(相当于*2)赋值给 newThr
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            //进入此if证明创建map时用的带参构造:public HashMap(int initialCapacity)或 public HashMap(int initialCapacity, float loadFactor)
            //注:带参的构造中initialCapacity(初始容量值)不管是输入几都会通过 “this.threshold = tableSizeFor(initialCapacity);”此方法计算出接近initialCapacity参数的2^n来作为初始化容量(初始化容量==oldThr)
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            //进入此if证明创建map时用的无参构造:
            //然后将参数newCap(新的容量)、newThr(新的扩容阀界值)进行初始化
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {
 
            //进入此if有两种可能
            // 第一种:进入此“if (oldCap > 0)”中且不满足该if中的两个if
            // 第二种:进入这个“else if (oldThr > 0)”
 
            //分析:进入此if证明该map在创建时用的带参构造,如果是第一种情况就说明是进行扩容且oldCap(旧容量)小于16,如果是第二种说明是第一次put
            float ft = (float)newCap * loadFactor;
            //计算扩容阀界值
            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;
        //如果“oldTab != null”说明是扩容,否则直接返回newTab
        if (oldTab != null) {
            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的实例
                        ((TreeNode)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        Node loHead = null, loTail = null;//此对象接收会放在原来位置
                        Node hiHead = null, hiTail = null;//此对象接收会放在“j + oldCap”(当前位置索引+原容量的值)
                        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;
    }

详情可以参考博客 https://www.cnblogs.com/shianliang/p/9204942.html

3 ConcurrentHashMap 1.7与1.8的锁区别,以及数据结构区别
JDK1.7版本的ReentrantLock+Segment+HashEntry,到JDK1.8版本中synchronized+CAS+HashEntry+红黑树

1.7时候put

Segment的继承体系可以看出,Segment实现了ReentrantLock,也就带有锁的功能,当执行put操作时,会进行第一次key的hash来定位Segment的位置,如果该Segment还没有初始化,即通过CAS操作进行赋值,然后进行第二次hash操作,找到相应的HashEntry的位置,这里会利用继承过来的锁的特性,在将数据插入指定的HashEntry位置时(链表的尾端),会通过继承ReentrantLock的tryLock()方法尝试去获取锁,如果获取成功就直接插入相应的位置,如果已经有线程获取该Segment的锁,那当前线程会以自旋的方式去继续的调用tryLock()方法去获取锁,超过指定次数就挂起,等待唤醒

1.8时候put

1)如果没有初始化就先调用initTable()方法来进行初始化过程
2)如果没有hash冲突就直接CAS插入
3如果还在进行扩容操作就先进行扩容
4)如果存在hash冲突,就加锁来保证线程安全,这里有两种情况,一种是链表形式就直接遍历到尾端插入,一种是红黑树就按照红黑树结构插入,
5)最后一个如果Hash冲突时会形成Node链表,在链表长度超过8,Node数组超过64时会将链表结构转换为红黑树的结构,break再一次进入循环
6)如果添加成功就调用addCount()方法统计size,并且检查是否需要扩容

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