面试必备系列JUC(8)-- hashmap和concurrenthashmap详解

前言

宁中则乃是华山派掌门人君子剑岳不群的妻子,而君子剑岳不群却又历来以“伪君子”而著称,这可以说是最大的讽刺了。但是华山女侠宁中则,倒是实实在在的能担得起“女侠”二字。

一、HashMap

令狐冲:师娘,最近我去猪猪厂面试了,面试官在一面考察基础知识的时候,问了我很多hashmap和concurrenthashmap的问题,我怕自己回答的不全,师娘能给我讲讲吗?

宁中则:冲儿,hashmap和concurrenthashmap是java中非常重要的两个数据结构,属于面试必问的,你之前太顽皮了,师娘再给你讲讲吧~

java7 HashMap

宁中则:冲儿,HashMap 底层是基于 数组 + 链表 组成的,不过在 jdk1.7 和 1.8 中具体实现稍有不同。你知道哪里不同吗?

令狐冲:这个难不倒我,hashmap是个hash桶加上链表。hashmap最关键的操作就是hash的逻辑,即根据把各种给了键值对的节点node,对应到数组中的逻辑,也就是确定哈希桶数组索引位置,java7的结构就是下面这种:

令狐冲:总的来说,HashMap里面是一个数组,然后数组中每个元素是一个单向链表。上图中,每个蓝色的方框都是嵌套类 Entry 的实例,Entry 包含四个属性:key, value, hash 值和用于单向链表的 next。

宁中则:那你知道hashmap的容量是多大吗?啥时候会发生扩容吗?

令狐冲:hashmap的数组容量为16,负载因子为 0.75,当数组的长度达到扩容的阈值capacity * loadFactor的时候,就会发生扩容,扩容后数组大小为当前的 2 倍。

宁中则:说的没错,那你简单的说说如何将一个key-value对插入到hashmap的吧~

令狐冲:这个先看下代码吧,我加了详细的注释

public V put(K key, V value) {
    // 当插入第一个元素的时候,需要先初始化数组大小
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);
    }
    // 如果 key 为 null,感兴趣的可以往里看,最终会将这个 entry 放到 table[0] 中
    if (key == null)
        return putForNullKey(value);
    // 1. 求 key 的 hash 值
    int hash = hash(key);
    // 2. 找到对应的数组下标
    int i = indexFor(hash, table.length);
    // 3. 遍历一下对应下标处的链表,看是否有重复的 key 已经存在,
    //    如果有,直接覆盖,put 方法返回旧值就结束了
    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++;
    // 4. 不存在重复的 key,将此 entry 添加到链表中,细节后面说
    addEntry(hash, key, value, i);
    return null;
}

宁中则:冲儿,你先给师娘详细解释下,hashmap的初始化部分吧,这块面试常问~

令狐冲:这块我恰好比较熟悉,在第一个元素插入 HashMap 的时候做一次数组的初始化,就是先确定初始的数组大小,并计算数组扩容的阈值。师娘你看看下面的源码:

private void inflateTable(int toSize) {
    // 保证数组大小一定是 2 的 n 次方。
    // 比如这样初始化:new HashMap(20),那么处理成初始数组大小是 32
    int capacity = roundUpToPowerOf2(toSize);
    // 计算扩容阈值:capacity * loadFactor
    threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
    // 算是初始化数组吧
    table = new Entry[capacity];
    initHashSeedAsNeeded(capacity); //ignore
}

宁中则:是的,源码写的很清楚,这里有一个将数组大小保持为 2 的 n 次方的做法,Java7 和 Java8 的 HashMap 和 ConcurrentHashMap 都有相应的要求,只不过实现的代码稍微有些不同。那key的hash值是如何计算的,你知道吗?

令狐冲:这个我也知道,还是用源码说话吧,先不用管具体实现,只需要知道它是通过对给的node中的key(键)调用每个Object都有的hashCode()方法得到一个值,然后做了一些处理,就给出了这里的hash值。

