HashMap知识点记录

100 阅读7分钟

jdk8中的hashMap和jdk7中的hashMap有什么不同?

1、jdk8中新增了红黑树,在jdk8中,hashMap的底层是通过数组+链表+红黑树的方式实现的。

2、jdk7中链表的插入是头插法,jdk8中改为了尾插法。

3、jdk8中因为使用了红黑树保证了插入和查询的效率,所以实际上jdk8中的hash算法的复杂度降低了。

4、jdk8中数组扩容的条件也发生了变化,只会判断当前元素个数是否超过了阈值,而不再判断当前put进来的元素对应的数组下标位置是否有元素。

HashMap中put方法的流程?

1、通过key计算出一个hashCode。

2、通过hashCode与“与操作”计算出数组下标。

3、把put进来的key和value封装为一个entry对象。

4、判断数组下标位置是不是为空,如果为空则直接把entry对象存在改下标位置

5、如果该数组下标对应的位置不为空,则需要把entry插入到链表中

6、判断链表中是否存在相同的key,如果存在则更新value

7、如果是jdk7则使用头插法,如果是jdk8则使用尾插法

8、如果是jdk8,则会遍历链表,并且在遍历链表的过程中,统计当前链表的元素个数,如果超过8个,则先把链表转为红黑树,并且把元素插入到红黑树中

为什么HashMap在多线程下会形成环形链表

HashMap是有一个一维数组和一个链表组成,从而得知,在解决冲突问题时,hashmap选择的是链地址法。为什么HashMap会用一个数组这链表组成,当时给出的答案是从那几种解决冲突的算法中推论的,这里给出一个正面的理由:

1,为什么用了一维数组:数组存储区间是连续的,占用内存严重,故空间复杂的很大。但数组的二分查找时间复杂度小,为O(1);数组的特点是:寻址容易,插入和删除困难

2,为什么用了链表:链表存储区间离散,占用内存比较宽松,故空间复杂度很小,但时间复杂度很大,达O(N)。链表的特点是:寻址困难,插入和删除容易

而HashMap是两者的结合,用一维数组存放散列地址,以便更快速的遍历;用链表存放地址值,以便更快的插入和删除!

一 、环形链表的形成分析

那么,在HashMap中,到底是怎样形成环形链表的?这个问题,得从HashMap的resize扩容问题说起!

HashMap的扩容原理:

  1. /**

  2. * The default initial capacity - MUST be a power of two.

  3. */

  4. static final int DEFAULT_INITIAL_CAPACITY = 16;

  5. /**

  6. * The maximum capacity, used if a higher value is implicitly specified

  7. * by either of the constructors with arguments.

  8. * MUST be a power of two <= 1<<30.

  9. */

  10. static final int MAXIMUM_CAPACITY = 1 << 30;

  11. /**

  12. * The load factor used when none specified in constructor.

  13. */

  14. static final float DEFAULT_LOAD_FACTOR = 0.75f;

当HashMap中的元素个数超过数组大小(数组总大小length,不是数组中个数size)*loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,这是一个折中的取值。也就是说,默认情况下,数组大小为16,那么当HashMap中元素个数超过16*0.75=12(这个值就是代码中的threshold值,也叫做临界值)的时候,就把数组的大小扩展为 2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。

再看源码中,关于扩容resize()的实现:

  1. /**

  2. * Rehashes the contents of this map into a new array with a

  3. * larger capacity. This method is called automatically when the

  4. * number of keys in this map reaches its threshold.

  5. *

  6. * If current capacity is MAXIMUM_CAPACITY, this method does not

  7. * resize the map, but sets threshold to Integer.MAX_VALUE.

  8. * This has the effect of preventing future calls.

  9. *

  10. * @param newCapacity the new capacity, MUST be a power of two;

  11. * must be greater than current capacity unless current

  12. * capacity is MAXIMUM_CAPACITY (in which case value

  13. * is irrelevant).

  14. */

  15. void resize(int newCapacity) {

  16. Entry[] oldTable = table;

  17. int oldCapacity = oldTable.length;

  18. if (oldCapacity == MAXIMUM_CAPACITY) {

  19. threshold = Integer.MAX_VALUE;

  20. return;

  21. }

  22. Entry[] newTable = new Entry[newCapacity];

  23. boolean oldAltHashing = useAltHashing;

  24. useAltHashing |= sun.misc.VM.isBooted() &&

  25. (newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);

  26. boolean rehash = oldAltHashing ^ useAltHashing;

  27. transfer(newTable, rehash);

  28. table = newTable;

  29. threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);

  30. }

