简单的理解一下 HashMap 的源码

一、数据结构

基于哈希表的 Map 接口实现,是 Java 中常用的集合类框架,也是非常典型的数据结构。HashMap 的底层实现主要是数组和链表。

数组的特点:存储区间是连续的,占用内存严重,空间复杂也很大,时间复杂为O(1)。优点:随机读取效率很高,随机访问性强,查找速度快。缺点:插入和删除数据效率低,因插入数据,这个位置后面的数据在内存中要往后移的,且大小固定不易动态扩展。

链表的特点:区间离散,占用内存宽松,空间复杂度小,时间复杂度为O(N)。优点:插入删除速度快,内存利用率高,没有大小固定,扩展灵活。缺点:查询效率低,不能随机查找,每次都是从第一个开始遍历。

二、源码浅析

1. HashMap的节点

HashMap 底层是 hash 数组和链表实现的,数组中的每个元素都是链表,链表由内部类 Node 实现 Map.Entry<K, V>接口实现。

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

    Node(int hash, K key, V value, HashMap.Node<K,V> next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }
    /* ······ */
}

2. HashMap的数据结构

在 JDK 1.7 的版本中,HashMap 使用的是 数组 + 链表 的形式实现。在 JDK 1.8 的版本里,HashMap 使用 数组 + 链表 + 红黑树 的形式实现。

数组的特点是查询效率高,但插入和删除的效率低。链表的特点是查询效率低,但插入和删除的效率高。HashMap 将数组和链表融合,使得查询、插入和删除的效率都很高。

图片

在 JDK 1.8 开始的版本,HashMap 引入了红黑树,进一步提高了查询的速度。

3. HashMap的几个基础变量

★DEFAULT_INITIAL_CAPACITY

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 

本变量为 HashMap 的初始容量,HashMap 的初始容量为 16 ,在构造 HashMap 时如果不人工干预构造参数,则默认创建一个该容量大小的 HashMap 。

MAXIMUM_CAPACITY

static final int MAXIMUM_CAPACITY = 1 << 30;

本变量为 HashMap 的最大容量,哪怕是构造 HashMap 时人工设置参数超过该值,也只会创建该大小的 HashMap 。本值为 1073741824 ,并非 int 类型最大值。

★DEFAULT_LOAD_FACTOR

static final float DEFAULT_LOAD_FACTOR = 0.75f;

本变量为 HashMap 的负载因子,默认为 0.75 。意思是如果该 HashMap 包含的元素超过 当前容量 * 负载因子 时,要进行扩容处理。

例如:当前容量为 16 ,负载因子为 0.75 ,当存放了 16*0.75 = 12 个元素时,便会进行一次扩容。

TREEIFY_THRESHOLD

static final int TREEIFY_THRESHOLD = 8;