final int hash(Object k) {
    int h = hashSeed;
    if (0 != h && k instanceof String) {
        return sun.misc.Hashing.stringHash32((String) k);
    }

    h ^= k.hashCode();

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

宁中则:那是如何将一个key-value,放到一个具体的桶?

令狐冲:简单说就是取 hash 值的低 n 位。如在数组长度为 32 的时候,其实取的就是 key 的 hash 值的低 5 位,作为它在数组中的下标位置。

static int indexFor(int h, int length) {
    return h & (length-1);
}

宁中则:那找到桶之后,如何将entry加入到链表中的?

令狐冲:找到数组(桶)下标后,会先进行 key 判重,如果没有重复,就准备将新值放入到链表的表头。

void addEntry(int hash, K key, V value, int bucketIndex) {
    // 如果当前 HashMap 大小已经达到了阈值,并且新值要插入的数组位置已经有元素了,那么要扩容
    if ((size >= threshold) && (null != table[bucketIndex])) {
        // 扩容,后面会介绍一下
        resize(2 * table.length);
        // 扩容以后,重新计算 hash 值
        hash = (null != key) ? hash(key) : 0;
        // 重新计算扩容后的新的下标
        bucketIndex = indexFor(hash, table.length);
    }
    // 往下看
    createEntry(hash, key, value, bucketIndex);
}
// 这个很简单,其实就是将新值放到链表的表头,然后 size++
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++;
}

宁中则:我看出来了,这个方法的主要逻辑就是先判断是否需要扩容,需要的话先扩容,然后再将这个新的数据插入到扩容后的数组的相应位置处的链表的表头。那你知道hashmap是如何扩容的吗?

令狐冲:在插入新值的时候,如果当前的 size 已经达到了阈值,并且要插入的数组位置上已经有元素,那么就会触发扩容,扩容后,数组大小为原来的 2 倍。

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

令狐冲:扩容就是用一个新的大数组替换原来的小数组,并将原来数组中的值迁移到新的数组中。

宁中则:我问一个关键的问题,扩容之后,各个entry的hash值有没有变?换句话说,扩容之后,hashmap中的元素顺序改变了没有?

令狐冲:这个我就不太清楚了,师娘能给讲讲吗?

宁中则:其实这个是面试的重点,由于是双倍扩容,迁移过程中,会将原来 table[i] 中的链表的所有节点,分拆到新的数组的 newTable[i] 和 newTable[i + oldLength] 位置上。如原来数组长度是 16,那么扩容后,原来 table[0] 处的链表中的所有元素会被分配到新数组中 newTable[0] 和 newTable[16] 这两个位置。总之,扩容会再次触发hash取值。

令狐冲:这次终于记住了。。面试的时候我不慌了。

宁中则:那你知道hashmap如何取值的吗?

令狐冲:这个也知道,对于 put 过程,get 过程是非常简单的:
1. 根据 key 计算 hash 值。
2. 找到相应的数组下标:hash & (length - 1)。
3. 遍历该数组位置处的链表,直到找到相等(==或equals)的 key。

宁中则:冲儿回答的很棒~

java8 HashMap

宁中则:前面讲了很多java7 中的hashmap,那你知道java8中的hashmap有了哪些改进吗?

令狐冲:也知道一些, JDK1.8 之后的 HashMap 底层在解决哈希冲突的时候,就不单单是使用数组加上单链表的组合了, 因为当处理如果 hash 值冲突较多的情况下,链表的长度就会越来越长,此时通过单链表来寻找对应 Key 对应的 Value 的时候就会使得时间复杂度达到 O(n)。

宁中则:那有啥解决措施吗?

令狐冲:有的,在 JDK1.8 之后,在链表新增节点导致链表长度超过8 的时候,就会在添加元素的同时将原来的单链表转化为红黑树。同样的,如果一个红黑树的hashmap,当红黑树的长度为6的时候,就会将红黑树转换为单链表。所以源码中的临界值是7。

宁中则:java7 中使用 Entry 来代表每个 HashMap 中的数据节点,Java8 中使用 Node,基本没有区别,都是 key,value,hash 和 next 这四个属性,不过,Node 只能用于链表的情况,红黑树的情况需要使用 TreeNode。我们根据数组元素中,第一个节点数据类型是 Node 还是 TreeNode 来判断该位置下是链表还是红黑树的。

