第一部分:HashMap

1.HashMap介绍

  • JDK7:底层实现【数组+链表(头插法)】
  • JDK8: 底层实现【数组+链表(尾插法)\红黑树】
  • JDK7版本在多线程resize时会发生死循环
  • JDK8优化:红黑树的引入避免链表过长查找耗时(链表最坏O(n)、红黑树O(logn))、扩容优化。
  • 转红黑树的判断**:链表长度大于8,先判断数组容量是否超过64,是,直接转红黑树;否则,先扩容。**
  • image.png

    1.1HashMap允许空键空值么

    HashMap最多只允许一个键为Null(多条会覆盖),但允许多个值为Null

    1.2影响HashMap性能的重要参数

    初始容量:创建哈希表(数组)时桶的数量,默认为 16,创建时为空数组,``在添加第一个元素时进行初始化容量
    负载因子:哈希表在其容量自动增加之前可以达到多满的一种尺度,默认为 0.75

    2.HashMap的存储结构

  • 底层存储数据的数组是一个哈希桶数组,明显它是一个Node的数组。链表也是基于Node结点完成

    1. static class Node<K,V> implements Map.Entry<K,V> {
    2. final int hash;
    3. final K key;
    4. V value;
    5. Node<K,V> next;
    6. Node(int hash, K key, V value, Node<K,V> next) {
    7. this.hash = hash;
    8. this.key = key;
    9. this.value = value;
    10. this.next = next;
    11. }
    12. public final K getKey() { return key; }
    13. public final V getValue() { return value; }
    14. public final String toString() { return key + "=" + value; }
    15. public final int hashCode() {
    16. return Objects.hashCode(key) ^ Objects.hashCode(value);
    17. }
    18. public final V setValue(V newValue) {
    19. V oldValue = value;
    20. value = newValue;
    21. return oldValue;
    22. }
    23. public final boolean equals(Object o) {
    24. if (o == this)
    25. return true;
    26. if (o instanceof Map.Entry) {
    27. Map.Entry<?,?> e = (Map.Entry<?,?>)o;
    28. if (Objects.equals(key, e.getKey()) &&
    29. Objects.equals(value, e.getValue()))
    30. return true;
    31. }
    32. return false;
    33. }
    34. }

    红黑树结构

    1. static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
    2. TreeNode<K,V> parent; // red-black tree links
    3. TreeNode<K,V> left;
    4. TreeNode<K,V> right;
    5. TreeNode<K,V> prev; // needed to unlink next upon deletion
    6. boolean red;
    7. TreeNode(int hash, K key, V val, Node<K,V> next) {
    8. super(hash, key, val, next);
    9. }
    10. }

    3.功能实现

    本文主要从根据key获取哈希桶数组索引位置put方法的详细执行扩容过程三个具有代表性的点进行记录

    3.1 key获取Hash索引位置

    计算hash索引的源代码如下:
    // h = key.hashCode() 为第一步 取hashCode值
    // h ^ (h >>> 16) 为第二步 高16位与低16位参与异或运算

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

    image.png

在JDK1.8的实现中,优化了高位运算的算法,通过hashCode()的高16位异或低16位实现的:(h = key.hashCode()) ^ (h >>> 16),主要是从速度、功效、质量来考虑的,这么做可以在数组table的length比较小的时候,也能保证考虑到高低Bit都参与到Hash的计算中,同时不会有太大的开销。

HashMap和ConcurrentHashMap - 图3

3.2 put方法

HashMap和ConcurrentHashMap - 图4

3.3 扩容机制

JDK8则因为巧妙的设计,性能有了大大的提升:由于数组的容量是以2的幂次方扩容的,那么一个Entity在扩容时,新的位置要么在原位置,要么在原长度+原位置的位置。原因如下图:
HashMap和ConcurrentHashMap - 图5
数组长度变为原来的2倍,表现在二进制上就是多了一个高位参与数组下标确定。此时,一个元素通过hash转换坐标的方法计算后,恰好出现一个现象:最高位是0则坐标不变,最高位是1则坐标变为“10000+原坐标”,即“原长度+原坐标”。如下图:
image.png
JDK8中的扩容,索引下标位置要么不变,要么是原来的下表+原来数组的容量。