链表转化为红黑树的阈值。当 HashMap 的某个桶位的链表长度达到这个数时,会转化成红黑树的形式储存(转化红黑树的因素并不只有该变量控制,有时即便长度达到8,也不会转化为红黑树的形式。详细见后文有关扩容的机制。

之所以该变量的值默认为8,是考虑到 hash碰撞 的问题。在 hash函数 设计合理的情况下,发生 hash碰撞8次 的几率为 百万分之六,概率非常低。也可以设置为更高的数,但是设置为8已经足够使用了。

除此之外,在 HashMap 的源码中也通过数学分析泊松分布给出了为什么值为8的原因。

Because TreeNodes are about twice the size of regular nodes, we use them only when bins contain enough nodes to warrant use (see TREEIFY_THRESHOLD). And when they become too small (due to removal or resizing) they are converted back to plain bins. In usages with well-distributed user hashCodes, tree bins are rarely used. Ideally, under random hashCodes, the frequency of nodes in bins follows a Poisson distribution (http://en.wikipedia.org/wiki/Poisson_distribution) with a parameter of about 0.5 on average for the default resizing threshold of 0.75, although with a large variance because of resizing granularity. Ignoring variance, the expected occurrences of list size k are (exp(-0.5) pow(0.5, k) / factorial(k)). The first values are:

0: 0.60653066
1: 0.30326533
2: 0.07581633
3: 0.01263606
4: 0.00157952
5: 0.00015795
6: 0.00001316
7: 0.00000094
8: 0.00000006
more: less than 1 in ten million

UNTREEIFY_THRESHOLD

static final int UNTREEIFY_THRESHOLD = 6;

红黑树转化为链表的阈值。并没有过多的讲究,主要是因为如果一个桶位的“链表长度”总是在8的范围徘徊,就会不断的产生 链表->红黑树,红黑树->链表 的过程。而这个过程本就是需要消耗时间的,于是便设计为了比8和7更小的数6。

MIN_TREEIFY_CAPACITY

static final int MIN_TREEIFY_CAPACITY = 64;

由变量名不难看出,该变量也是和 红黑树 有关的变量,前文提到当桶位的链表长度达到 TREEIFY_THRESHOLD 的值 8时,会转化为红黑树的形式储存,而实际上在转化红黑树之前还要判断一下是否达到了这个"最小转化成红黑树的容量",如果 HashMap 的容量没有达到 MIN_TREEIFY_CAPACITY 的值(也就是64),那么会优先对 HashMap 进行扩容处理。详细见后文有关扩容的机制。

4. HashMap的 put()

put() 方法的简单描述:首先使用哈希函数获取 key 的 hash 值,然后计算对应的数组下标。如果没有哈希冲突则直接把 key 和 value 以节点的形式放入对应桶位,如果出现哈希冲突则以链表的形式插入到链表后面。在插入之前要判断桶位中链表各个节点的 key ,先判断 hash 后判断 equals ,如果目标 key 与传入 key 相同,则不是插入新的节点而是把旧的节点 key 对应的 value 更换成新的 value 。插入链表之后对链表长度进行判断,如果链表长度超过 TREEIFY_THRESHOLD 则尝试将链表转化为红黑树,在转化红黑树之前还需判断 HashMap 的容量是否达到了 MIN_TREEIFY_CAPACITY ,如果没有达到则优先对HashMap扩容,反之则转化红黑树。在这之后再次判断 HashMap 包含的元素数量,如果达到了负载因子的要求,则需要执行扩容的操作。

下面对 put() 方法的具体流程进行分析:

我们在使用 HashMap 进行简单插入时,往往会使用这么一行代码

//HashMap<Integer,Integer> hashMap = new HashMap<>();
hashMap.put(1,1);

put(k, v) 的实现是调用 putVal() 方法实现的

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

所以接下来利用 putVal() 的执行流程来解读插入流程。

首先先放一张图,简易的展示了 putVal() 的流程,为了方便绘制,图中用”hm”表示“HashMap"

在这里插入图片描述

接下来结合源码描述流程:

// HashMap 的实现是个数组
transient Node<K,V>[] table;
//用来处理传入的数据和当前存在的数据
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict){
    Node<K,V>[] tab; Node<K,V> p; int n, i;
}
  1. 首先判断 HashMap 是否为空,如果为空则需要初始化。

    if ((tab = table) == null || (n = tab.length) == 0)
                n = (tab = resize()).length;
    
  2. 根据传入的 key 计算 hash 数值,然后进一步计算 (n - 1) & hash 得出下标位置,并进行操作,如果下标位置为空,则直接赋值。

    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    
  3. 如果下标位置不为空,则需要考虑多种情况

  4. 先创建临时变量用来存放数据

    Node<K,V> e; K k;
    
  5. 既然下标位置不为空,则先判断下标位置的 key 是否与我们传入的 key 相同。为了判断更迅速,首先判断 hash 值再进行equals判断。如果相同的话,则直接覆盖掉原来的value即可。

    if (p.hash == hash &&
        ((k = p.key) == key || (key != null && key.equals(k))))
        e = p;
    
  6. 如果不同的话,则判断一下下标位置存储的是链表还是树,源码里是判断是否为树,如果为树则执行树有关的操作。

    else if (p instanceof TreeNode)
        e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
    
  7. 反之就是链表存储,那就直接遍历到末端然后添加新的节点即可。添加完节点要判断一下是否需要转化生成树,

    else {
        for (int binCount = 0; ; ++binCount) {
            // 遍历到末端节点才会执行
            if ((e = p.next) == null) {
                // 末端添加节点
                p.next = newNode(hash, key, value, null);
                // 判断是否需要转化成树
                if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                    treeifyBin(tab, hash);
                break;
            }
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                break;
            p = e;
        }
    }
    
  8. 最后让 HashMap 的元素个数+1,并判断是否需要扩容。

    if (++size > threshold)
        resize();
    

    putVal() 的完整代码

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
    

    5. HashMap的 get()

    HashMap 的 get() 方法就远远简单于 put() 方法了。

    在源码里,get() 的实现是这样的:

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

    直接定义一个 node ,调用 getNode() 方法查值,查到了返回 value,没查到返回 null 即可。

    final Node<K,V> getNode(int hash, Object key) {
        // 预定义变量
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        // HashMap 不为空 且 key 对应的桶位不为空
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            // 先判断桶位的头结点
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            // 头结点不是目标节点,则尝试遍历该桶位所有节点
            if ((e = first.next) != null) {
                // 判断是否为树形结构,如果是树形结构则调用树形结构遍历方法
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                // 使用 do-while 循环遍历链表获得目标节点并返回
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
             } while ((e = e.next) != null);
            }
        }
        return null;
    }
    