为什么HashMap是线程不安全的?

宁中则:冲儿,你知道为什么hashmap是线程不安全的吗?

令狐冲:师娘,这个我还真的不知道,我只是知道concurrenthashmap是线程安全的。为什么说HashMap是线程不安全的呢?它在多线程环境下,会发生什么情况呢?我都不懂~

宁中则:hashmap在多线程下不安全主要是有两方面的体现,一是resize死循环,二是fail-fast,我下面分别给你讲一下。

宁中则:先给你说下resize死循环。当HashMap要在链表里插入新的Entry时,在Java 8之前是将Entry插入到链表头部,在Java 8开始是插入链表尾部(Java 8用Node对象替代了Entry对象)。Java 7插入链表头部,是考虑到新插入的数据,更可能作为热点数据被使用,放在头部可以减少查找时间。

宁中则:Java 8改为插入链表尾部,原因就是防止环化。 因为resize的赋值方式,也就是使用了单链表的头插入方式,同一位置上新元素总会被放在链表的头部位置,在旧数组中同一条Entry链上的元素,通过重新计算索引位置后,有可能被放到了新数组的不同位置上。这也是HashMap不能用于多线程场景的一个重要原因。

令狐冲:师娘能不能举个例子?

宁中则:我们来看一下多线程场景下的示例:

宁中则:图上的hash算法是自定义的,不要纠结这个,是简单的用key mod 一下表的大小(也就是数组的长度)。不是那个实际的hash算法!

最上面的是old hash 表,其中的Hash表的size=2, 所以key = 3, 7, 5,在mod 2以后都冲突在table[1]这里了。接下来的三个步骤是Hash表 扩容变成4,然后所有的entry重新hash!

do {
    Entry<K,V> next = e.next; // <--假设线程一执行到这里就被调度挂起了
    int i = indexFor(e.hash, newCapacity);
    e.next = newTable[i];
    newTable[i] = e;
    e = next;
} while (e != null);

宁中则:重hash的结果如下:

宁中则:注意,因为Thread1的 e 指向了key(3),而next指向了key(7),其在线程二rehash后,指向了线程二重组后的链表。我们可以看到链表的顺序被反转了。这里的意思是线程1这会还没有完全开始扩容,但e和next已经指向了,线程2是正常的扩容的,那这会在3这个位置上,就是7->3这个顺序。
然后,线程一被调度回来执行。

宁中则:回到线程1里面的时候,一切安好。线程一接着工作。把key(7)摘下来,放到newTable[i]的第一个,然后把e和next往下移。这时候,原来的线程2里面的key7的e和key3的next没了,e=key3,next=null。当继续执行,需要将key3加回到key7的前面。e.next = newTable[i] 导致 key(3).next 指向了 key(7)。

令狐冲:师娘,此时的key(7).next 已经指向了key(3), 那么就会出现环形链表了呀?

宁中则:是的,线程2生成的e和next的关系影响到了线程1里面的情况。从而打乱了正常的e和next的链。于是,当我们的线程一调用到,HashTable.get(11)时,即又到了3这个位置,需要插入新的,那这会就e 和next就乱了。

令狐冲:哦哦,这下我终于懂得为什么hashmap是线程不安全的了。

ConcurrentHashMap

Java7 ConcurrentHashMap

令狐冲:师娘给我讲讲ConcurrentHashMap呗~

宁中则:ConcurrentHashMap 和 HashMap 思路是差不多的,但是因为它支持并发操作,所以要复杂一些。整个 ConcurrentHashMap 由一个个 Segment 组成,Segment 代表”部分“或”一段“的意思,所以很多地方都会将其描述为分段锁。注意,行文中,我很多地方用了“槽”来代表一个 segment。简单理解就是,ConcurrentHashMap 是一个 Segment 数组,Segment 通过继承 ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全。