4.为什么HashMap的底层数组长度为何总是2的n次方【经典面试题】

第一:当length为2的N次方的时候,h & (length-1) = h % length

位运算速度比取模运算速度快

第二:当length为2的N次方的时候,数据分布均匀,减少hash冲突

此时我们基于第一个原因进行分析,此时hash策略为h & (length-1)。
我们来举例当length为奇数、偶数时的情况:
HashMap和ConcurrentHashMap - 图7
HashMap和ConcurrentHashMap - 图8
从上面的图表中我们可以看到,当 length 为15时总共发生了8次碰撞,同时发现空间浪费非常大,因为在 1、3、5、7、9、11、13、15 这八处没有存放数据。
这是因为hash值在与14(即 1110)进行&运算时,得到的结果最后一位永远都是0,那么最后一位为1的位置即 0001、0011、0101、0111、1001、1011、1101、1111位置处是不可能存储数据的。这样,空间的减少会导致碰撞几率的进一步增加,从而就会导致查询速度慢。
而当length为16时,length – 1 = 15, 即 1111,那么,在进行低位&运算时,值总是与原来hash值相同,而进行高位运算时,其值等于其低位值。所以,当 length=2^n 时,不同的hash值发生碰撞的概率比较小,这样就会使得数据在table数组中分布较均匀,查询速度也较快。
如果上面这句话大家还看不明白的话,可以多试一些数,就可以发现规律。当length为奇数时,length-1为偶数,而偶数二进制的最后一位永远为0,那么与其进行 & 运算,得到的二进制数最后一位永远为0,那么结果一定是偶数,那么就会导致下标为奇数的桶永远不会放置数据,这就不符合我们均匀放置,减少冲突的要求了。

第三:数组扩容以后,所有元素的下标的索引位置要么在原来的位置,要么在【原来索引+原来数组容量】处。

5.HashMap和Hashtable的比较

  • HashMap允许键或值为null的键存在,HashTable不允许键或值为null。
  • image.png

image.png

第二部分:ConcurrentHashMap

1.JDK7版本

1.1 底层数据结构

数组+链表实现。
image.png
如图所示,是由 Segment 数组、HashEntry 组成,和 HashMap 一样,仍然是
数组加链表**
Segment 是 ConcurrentHashMap 的一个内部类,主要的组成如下:

  1. static final class Segment<K,V> extends ReentrantLock implements Serializable {
  2. private static final long serialVersionUID = 2249069246763182397L;
  3. // 和 HashMap 中的 HashEntry 作用一样,真正存放数据的桶
  4. transient volatile HashEntry<K,V>[] table;
  5. transient int count;
  6. // 记得快速失败(fail—fast)么?
  7. transient int modCount;
  8. // 大小
  9. transient int threshold;
  10. // 负载因子
  11. final float loadFactor;
  12. }

HashEntry跟HashMap差不多的,但是不同点是,他使用volatile去修饰了他的数据Value还有下一个节点next。
image.png
image.png

1.2 并发度高的原因

ConcurrentHashMap 采用了分段锁技术,其中 Segment 继承于 ReentrantLock。不会像 HashTable 那样不管是 put 还是 get 操作都需要做同步处理,理论上 ConcurrentHashMap 支持 CurrencyLevel (Segment 数组数量)的线程并发。
每当一个线程占用锁访问一个 Segment 时,不会影响到其他的 Segment。就是说如果容量大小是16他的并发度就是16,可以同时允许16个线程操作16个Segment而且还是线程安全的。

  • put方法

首先第一步的时候会尝试获取锁,如果获取失败肯定就有其他线程存在竞争,则利用 scanAndLockForPut() 自旋获取锁。

  1. 尝试自旋获取锁。
  2. 如果重试的次数达到了 MAX_SCAN_RETRIES 则改为阻塞锁获取,保证能获取成功。
  • get方法

只需要将 Key 通过 Hash 之后定位到具体的 Segment ,再通过一次 Hash 定位到具体的元素上。
由于 HashEntry 中的 value 属性是用 volatile 关键词修饰的,保证了内存可见性,所以每次获取时都是最新值。ConcurrentHashMap 的 get 方法是非常高效的,因为整个过程都不需要加锁