6. hash相关的问题

6.1 HashMap的hash方法是如何实现的

先上源码

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

由源码不难看出,是通过对传入值 key 取 hashcode值,然后对 hashcode 的高16位和低16位进行异或运算。

原因:一个好的hash函数应当能够尽可能的将数值分散开,尽可能避免hash碰撞。让高低位16进行异或运算,则一旦目标值发生改变,hashcode随之改变,高低位16也会发生变化,此时的异或结果也就会发生改变。另一个原因是hash方法属于高频调用的方法,采用位运算能够最大化提高运行效率。。

6.2 hash函数为什么不直接用key的hashcode

key的 hashcode 是直接调用的 key 对象的 hashcode 方法,这个方法来自于 Object ,而 Object 的 hashcode 方法会返回int类型的 hash 值,范围自然是 int 的范围:-2147483648~2147483647,这个范围先后约有42亿的映射空间,而这个映射空间是需要存放到内存中的,显然42亿的长度在内存中是放不下的。在加上我们每次尝试访问桶位的时候都会执行 tab[(n - 1) & hash]) ,其中 n 为数组长度,那么对应的计算量也是非常庞大的。

6.3 两个key的hashcode相同会发生什么

正如第4节所说,我们在调用 putVal() 时,会尝试匹配链表中的 key 和传入的参数 key ,如果两个 key 的 hashcode 相同,则说明发生了 hash 碰撞。

发生hash碰撞后,我们会调用 equals() 方法对两个 key 进行进一步的判断,如果结果相同,则说明两个 key 相同,直接覆盖旧的 value ,反之则会遍历到链表末端,将 key 和 value 以新的 node 添加到结尾。(此处不在叙述后续的红黑树以及扩容处理)

6.4 为什么使用异或运算

只要目标值的 hashcode 中有1位数字发生改变,则 hash 结果都会发生改变,尽可能避免 hash 碰撞。

6.5 为什么 与运算 中与的都是 n-1

先来看有关的 hash 操作:(n - 1) & hash,其中 n 指代为 length

7. HashMap的容量?负载因子?怎么发生变化?

在讨论 HashMap 的容量之前,不妨先看看 HashMap 的构造方法。

首先是最根本的构造方法,其他的构造方法基本都是建立在此之上的。

// 传入一个初始容量参数 initialCapacity ,和负载因子 loadFactor
public HashMap(int initialCapacity, float loadFactor) {
    // 初始容量的合理性检验,肯定是不能为负数的
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    // 与第3节提到的 MAXIMUM_CAPACITY 属性有关,MAXIMUM_CAPACITY决定了 HashMap 的最大容量
    // 哪怕我们人为设置初始容量极大,也会在这一步被削减成 MAXIMUM_CAPACITY 的大小。
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    // 负载因子的合理性检验
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    // 配置负载因子
    this.loadFactor = loadFactor;
    // 设置 threshold 的数值,这个数值在 put 方法中最后一块有使用到,如果包含元素超过这个数值,则会扩容
    // tableSizeFor() 方法,会计算出大于等于传入值的最小的2的n次幂
    // 比如传入3,那么就会算出2的2次方4,传入5则返回8,传入10则返回16 ... 以此类推
    this.threshold = tableSizeFor(initialCapacity);
}