宁中则:冲儿,concurrencyLevel你要重点理解下,就是并行级别、并发数的意思,Segment 数默认是 16,也就是说 ConcurrentHashMap 有 16 个 Segments,所以理论上,最多可以同时支持 16 个线程并发写,只要它们的操作分别分布在不同的 Segment 上。这个值可以在初始化的时候设置为其他值,但是一旦初始化以后,它是不可以扩容的。

令狐冲:哦哦,那师娘能不能给我讲讲它的容量是多大?

宁中则:初始化之后,Segment 数组长度为 16,不可以扩容,Segment[i] 的默认大小为 2,负载因子是 0.75,得出初始阈值为 1.5,也就是以后插入第一个元素不会触发扩容,插入第二个会进行第一次扩容。

令狐冲:那是如何put 键值对的呢?

宁中则:我们先看 put 的主流程,对于其中的一些关键细节操作,后面会进行详细介绍。

public V put(K key, V value) {
    Segment<K,V> s;
    if (value == null)
        throw new NullPointerException();
    // 1. 计算 key 的 hash 值
    int hash = hash(key);
    // 2. 根据 hash 值找到 Segment 数组中的位置 j
    // hash 是 32 位,无符号右移 segmentShift(28) 位,剩下高 4 位,
    // 然后和 segmentMask(15) 做一次与操作,也就是说 j 是 hash 值的高 4 位,也就是槽的数组下标
    int j = (hash >>> segmentShift) & segmentMask;
    // 刚刚说了,初始化的时候初始化了 segment[0],但是其他位置还是 null,
    // ensureSegment(j) 对 segment[j] 进行初始化
    if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
         (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
        s = ensureSegment(j);
    // 3. 插入新值到 槽 s 中
    return s.put(key, hash, value, false);
}

第一层皮很简单,根据 hash 值很快就能找到相应的 Segment,之后就是 Segment 内部的 put 操作了。Segment 内部是由数组+链表组成的。

final V put(K key, int hash, V value, boolean onlyIfAbsent) {
    // 在往该 segment 写入前,需要先获取该 segment 的独占锁
    //    先看主流程,后面还会具体介绍这部分内容
    HashEntry<K,V> node = tryLock() ? null :
        scanAndLockForPut(key, hash, value);
    V oldValue;
    try {
        // 这个是 segment 内部的数组
        HashEntry<K,V>[] tab = table;
        // 再利用 hash 值,求应该放置的数组下标
        int index = (tab.length - 1) & hash;
        // first 是数组该位置处的链表的表头
        HashEntry<K,V> first = entryAt(tab, index);

        // 下面这串 for 循环虽然很长,不过也很好理解,想想该位置没有任何元素和已经存在一个链表这两种情况
        for (HashEntry<K,V> e = first;;) {
            if (e != null) {
                K k;
                if ((k = e.key) == key ||
                    (e.hash == hash && key.equals(k))) {
                    oldValue = e.value;
                    if (!onlyIfAbsent) {
                        // 覆盖旧值
                        e.value = value;
                        ++modCount;
                    }
                    break;
                }
                // 继续顺着链表走
                e = e.next;
            }
            else {
                // node 到底是不是 null,这个要看获取锁的过程,不过和这里都没有关系。
                // 如果不为 null,那就直接将它设置为链表表头;如果是null,初始化并设置为链表表头。
                if (node != null)
                    node.setNext(first);
                else
                    node = new HashEntry<K,V>(hash, key, value, first);

                int c = count + 1;
                // 如果超过了该 segment 的阈值,这个 segment 需要扩容
                if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                    rehash(node); // 扩容后面也会具体分析
                else
                    // 没有达到阈值,将 node 放到数组 tab 的 index 位置,
                    // 其实就是将新的节点设置成原链表的表头
                    setEntryAt(tab, index, node);
                ++modCount;
                count = c;
                oldValue = null;
                break;
            }
        }
    } finally {
        // 解锁
        unlock();
    }
    return oldValue;
}

宁中则:整体流程还是比较简单的,由于有独占锁的保护,所以 segment 内部的操作并不复杂。至于这里面的并发问题,我们稍后再进行介绍。到这里 put 操作就结束了,接下来,我给你说一说其中几步关键的操作。

宁中则:第一个是,初始化槽ensureSegment,ConcurrentHashMap 初始化的时候会初始化第一个槽 segment[0],对于其他槽来说,在插入第一个值的时候进行初始化。这里需要考虑并发,因为很可能会有多个线程同时进来初始化同一个槽 segment[k],不过只要有一个成功了就可以。

private Segment<K,V> ensureSegment(int k) {
    final Segment<K,V>[] ss = this.segments;
    long u = (k << SSHIFT) + SBASE; // raw offset
    Segment<K,V> seg;
    if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
        // 这里看到为什么之前要初始化 segment[0] 了,
        // 使用当前 segment[0] 处的数组长度和负载因子来初始化 segment[k]
        // 为什么要用“当前”,因为 segment[0] 可能早就扩容过了
        Segment<K,V> proto = ss[0];
        int cap = proto.table.length;
        float lf = proto.loadFactor;
        int threshold = (int)(cap * lf);

        // 初始化 segment[k] 内部的数组
        HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
        if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
            == null) { // 再次检查一遍该槽是否被其他线程初始化了。

            Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
            // 使用 while 循环,内部用 CAS,当前线程成功设值或其他线程成功设值后,退出
            while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
                   == null) {
                if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
                    break;
            }
        }
    }
    return seg;
}