2.JDK8版本

2.1 底层数据结构

  • HashMap和ConcurrentHashMap - 图14
  • 加锁机制:1.8抛弃segment分段锁,而采用了 CAS + synchronized 来保证并发安全性。
  • 数组+链表\红黑树

    2.2 安全原因分析

  • put方法

    1. final V putVal(K key, V value, boolean onlyIfAbsent) {
    2. if (key == null || value == null) throw new NullPointerException();
    3. int hash = spread(key.hashCode());
    4. int binCount = 0;
    5. for (Node<K,V>[] tab = table;;) {
    6. Node<K,V> f; int n, i, fh;
    7. if (tab == null || (n = tab.length) == 0)
    8. tab = initTable();
    9. else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
    10. if (casTabAt(tab, i, null,
    11. new Node<K,V>(hash, key, value, null)))
    12. break; // no lock when adding to empty bin
    13. }
    14. else if ((fh = f.hash) == MOVED)
    15. tab = helpTransfer(tab, f);
    16. else {
    17. V oldVal = null;
    18. synchronized (f) {
    19. if (tabAt(tab, i) == f) {
    20. if (fh >= 0) {
    21. binCount = 1;
    22. for (Node<K,V> e = f;; ++binCount) {
    23. K ek;
    24. if (e.hash == hash &&
    25. ((ek = e.key) == key ||
    26. (ek != null && key.equals(ek)))) {
    27. oldVal = e.val;
    28. if (!onlyIfAbsent)
    29. e.val = value;
    30. break;
    31. }
    32. Node<K,V> pred = e;
    33. if ((e = e.next) == null) {
    34. pred.next = new Node<K,V>(hash, key,
    35. value, null);
    36. break;
    37. }
    38. }
    39. }
    40. else if (f instanceof TreeBin) {
    41. Node<K,V> p;
    42. binCount = 2;
    43. if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
    44. value)) != null) {
    45. oldVal = p.val;
    46. if (!onlyIfAbsent)
    47. p.val = value;
    48. }
    49. }
    50. }
    51. }
    52. if (binCount != 0) {
    53. if (binCount >= TREEIFY_THRESHOLD)
    54. treeifyBin(tab, i);
    55. if (oldVal != null)
    56. return oldVal;
    57. break;
    58. }
    59. }
    60. }
    61. addCount(1L, binCount);
    62. return null;
    63. }

    1.如果key或者value为null,则抛出空指针异常,和HashMap不同的是HashMap单线程是允许为Null;
    if (key == null || value == null) throw new NullPointerException();
    2.for的死循环,为了实现CAS的无锁化更新,如果table为null或者table的长度为0,则初始化table,调用initTable()方法(第一次put数据,调用默认参数实现,其中重要的sizeCtl参数)。

    1. //计算索引的第一步,传入键值的hash值
    2. int hash = spread(key.hashCode());
    3. int binCount = 0; //保存当前节点的长度
    4. for (Node<K,V>[] tab = table;;) {
    5. Node<K,V> f; int n, i, fh; K fk; V fv;
    6. if (tab == null || (n = tab.length) == 0)
    7. tab = initTable(); //初始化Hash表
    8. ...
    9. }

    3.确定元素在Hash表的索引
    通过hash算法可以将元素分散到哈希桶中。在ConcurrentHashMap中通过如下方法确定数组索引:
    第一步:

    1. static final int spread(int h) {
    2. return (h ^ (h >>> 16)) & HASH_BITS;
    3. }

    第二步:(length-1) & (h ^ (h >>> 16)) & HASH_BITS);
    4.通过tableAt()方法找到位置tab[i]Node,当Node为null时为没有hash冲突的话,使用casTabAt()方法CAS操作将元素插入到Hash表中,ConcurrentHashmap使用CAS无锁化操作,这样在高并发hash冲突低的情况下,性能良好。

    1. else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
    2. //利用CAS操作将元素插入到Hash表中
    3. if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
    4. break; // no lock when adding to empty bin(插入null的节点,无需加锁)
    5. }

    5.当f不为null时,说明发生了hash冲突,当f.hash == MOVED==-1 时,说明ConcurrentHashmap正在发生resize操作,使用helpTransfer()方法帮助正在进行resize操作。

    1. else if ((fh = f.hash) == MOVED) //f.hash == -1
    2. //hash为-1 说明是一个forwarding nodes节点,表明正在扩容
    3. tab = helpTransfer(tab, f);

    6.以上情况都不满足的时,使用synchronized同步块上锁当前节点Node,并判断有没有线程对数组进行了修改,如果没有则进行:

    • 遍历该链表并统计该链表长度binCount,查找是否有和key相同的节点,如果有则将查找到节点的val值替换为新的value值,并返回旧的value值,否则根据key,value,hash创建新Node并将其放在链表的尾部
    • 如果Node fTreeBin的类型,则使用红黑树的方式进行插入。然后则退出synchronized(f)锁住的代码块
      1. //当前节点加锁
      2. synchronized (f) {
      3. //判断下有没有线程对数组进行了修改
      4. if (tabAt(tab, i) == f) {
      5. //如果hash值是大于等于0的说明是链表
      6. if (fh >= 0) {
      7. binCount = 1;
      8. for (Node<K,V> e = f;; ++binCount) {
      9. K ek;
      10. //插入的元素键值的hash值有节点中元素的hash值相同,替换当前元素的值
      11. if (e.hash == hash &&
      12. ((ek = e.key) == key ||
      13. (ek != null && key.equals(ek)))) {
      14. oldVal = e.val;
      15. if (!onlyIfAbsent)
      16. //替换当前元素的值
      17. e.val = value;
      18. break;
      19. }
      20. Node<K,V> pred = e;
      21. //如果循环到链表结尾还没发现,那么进行插入操作
      22. if ((e = e.next) == null) {
      23. pred.next = new Node<K,V>(hash, key, value);
      24. break;
      25. }
      26. }
      27. }else if (f instanceof TreeBin) { //节点为树
      28. Node<K,V> p;
      29. binCount = 2;
      30. if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
      31. value)) != null) {
      32. oldVal = p.val;
      33. if (!onlyIfAbsent)
      34. //替换旧值
      35. p.val = value;
      36. }
      37. }
      38. else if (f instanceof ReservationNode)
      39. throw new IllegalStateException("Recursive update");
      40. }
      41. }
      7.执行完synchronized(f)同步代码块之后会先检查binCount,如果大于等于TREEIFY_THRESHOLD = 8则进行treeifyBin操作尝试将该链表转换为红黑树。
      1. if (binCount != 0) {
      2. //如果节点长度大于8,转化为树
      3. if (binCount >= TREEIFY_THRESHOLD)
      4. treeifyBin(tab, i);
      5. if (oldVal != null)
      6. return oldVal;
      7. break;
      8. }
      执行了一个addCount方法,主要用于统计数量以及决定是否需要扩容.
      1. addCount(1L, binCount);
  • get方法

    1. public V get(Object key) {
    2. Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
    3. // 计算hashcode
    4. int h = spread(key.hashCode());
    5. //在桶上直接返回
    6. if ((tab = table) != null && (n = tab.length) > 0 &&
    7. (e = tabAt(tab, (n - 1) & h)) != null) {
    8. //如果是红黑树就按照树的方式遍历
    9. if ((eh = e.hash) == h) {
    10. if ((ek = e.key) == key || (ek != null && key.equals(ek)))
    11. return e.val;
    12. }
    13. else if (eh < 0)
    14. //如果还不满足就用链表遍历
    15. return (p = e.find(h, key)) != null ? p.val : null;
    16. while ((e = e.next) != null) {
    17. if (e.hash == h &&
    18. ((ek = e.key) == key || (ek != null && key.equals(ek))))
    19. return e.val;
    20. }
    21. }
    22. return null;
    23. }

    1.根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。
    2.如果是红黑树那就按照树的方式获取值。
    3.就不满足那就按照链表的方式遍历获取值。

    其它

    CopyOnWriteArrayList

    https://www.nowcoder.com/discuss/232479

    红黑树

    https://blog.csdn.net/v_july_v/article/details/6105630