// 附 tableSizeFor() 源码参考
static final int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

我们在看看其他的构造方法

// 只人为传入初始容量的构造方法,可以清晰的看到该方法自行调用了最根本的构造方法
// 并传入了 DEFAULT_LOAD_FACTOR ,这个参数在第3节有提及,值为16
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
// 什么都不传入,无参构造方法
// 我们回头去看最根本的构造方法,发现最复杂的部分其实是对容量的初始化设计
// 而无参构造表明不要人为设计容量,所以这里只要简单的设置负载因子 loadFactor 即可
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
// 本构造方法不进行分析
public HashMap(Map<? extends K, ? extends V> m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}

看完上述的构造方法,我们不难回答一开始给出的题目了。

HashMap 的容量取决于 initialCapacity ,默认为 DEFAULT_INITIAL_CAPACITY 的值16。我们可以人为的传入这个容量数值,而传入的值并不会直接成为 HashMap 的容量值,会进行一次运算,计算出大于传入值的 2 的整数次方。

loadFactor 是负载因子,主要目的是用来确认table 数组是否需要动态扩展,默认值 DEFAULT_LOAD_FACTOR 是0.75,比如table 数组大小为 16,负载因子为 0.75 时,threshold 就是12,当 table 的实际大小超过 12 时,table就需要动态扩容。

扩容时调用 resize() 方法,将 table 的长度扩大到原来的2倍(其实就是左移1位)。然后把旧的 table 中的数据复制到新的 table 中。如果数据量非常大的话,那么这个复制的操作将会消耗很多的性能。

8. HashMap的 resize()

HashMap 的 resize() 源码过于冗长,后面会附加源码供大家自行品读。在本文只简单概述 resize 的执行流程和一些小的操作。

  1. 首先,reszie 只会发生在 put 之后,这也是 JDK 1.8 之后的设计,在 JDK 1.7 的版本里,执行 put 操作之前需要先判断扩容,而现在是先插入再考虑扩容。
  2. 扩容的方式如前文提及,就是将 table数组 的长度扩大到原来的2倍,然后再把原来的元素重新放到新的数组中。在 JDK 1.7 的版本里,存在一个 transfer() 函数,会重新计算 hash值 来定位元素在新的数组中的位置。而在 1.8 开始的版本里,数组扩容为原来的两倍,这在二进制上来看只是在原来的基数上,高位补了个1,再加之计算的方式为 (n-1) & hash ,而1和任何数进行&运算都是数的本身,那么节点在新的数组中的位置只有两种情况:即原下标位置或者是原下标位置+原数组长度。

9. HashMap在1.8版本有什么改动

  1. 四次移位和四次异或变成了一次。主要是考虑到边际效益,操作四次并不会带来理想的收益,但是执行了更多的操作,这会付出更多的成本。
  2. 数组 + 链表 变成了 数组 + 链表 + 红黑树。主要是考虑到效率问题,在一些情况下,搜索红黑树的效率要比简单的遍历链表更高。红黑树的时间复杂度为O(logN)而链表的时间复杂度是O(N)。
  3. 链表的插入方式从头插法变成了尾插法。字面意思,但是并不是无意义的改动,头插法是存在严重风险的,后文会提到。
  4. 扩容计算元素在新数组的下标位置方式改变。1.7需要重新计算每个元素的位置,而1.8在逻辑上更为简单,要么是位置不变,要么是索引+旧容量大小。
  5. 插入时,1.7版本是先判断扩容再插入,1.8是先插入再判断扩容。
  6. 1.7版本是使用 Entry,1.8版本被 Node 替代。

10. 头插法的问题是什么,为什么1.8版本变成了尾插法。

本问题亦是一个难以详细解释的问题,可以自行搜索相关文章。

简要概述原因如下:头插法扩容的时候,会导致链表反转。而在头插法的过程中如果加入多线程的情况,则链表反转时A线程可能被挂起,B线程对链表再次进行头插法操作,B执行结束,A继续进行头插法,此时会导致链表产生环。