备注:请注意这句话: newCapacity the new capacity, MUST be a power of two; must be greater than current capacity unless current capacity is MAXIMUM_CAPACITY (in which case value is irrelevant)

在这里面,又调用了一个函数transfer函数:

[java] view plain copy

  1. /**
  2. * Transfers all entries from current table to newTable.
  3. */
  4. void transfer(Entry[] newTable, boolean rehash) {
  5. int newCapacity = newTable.length;
  6. for (Entry<K,V> e : table) {
  7. while(null != e) {
  8. Entry<K,V> next = e.next;
  9. if (rehash) {
  10. e.hash = null == e.key ? 0 : hash(e.key);
  11. }
  12. int i = indexFor(e.hash, newCapacity);
  13. e.next = newTable[i];
  14. newTable[i] = e;
  15. e = next;
  16. }
  17. }
  18. }

总得来说,就是拷贝旧的数据元素,从新新建一个更大容量的空间,然后进行数据复制!

那么关于环形链表的形成,则主要在这扩容的过程。当多个线程同时对这个HashMap进行put操作,而察觉到内存容量不够,需要进行扩容时,多个线程会同时执行resize操作,而这就出现问题了,问题的原因分析如下:

首先,在HashMap扩容时,会改变链表中的元素的顺序,将元素从链表头部插入。PS:说是为了避免尾部遍历,这一部分不是本博客的主要介绍内容,后面再说。

而环形链表就在这一时刻发生,以下模拟2个线程同时扩容。假设,当前hashmap的空间为2(临界值为1),hashcode分别为0和1,在散列地址0处有元素A和B,这时候要添加元素C,C经过hash运算,得到散列地址为1,这时候由于超过了临界值,空间不够,需要调用resize方法进行扩容,那么在多线程条件下,会出现条件竞争,模拟过程如下:

线程一:读取到当前的hashmap情况,在准备扩容时,线程二介入

线程二:读取hashmap,进行扩容

线程一:继续执行

这个过程为,先将A复制到新的hash表中,然后接着复制B到链头(A的前边:B.next=A),本来B.next=null,到此也就结束了(跟线程二一样的过程),但是,由于线程二扩容的原因,将B.next=A,所以,这里继续复制A,让A.next=B,由此,环形链表出现:B.next=A; A.next=B

HashMap与HashTable的不同点

1、线程安全

两者最主要的区别在于Hashtable是线程安全,而HashMap则非线程安全。

Hashtable的实现方法里面都添加了synchronized关键字来确保线程同步,因此相对而言HashMap性能会高一些,我们平时使用时若无特殊需求建议使用HashMap,在多线程环境下若使用HashMap需要使用Collections.synchronizedMap()方法来获取一个线程安全的集合。

Note:

Collections.synchronizedMap()实现原理是Collections定义了一个SynchronizedMap的内部类,这个类实现了Map接口,在调用方法时使用synchronized来保证线程同步,当然了实际上操作的还是我们传入的HashMap实例,简单的说就是Collections.synchronizedMap()方法帮我们在操作HashMap时自动添加了synchronized来实现线程同步,类似的其它Collections.synchronizedXX方法也是类似原理。

2、针对null的不同

HashMap可以使用null作为key,而Hashtable则不允许null作为key

虽说HashMap支持null值作为key,不过建议还是尽量避免这样使用,因为一旦不小心使用了,若因此引发一些问题,排查起来很是费事。

Note:HashMap以null作为key时,总是存储在table数组的第一个节点上。

3、继承结构

HashMap是对Map接口的实现,HashTable实现了Map接口和Dictionary抽象类。

4、初始容量与扩容

HashMap的初始容量为16,Hashtable初始容量为11,两者的填充因子默认都是0.75。

HashMap扩容时是当前容量翻倍即:capacity*2,Hashtable扩容时是容量翻倍+1即:capacity*2+1。

5、两者计算hash的方法不同

Hashtable计算hash是直接使用key的hashcode对table数组的长度直接进行取模

int hash = key.hashCode();

int index = (hash & 0x7FFFFFFF) % tab.length;

HashMap计算hash对key的hashcode进行了二次hash,以获得更好的散列值,然后对table数组长度取摸。

int hash = hash(key.hashCode());

int i = indexFor(hash, table.length);

static int hash(int h) {

// This function ensures that hashCodes that differ only by

// constant multiples at each bit position have a bounded

// number of collisions (approximately 8 at default load factor).

h ^= (h >>> 20) ^ (h >>> 12);

return h ^ (h >>> 7) ^ (h >>> 4);

}