宁中则:冲儿,总的来说,ensureSegment(int k) 比较简单,对于并发操作使用 CAS 进行控制。如果当前线程 CAS 失败,这里的 while 循环是为了将 seg 赋值返回。

宁中则:第二个是,获取写入锁 scanAndLockForPut。在代码中,在往某个 segment 中 put 的时候,首先会调用 node = tryLock() ? null : scanAndLockForPut(key, hash, value),也就是说先进行一次 tryLock() 快速获取该 segment 的独占锁,如果失败,那么进入到 scanAndLockForPut 这个方法来获取锁。

令狐冲:那师娘,那是如何控制加锁的呢?

宁中则:冲儿,下面我们来具体分析这个方法中是怎么控制加锁的。

private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
    HashEntry<K,V> first = entryForHash(this, hash);
    HashEntry<K,V> e = first;
    HashEntry<K,V> node = null;
    int retries = -1; // negative while locating node

    // 循环获取锁
    while (!tryLock()) {
        HashEntry<K,V> f; // to recheck first below
        if (retries < 0) {
            if (e == null) {
                if (node == null) // speculatively create node
                    // 进到这里说明数组该位置的链表是空的,没有任何元素
                    // 当然,进到这里的另一个原因是 tryLock() 失败,所以该槽存在并发,不一定是该位置
                    node = new HashEntry<K,V>(hash, key, value, null);
                retries = 0;
            }
            else if (key.equals(e.key))
                retries = 0;
            else
                // 顺着链表往下走
                e = e.next;
        }
        // 重试次数如果超过 MAX_SCAN_RETRIES(单核1多核64),那么不抢了,进入到阻塞队列等待锁
        //    lock() 是阻塞方法,直到获取锁后返回
        else if (++retries > MAX_SCAN_RETRIES) {
            lock();
            break;
        }
        else if ((retries & 1) == 0 &&
                 // 这个时候是有大问题了,那就是有新的元素进到了链表,成为了新的表头
                 //     所以这边的策略是,相当于重新走一遍这个 scanAndLockForPut 方法
                 (f = entryForHash(this, hash)) != first) {
            e = first = f; // re-traverse if entry changed
            retries = -1;
        }
    }
    return node;
}

宁中则:这个方法有两个出口,一个是 tryLock() 成功了,循环终止,另一个就是重试次数超过了 MAX_SCAN_RETRIES,进到 lock() 方法,此方法会阻塞等待,直到成功拿到独占锁。
这个方法就是看似复杂,但是其实就是做了一件事,那就是获取该segment的独占锁,如果需要的话顺便实例化了一下 node。