11. HashMap的key通常使用String,可以使用其他对象吗

可以。

但是 String 本身是一个 final类型 的数据,这意味着 String 是不可变的,那么它的 hashcode 自然也是固定的,而且 String 本身就缓存了 hashcode ,每次访问 String 的哈希值时不需要重复计算,提高了效率。使用其他对象则要重写 hashcode() 方法和 equals() 方法,但是即便如此也没有使用 String 更加高效。

附 String 类型的少量源码

// 该值用于字符存储
private final char value[];
// 缓存字符串的哈希值
private int hash; 

12. HashMap如果要存自定义的类对象,需要对该类进行什么操作

本题同 11 ,需要重写 hashcode() 和 equals()

13. 链表过深时,为什么不使用二叉查找树,为什么不使用二叉树,而使用红黑树

因为二叉查找树存在缺陷,在一些特殊的情况下,二叉查找树会变成一条线性结构,树的深度很深,此时与原来的链表结构基本相同,遍历查找会变得很慢。

使用二叉树亦会出现以上类似的问题,使用二叉树有可能出现只有左子树或者只有右子树的情况,也是和链表没有区别。

14. 为什么不一直使用红黑树,还要转化为链表

红黑树属于平衡二叉树,查找的效率非常高效。但是为了保证红黑树始终是“平衡”的状态,就要经常执行左旋、右旋、变色这些操作,而执行这些操作也是需要消耗时间的。所以如果数据量很小,与其一直维护“平衡”,倒不如直接遍历链表的效率更高。

15. 红黑树拓展

自行移步红黑树相关的文章,本篇文章主要内容为 HashMap 。

16. HashMap是线程安全的吗

不是。

在多线程的情况下,1.7版本的 HashMap 会出现死循环、数据丢失、数据覆盖的情况。在1.8版本中,数据覆盖的情况仍然存在。

而且每次执行 put 操作时,都会有 ++size 的操作(详见第4节),多线程的情况下,容易导致同时扩容的现象发生,但是最后只会有一个线程扩容后的数组会复制给table,其他线程都会丢失,同时会丢失各个线程中 put 的数据。

17. 怎么解决线程不安全的问题

使用 HashTable、Collections.synchronizedMap、 ConcurrentHashMap

Hashtable

Hashtable 是直接在操作方法上添加了关键字 synchronized ,把整个数组锁住,锁粒度比较大。

public synchronized V put(K key, V value);
public synchronized V get(Object key);

Collections.synchronizedMap

Collections.synchronizedMap 是使用 Collections 集合工具的内部类,通过传入 Map 封装出一个 SynchronizedMap 对象,内部定义了一个对象锁,方法内通过对象锁实现。

final V putVal(K key, V value, boolean onlyIfAbsent) {
    /**
     * code
     */
    for (ConcurrentHashMap.Node<K,V>[] tab = table;;) {
        ConcurrentHashMap.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) {
            /**
             * code
             */
        }
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
            synchronized (f) {
                /**
                 * code
                 */
            }
            /**
             * code
             */
        }
    }
    addCount(1L, binCount);
    return null;
}

ConcurrentHashMap

ConcurrentHashMap 使用了分段锁,降低了锁粒度,让并发度大幅度提高。

/* ---------------- Nodes -------------- */
static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    volatile V val;
    volatile ConcurrentHashMap.Node<K,V> next;
    /**
     * code
     */
}

/* ---------------- Fields -------------- */
transient volatile Node<K,V>[] table;
private transient volatile Node<K,V>[] nextTable;
private transient volatile long baseCount;
private transient volatile int sizeCtl;
private transient volatile int transferIndex;
private transient volatile int cellsBusy;
private transient volatile CounterCell[] counterCells;

/* ---------------- TreeBins -------------- */
static final class TreeBin<K,V> extends ConcurrentHashMap.Node<K,V>{
    ConcurrentHashMap.TreeNode<K,V> root;
    volatile ConcurrentHashMap.TreeNode<K,V> first;
    volatile Thread waiter;
    volatile int lockState;
    /**
     * code
     */
}

18. ConcurrentHashMap的分段锁实现原理

如上文所示,ConcurrentHashMap 成员变量使用 volatile 修饰,免除了指令重排序,同时保证内存可见性,另外使用 CAS 操作和 synchronized 结合实现赋值操作,多线程操作只会锁住当前操作索引的节点。如下图,线程 A 锁住 A 节点所在链表,线程 B 锁住 B 节点所在链表,操作互不干涉。

图片

19. HashMap,LinkedHashMap,TreeMap 有什么区别?

LinkedHashMap 保存了插入的先后顺序,是有序的。

TreeMap 实现了 SortMap 接口,能够进行自然排序或者指定比较器进行排序,也是有序的。

HashMap 的内部节点是无序的。

20. LinkedHashMap和TreeMap是如何实现有序的?

LinkedHashMap 里面有一个单链表,包含头尾节点,同时 LinkedHashMap 的每个节点都有 before 和 after 用来表示前后节点。

// 头尾节点
transient LinkedHashMap.Entry<K,V> head;
transient LinkedHashMap.Entry<K,V> tail;

// 维护的单链表
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
    LinkedHashMap.Entry<K,V> last = tail;
    tail = p;
    if (last == null)
        head = p;
    else {
        p.before = last;
        last.after = p;
    }
}

// LinkedHashMap 的节点
static class Entry<K,V> extends HashMap.Node<K,V> {
    Entry<K,V> before, after;
    Entry(int hash, K key, V value, Node<K,V> next) {
        super(hash, key, value, next);
    }
}

TreeMap 里存在一个比较器,我们在创建 TreeMap 的时候可以为其传入比较器的参数,使其根据比较器进行排序。

// 内部成员
private final Comparator<? super K> comparator;

// 构造方法
public TreeMap() {
    comparator = null;
}

public TreeMap(Comparator<? super K> comparator) {
    this.comparator = comparator;
}

public TreeMap(SortedMap<K, ? extends V> m) {
    comparator = m.comparator();
    try {
        buildFromSorted(m.size(), m.entrySet().iterator(), null, null);
    } catch (java.io.IOException cannotHappen) {
    } catch (ClassNotFoundException cannotHappen) {
    }
}

在执行 put 操作的时候,会访问这个比较器(即便为空)。内部使用红黑树实现。要么 key 所属的类实现 Comparable 接口,或者自定义一个实现了 Comparator 接口的比较器,传给 TreeMap 用户 key 的比较。

// 获取传入的比较器
Comparator<? super K> cpr = comparator;
// 比较器不为空,根据 comparator 进行排序
if (cpr != null) {
    do {
        parent = t;
        cmp = cpr.compare(key, t.key);
        if (cmp < 0)
            t = t.left;
        else if (cmp > 0)
            t = t.right;
        else
            return t.setValue(value);
    } while (t != null);
}
// 比较器为空,自己生成比较策略 --> 根据 key 的自然顺序排序
else {
    if (key == null)
        throw new NullPointerException();
    @SuppressWarnings("unchecked")
    Comparable<? super K> k = (Comparable<? super K>) key;
    do {
        parent = t;
        cmp = k.compareTo(t.key);
        if (cmp < 0)
            t = t.left;
        else if (cmp > 0)
            t = t.right;
        else
            return t.setValue(value);
    } while (t != null);
}
Entry<K,V> e = new Entry<>(key, value, parent);
if (cmp < 0)
    parent.left = e;
else
    parent.right = e;

21. HashMap & TreeMap & LinkedHashMap 使用场景?

一般情况下,使用最多的是 HashMap。
HashMap:在 Map 中插入、删除和定位元素时;
TreeMap:在需要按自然顺序或自定义顺序遍历键的情况下;
LinkedHashMap:在需要输出的顺序和输入的顺序相同的情况下。

22. HashMap 和 HashTable 有什么区别?

  1. HashMap 是线程不安全的,HashTable 是线程安全的;
  2. 由于线程安全,所以 HashTable 的效率比不上 HashMap;
  3. HashMap最多只允许一条记录的键为null,允许多条记录的值为null,而 HashTable不允许;
  4. HashMap 默认初始化数组的大小为16,HashTable 为 11,前者扩容时,扩大两倍,后者扩大两倍+1;
  5. HashMap 需要重新计算 hash 值,而 HashTable 直接使用对象的 hashCode