宁中则:第三个是,扩容rehash,segment 数组不能扩容,扩容是 segment 数组某个位置内部的数组 HashEntry[] 进行扩容,扩容后,容量为原来的 2 倍。首先,我们要回顾一下触发扩容的地方,put 的时候,如果判断该值的插入会导致该 segment 的元素个数超过阈值,那么先进行扩容,再插值,读者这个时候可以回去 put 方法看一眼。

令狐冲:那这里需要考虑并发吗?

宁中则:该方法不需要考虑并发,因为到这里的时候,是持有该 segment 的独占锁的。

令狐冲:那如何从concurrenthashmap中get数据呢?

宁中则:相对于 put 来说,get 真的很简单了:

  1. 计算 hash 值,找到 segment 数组中的具体位置,或我们前面用的“槽”
  2. 槽中也是一个数组,根据 hash 找到数组中具体的位置
  3. 到这里是链表了,顺着链表进行查找即可

Java8 ConcurrentHashMap

令狐冲:师娘,那Java8 中 ConcurrentHashMap有啥不同吗?

宁中则: Java8 对 ConcurrentHashMap 进行了比较大的改动。对于 ConcurrentHashMap,Java8 也引入了红黑树。我们先用一个示意图来描述下其结构:

比java8的HashMap复杂很多,但是结构差不多全。

令狐冲:师娘,同步方式也差不多吗?

宁中则:对于put操作,如果Key对应的数组元素为null,则通过CAS操作将其设置为当前值。 如果Key对应的数组元素(也即链表表头或者树的根元素)不为null,则对该元素使用synchronized关键字申请锁,然后进行操作。如果该put操作使得当前链表长度超过一定阈值,则将该链表转换为树,从而提高寻址效率。

宁中则:对于读操作,由于数组被volatile关键字修饰,因此不用担心数组的可见性问题。同时每个元素是一个Node实例(Java 7中每个元素是一个HashEntry),它的Key值和hash值都由final修饰,不可变更,无须关心它们被修改后的可见性问题。而其Value及对下一个元素的引用由volatile修饰,可见性也有保障

令狐冲:师娘,面试的时候,我还被问到HashMap和ConcurrentHashMap对比,这个可以说说 吗?

宁中则:主要是有三点,

  1. HashMap非线程安全、ConcurrentHashMap线程安全
  2. HashMap允许Key与Value为空,ConcurrentHashMap不允许
  3. HashMap不允许通过迭代器遍历的同时修改,ConcurrentHashMap允许。并且更新可见

令狐冲:那HashMap和HashTable的对比呢

宁中则:也有三点,
(1)HashMap是非线程安全的,HashTable是线程安全的。
(2)HashMap的键和值都允许有null存在,而HashTable则都不行。
(3)因为线程安全、哈希效率的问题,HashMap效率比HashTable的要高。

令狐冲:那HashTable和ConcurrentHashMap对比呢?

宁中则:HashTable里使用的是synchronized关键字,这其实是对对象加锁,锁住的都是对象整体,当Hashtable的大小增加到一定的时候,性能会急剧下降,因为迭代时需要被锁定很长的时间。ConcurrentHashMap相对于HashTable的syn关键字锁的粒度更精细了一些,并发性能更好。

令狐冲:多谢师娘,这次我记住了!

面试总结

  1. 知道hashmap吗?java7和java8有啥区别
  2. 说说hashmap的扩容机制
  3. 为什么hashmap是线程不安全的?
  4. concurrenthashmap的并发数是多少?可控吗?
  5. concurrenthashmap的原理你知道吗?讲一讲
  6. 说说HashMap和ConcurrentHashMap对比
  7. HashTable和ConcurrentHashMap对比,你知道吗?

===================================================
字节内推:
字节内推〉字节校招开启。简历砸过来!!!!!!!
200多个岗位,地点:北京 上海 广州 杭州 成都 深圳。。
有问题可以直接在公众号中回复,必回答!!!
社招请直接简历发邮箱:908248386@qq.com

字节内推码:B1RHWFK
官网校招简历投递通道:https://jobs.toutiao.com/campus/m/position?referral_code=B1RHWFK

===================================================
微信公众号:猿侠令狐冲
公众号定期更新文章!!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值