23. Java 中的另一个线程安全的与 HashMap 极其类似的类是什么?同样是线程安全,它与 HashTable 在线程同步上有什么不同?

ConcurrentHashMap 类

HashTable 是使用 synchronize 对整个对象加锁;
而 ConcurrentHashMap,在 JDK 1.7 中采用分段锁的方式;JDK 1.8 中直接采用了CAS + synchronized。

24. HashMap & ConcurrentHashMap 的区别?

ConcurrentHashMap 有加锁的操作,其他在本质上没有什么区别。

HashMap 的键值对允许有 null ,ConcurrentHashMap 不允许。

25. 为什么 ConcurrentHashMap 比 HashTable 效率要高?

参考 23 。

HashTable 使用 synchronized 直接锁住整个对象,用来处理并发问题。多个线程同时争夺一把锁,容易造成线程阻塞。

ConcurrentHashMap 在 1.7版本使用分段锁,在 1.8版本使用 CAS + synchronized + Node + 红黑树 ,锁粒度是桶位的 Node 头节点,降低了锁粒度。

26. 链表转红黑树是链表长度达到阈值,这个阈值是多少 ?

跳转到 本文 -> 源码浅析 -> 3. HashMap的几个基础变量 ,查看其中的 TREEIFY_THRESHOLDUNTREEIFY_THRESHOLD 相关内容。

三、其他拓展

ConcurrentHashMap重要的常量

由于大部分常量与 HashMap 一致,不再赘述,只查看比较特别的常量。

private transient volatile int sizeCtl;

当为负数时,-1 表示正在初始化,-N 表示 N - 1 个线程正在进行扩容;
当为 0 时,表示 table 还没有初始化;
当为其他正数时,表示初始化或者下一次进行扩容的大小。

ConcurrentHashMap的数据结构

最基本的结构也是继承 Entry 实现的 Node

static class Node<K,V> implements Map.Entry<K,V>

TreeNode 继承自 Node

static final class TreeNode<K,V> extends Node<K,V> {
    ConcurrentHashMap.TreeNode<K, V> parent;  // red-black tree links
    ConcurrentHashMap.TreeNode<K, V> left;
    ConcurrentHashMap.TreeNode<K, V> right;
    ConcurrentHashMap.TreeNode<K, V> prev;    // needed to unlink next upon deletion
    boolean red;

    TreeNode(int hash, K key, V val, ConcurrentHashMap.Node<K, V> next,
             ConcurrentHashMap.TreeNode<K, V> parent) {
        super(hash, key, val, next);
        this.parent = parent;
    }
    // ······
}

TreeBin 是封装 TreeNode 的容器

static final class TreeBin<K,V> extends ConcurrentHashMap.Node<K,V> {
    ConcurrentHashMap.TreeNode<K, V> root;
    volatile ConcurrentHashMap.TreeNode<K, V> first;
    volatile Thread waiter;
    volatile int lockState;
    // values for lockState
    static final int WRITER = 1; // set while holding write lock
    static final int WAITER = 2; // set when waiting for write lock
    static final int READER = 4; // increment value for setting read lock
    // ······
}

ConcurrentHashMap的put()

先看 put 的执行代码

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

由上可见,put 也是直接调用 putVal 进行后续操作,所以本段内容依旧解释 putVal 的实现过程

先判断 ConcurrentHashMap 有没有初始化,没有初始化先初始化

if (tab == null || (n = tab.length) == 0)
    tab = initTable();

之后判断是否存在 hash 冲突,不存在就直接插入进去

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
}

如果需要扩容,则就扩容,同时调用 helpTransfer 函数协助扩容时的数据传输

else if ((fh = f.hash) == MOVED)
    tab = helpTransfer(tab, f);

之后根据对应的桶位上的数据类型进行链表操作或者树操作,注意 这一步是带锁进行的。

操作后根据阈值进行红黑树和链表的转化,并统计新的 size 判断是否需要扩容。

附:putVal() 的源码

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

}


  • 5
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值