一文整理总结常见Java面试题系列——Java集合篇(2022最新版)

Java面试------集合篇 hash冲突说白了就是,你有两个不同的对象,在你往HashMap中插入的时候,计算出的Hash值是相同的。解决:1>链地址法(Chaining):将哈希冲突的元素存储在链表中。在哈希表的每个槽位上维护一个链表,具有相同哈希值的元素都放入到对应链表的末尾。当发生冲突时,只需要在链表中进行线性搜索即可找到目标元素。2>开放地址法(Open Addressing):在发生冲突时,通过一定的探测策略在哈希表中寻找下一个可用的空槽位。常见的探测策略包括线性探测、二次探测和双重哈希等。 阅读详情
AI摘要

本文档全面梳理了Java后端面试中的关键知识点,涵盖MySQL数据库、Redis缓存、计算机网络、操作系统、Linux、Spring框架、Java基础、集合、并发、虚拟机、异常处理、设计模式、Dubbo、Netty等技术,助力求职者高效准备面试。

关于作者
🐶 程序猿周周
⌨️ 短视频小厂BUG攻城狮
🤺 如果文章对你有帮助,记得关注、点赞、收藏,一键三连哦,你的支持将成为我最大的动力

本文是《后端面试小册子》系列的第 8️⃣ 篇文章,该系列将整理和梳理笔者作为 Java 后端程序猿在日常工作以及面试中遇到的实际问题,通过这些问题的系统学习,也帮助笔者顺利拿到阿里、字节、华为、快手等多个大厂 Offer,也祝愿大家能够早日斩获自己心仪的 Offer。


PS:《后端面试小册子》已整理成册,目前共十三章节,总计约二十万字,欢迎👏🏻关注公众号【程序猿周周】获取电子版和更多学习资料(最新系列文章也会在此陆续更新)。公众号后台可以回复关键词「电⼦书」可获得这份面试小册子。文中所有内容都会在 Github 开源,项目地址 csnotes,如文中存在错误,欢迎指出。如果觉得文章还对你有所帮助,赶紧点个免费的 star 支持一下吧!

在这里插入图片描述

标题地址
MySQL数据库面试题总结(2022版)https://blog.youkuaiyun.com/adminpd/article/details/122910606
Redis面试题总结(2022版)https://blog.youkuaiyun.com/adminpd/article/details/122934938
计算机网络面试题总结(2022版)https://blog.youkuaiyun.com/adminpd/article/details/122973684
操作系统面试题总结(2022版)https://blog.youkuaiyun.com/adminpd/article/details/122994599
Linux面试题总结(2022版)https://blog.youkuaiyun.com/adminpd/article/details/122994862
Spring面试题总结(2022版)https://blog.youkuaiyun.com/adminpd/article/details/123016872
Java基础面试题总结(2022版)https://blog.youkuaiyun.com/adminpd/article/details/123080189
Java集合面试题总结(2022版)https://blog.youkuaiyun.com/adminpd/article/details/123171501
Java并发面试题总结(2022版)https://blog.youkuaiyun.com/adminpd/article/details/123266624
Java虚拟机面试题总结(2022版)https://blog.youkuaiyun.com/adminpd/article/details/123412605
Java异常面试题总结(2022版)https://blog.youkuaiyun.com/adminpd/article/details/123462676
设计模式面试题总结(2022版)https://blog.youkuaiyun.com/adminpd/article/details/123490442
Dubbo面试题总结(2022版)https://blog.youkuaiyun.com/adminpd/article/details/123538243
Netty面试题总结(2022版)https://blog.youkuaiyun.com/adminpd/article/details/123564362

文章目录

1 容器概述

1、什么是集合以及使用的好处?

集合是用于存放对象的容器,而集合类是 Java 的一种数据结构,常用的集合类定义在 java.util 包中。

需要注意的是,集合类只能存放对象,不能存放基本数据类型,且是对象的引用,而非对象本身。

好处

  • 提供高效的的数据结构和算法,挺高程序运行效率;
  • 提供通用 API 能力,降低开发和维护成本;

2、Java 有哪些常见的集合类?

Java 中的容器集合类主要包括 Collection(集合) 和 Map(映射) 两大类,而 Collection 接口又包括 Set(无序不重复) 和 List(有序可重复) 两个子接口。

1)List 接口的实现类主要有:ArrayList、LinkedList、Stack 以及 Vector 等;

2)Set 接口的实现类主要有:HashSet、TreeSet、LinkedHashSet等;

3)Map 接口的实现类主要有:HashMap、TreeMap、Hashtable、ConcurrentHashMap 以及 Properties 等。

在这里插入图片描述

3、数组和集合的区别?

  • 数组固定长度,集合成都可变;

  • 数组可以存放基本数据类型,集合只能存储引用数据类型;

  • 数组存储的元素必须是同一个数据类型,集合支持存储不同数据类型。

4、集合是否可以存储 NULL?

1)AList 接口 ArrayList、LinkedList 以及 Vector 等都可以存储多个 null;

2)Set 接口中 HashSet、LinkedSet 可以存储一个 null,TreeSet 不能存储 null;

3)Map 接口中 HashMap、LinkedHashMa 的 key 与 value 均可以为 null。Treemap 的 key 不可以为 null,value 可以为 null。HashTable、ConcurrentHashMap 的 key 与 value 均不能为 null。

2 Collection

2.1 List

1、ArrayList 和 LinkedList 的区别?

二者都是 List 接口的实现,顾名思义,从数据结构上看,它们底层一个是数组,一个是链表的数据结构。前者是数组队列,相当于动态数组;后者为双向链表结构,也可当作堆栈、队列、双端队列。

1)访问效率的不同

  • 对于随机访问(get 和 set)操作,ArrayList 效率比 LinkedList 更高,因为 LinkedList 是线性的数据存储方式,所以需要移动指针从前往后依次查找。
  • 对于数据增加删除(add 和 remove)操作,LinkedList 的效率又会比 ArrayList 更高,因为数组在其中进行增删操作时,会对操作点之后所有数据的下标索引造成影响,需要进行数据的移动。

2)利用效率的不同

从利用效率来看,ArrayList 自由性较低,因为它需要有手动或默认一个固定大小的容量,但是它的使用比较方便,只需要创建,然后添加数据,通过调用下标进行使用;而 LinkedList 自由性较高,能够动态的随数据量的变化而变化,但是它不便于使用。

3)控件开销的不同

ArrayList 控件开销在于底层数组中预留了一定空间;而LinkList主要控件开销在于需要存储结点信息以及结点指针信息。

2、ArrayList 和 Vector 的区别?

  • 同步性不同

Vector 是线程安全的,它的方法之间都加了 synchronized 关键字修饰,而 ArrayList 是非线程安全的。因此在不考虑线程安全情况下使用 ArrayList 的效率更高。

  • 数据增长不同

ArrayList 和 Vector 都有一个初始的容量大小,并在必要时对数组进行扩容。但 Vector 每次增长两倍,而 ArrayList 增长 1.5 倍。

3、如何边遍历边删除集合中的元素?

唯一正确的做法是使用迭代器 Iterator.remove() 方法。

Iterator<Integer> it=list.iterator();
while(it.hasNext()){
    it.remove();
}

如果直接使用 foreach 循环遍历时删除时,该语法糖会自动生成一个 Iterator 进行遍历集合,但 Iterator 不允许遍历中的元素被修改,故会抛出 ConcurrentModificationException 异常。

for(Integer i : list){
    list.remove(I);
}

4、Iterator 和 ListIterator 的区别?

ListIterator 是一个继承于 Iterator 接口,功能更加强大的迭代器,但只能用于各种 List 类型的访问。

故二者的主要区别:

  • Iterator 支持 List 和 Set,而 ListIterator 只支持 List;
  • Iterator 只能单向访问,而 ListIterator 可以双向前后遍历;
  • ListIterator 继承了 Iterator 的所有方法,并新增了如添加 add()、替换 set() 等更多好用方法。

5、什么是 RandomAccess 接口?

RandomAccess 接口是 Java 集合框架中为了标记 List 集合是否支持随机访问而设计。

即只要 List 集合实现这个接口,就能支持快速随机访问,按位置读取元素的平均时间复杂度是 o(1),如 ArrayList。而没有实现这个接口,表示不支持随机访问,如 LinkedList。

通过源码和测试,我们可以发现:支持 RandomAccess 接口的集合使用 for 循环比使用迭代器 Iterator 更快,反之,不支持 RandomAccess 接口的集合使用 Iterator 或 foreach 更优。

6、什么是 Fail-Fast 和 Fail-Safe?

在 Collection 集合类中,有线程安全和线程不安全这两大类型。对于线程不安全的类,并发情况下可能会出现快速失败(fail-fast) 情况,而线程安全的类,可能出现安全失败(fail-safe) 的情况。

这里需要注意的是,以上情况都是针对并发而言。

fail-fast 机制

当遍历一个集合对象时,如果集合对象的结构被修改了,就会抛出 ConcurrentModificationExcetion 异常。但需要注意的是,迭代器的快速失败行为无法得到保证,因为一般来说,不可能对是否出现不同步并发修改做出任何硬性保证。快速失败迭代器则会尽最大努力抛出 ConcurrentModificationException。

我们以 ArrayList 为例,ArrayList 继承自 AbstractList 类,AbstractList 内部有一个 modCount 属性,代表修改的次数。且每一次 add、remove 操作都会使得 modCount 自增。

public boolean add(E e) {
    modCount++;  // here
    add(e, elementData, size);
    return true;
}

迭代器对象有一个属性 expectedModCount 属性,它被赋值为该方法调用时 modCount 值。这也就意味着 expectedModCount 是 modCount 在这个时间点的快照值,且这个值在 iterator 内部是不会再发送变化的。

private class Itr implements Iterator<E> {
    int cursor = 0;  // Index of next elemnet to be returned.
    int lastRet = -1;  // Index of last elemnet returned.
    int expectedModCount = modCount;
}

我们在使用迭代器调用 next() 方法时,第一步就是检查 modCount 和 expectedModCount 是否相等,如果不相等就立即抛出 ConcurrentModificationException。到这就很好理解了,如果我们使用迭代器的同时使用 add、remove 操作了集合,则会导致 modCount 发生变化,从而导致抛出异常。

public E next() {
    checkForComodification();
    // ......
}

final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

至于为什么快速失败行为无法得到保证也就不难理解,因为虽然集合发生了变化,但如果没有调用 next() 方法,则也不会抛出异常。

即这个机制的主要目的是告诉你,当前迭代器要进行操作是有问题的,因为集合对象现在的状态发生了改变。

那没为何使用迭代器 remove() 方法不会触发该机制?

通过源码不难看出,remove() 方法没有进行 modCount 的检查,并手动把 expectedModCount 值修改成了 modCount,这又保证了下一次迭代的正确。

ref

fail-safe 机制

Fail-Safe 机制主要是针对线程安全的集合类(如 ConcurrentHashMap),其的出现的目的是为了解决 fail-fast 抛出异常处理起来不方便的问题。

并发容器的 iterator() 方法返回的迭代器内部都是保存了该集合对象的一个快照副本,并且没有 modCount 等数值做检查。这也造成了并发容器迭代器读取的数据是某个时间点的快照版本。你可以并发读取,不会抛出异常,但无法不保证遍历读取的值和当前集合对象的状态是一致的。 同时创建集合拷贝还需要时间和空间上的额外开销。

7、ArrayList 中 elementData 为何被 transient 修饰?

8、ArrayList 添加元素的流程或扩容机制?

private int newCapacity(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity <= 0) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return minCapacity;
    }
    return (newCapacity - MAX_ARRAY_SIZE <= 0)
        ? newCapacity : hugeCapacity(minCapacity);
}

上面是 ArrayList 扩容时计算新容量的方法,其中 minCapacity 是当前 size + 1,通过 oldCapacity + (oldCapacity >> 1) 我们可以知道新容量是老容量的 1.5 倍。

同时通过 MAX_ARRAY_SIZE 常量可知 ArrayList 的最大长度是 Integer.MAX_VALUE。

ref

2.2 Set

3 Map

3.1 HashMap

1、底层数据结构是什么?

在 JDK 1.7 中,HashMap 底层使用数组+链表的形式进行存储,在 JDK 1.8 之后,增加了红黑树,即数组+链表+红黑树的形式存储元素。

其中桶数组是用来存储元素,链表是用来解决哈希冲突,而红黑树是用来提高查询效率的。

PS 可能会被问到解决 Hash 冲突的常见方法?

  • 链地址法
  • 开放定址法:
  • 再哈希法
  • 建立公共溢出区

2、链表和红黑树之间的转换?

  • 链表长度大于 8 且数组长度大于 64,则将链表转换成红黑树;
  • 链表长度小于 6 时会将红黑树转换成链表。

3、阈值为何是 8 和 6?

作者在源码中注释到:TreeNodes 占用空间是普通 Nodes 的两倍, 所以只有当普通桶包含足够多的节点时才会转成 TreeNodes。

当 hashCode 离散性足够好时,树型桶用到的概率非常小,因为数据均匀分布在每个桶中,几乎不会有链表长度会达到阈值。但是在随机 hashCode 情况下,离散性可能会变差,然而又不能阻止用户使用这种不好的 hash 算法,因此就可能导致不均匀的数据分布。不过理想情况下随机 hashCode 算法下所有桶中节点的分布频率会遵循泊松分布,作者还计算出相关概率,即当链表长度达到 8 个元素的概率为 0.00000006,几乎是不可能事件。

因此,源码定义了在 binCount>=7(从 0 开始共 8 个数),即该槽插入第 9 个节点(超过 8 个)时转为红黑树。

至于为何树转链表的阈值选为 6?主要还是为了避免红黑树和链表之间频繁转换带来的性能损耗。

4、为何使用红黑树而非二叉树或平衡树?

相比普通二叉树,红黑树是一棵平衡树,它的添加、删除和查找操作最差时间复杂度为 o(logn),避免了普通二叉树最差情况下 o(n) 的复杂度。

平衡二叉树是比红黑树更加严格的平衡树,为了达到平衡需要进行更多的旋转次数,所以红黑树插入删除操作效率更高。

ref

5、put 操作的执行流程?

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

从上边的源码我们可以总结出 HashMap 在进行 put 操作时的一个大体流程:

1)首先通过 hash() 函数方法计算出 key 的 hash 值;

2)判断桶数组是否为空或长度为 0,是则使用 resize() 方法进行初始化;

3)根据 tab[i = (n - 1) & hash] (n 是数组长度)计算 key 在数组中对应的下标值,如果该位置没有存放数据,则使用 newNode() 方法直接插入当前数据;

4)如果对应下标已存在数据,则需要分三种情况处理碰撞:

  • 对应数组当前 node 节点与新元素 hash 值相等,且 key 值也相等,则使用新元素 value 值覆盖旧值;
  • 对应数组当前 node 节点是树型节点,则将新元素直接插入红黑树中;
  • 对应数组当前 node 节点是链表节点,则遍历此链表到尾部,并将新元素插入到链表尾部。插入成功之后再根据链表长度与树化阈值的比较判断是否需要进行树化;

5)最后在所有元素处理完毕后,还需判断当前元素大小是否超过扩容阈值,超过则调用 resize() 方法扩容。

6、hash 值如何计算?

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

当 key 为 null 时 hash 值为 0,否则,hash 值就是 key 的 hashCode 值异或 key 的 hashCode 无符号右移 16 位的结果。即 hash 值是 hashCode 值的高 16 位和低 16 位的异或结果。通过异或处理,避免了只靠低位数据计算 hash 值时导致的冲突,计算结果由高低位共同决定,可以使元素分布更均匀。

至于为什么扰动函数可以降低哈希碰撞发生的概率,这是因为 hash 值是一个 int 类型整数,拥有 40 量级的映射空间。由于 HasMap 桶数组初始长度才 16,就需要使用 hash 值对数组长度进行取模计算,通过前文源码可知下标计算方式是 (n-1) & hash,该运算的意义是保留 hash 值二进制中的最后 logn 位,高位直接清零。当只保留 hash 值最后几位的信息时,如果遇到 hash 函数不太理想时,如等差数列等漏洞,导致最后几位成规律重复,则将面临严重的碰撞问题。

故 HashMap 通过 hashCode 的高低 16 位进行异或处理,增大最终 hash 值的随机性,以解决上述问题。

7、常用的 Hash 函数?

  • 除留余数法

也是 HashMap 中使用的方法,计算公式为 H(key) = key % p (p<=n),关键字除以一个不大于哈希表长度的正整数 p,所得余数为地址。

  • 直接定址法

直接根据 key 映射到对应哈希表位置,如 i 就直接放到下标 i 的位置。

  • 数字分析法

根据 key 的某些数字(如十位或者百位)作为映射的位置。

  • 平方取中法

根据 key 平方的中间几位作为映射的位置。

  • 折叠法

把 key 分割成位数相同的几段,然后将其叠加和作为映射的位置。

8、HashMap 容量为何是 2 的倍数?

主要有两个原因:

1)为了方便哈希取余,因为当除数是 2 的倍数时,可以将 % 取模运算将换成 & 位运算,以达到更高的运算效率;

2)扩容后容量也是 2 的倍数,则每个桶中的节点要么保留在当前位置,要么移动 2 次幂的位置(即 原位置+原数组容量),这样就可以快速简洁的完成数据迁移。

9、为何负载因子是 0.75?

为了减少哈希冲突发生的概率,当 HashMap 中的元素个数达到一个阈值 threshold 的时候就会触发扩容,把所有元素 rehash 之后再放在扩容后的容器中,这是一个相当耗时的操作。

这个阈值 threshold 又与当前元素个数(newCap)以及负载因子(loadFactor)正相关。至于为何选址 0.75 作为负载因子,简单来讲这是出于对时间和空间成本综合考量。

1)如果我们设的负载因子比较大,元素比较多时,扩容时桶数组没有发生碰撞的位置较少,查找的时间成本就增加了。

2)如果设置的比较小,元素又比较少时,数组还有足够空位的时候就发生了扩容,发生哈希碰撞的概率就降低了,查找时间成本降低,但是就需要更多的空间去存储元素,空间成本就增加了。

10、HashMap 是如何进行扩容的?

我们在 HashMap 是如何进行 put 操作中讲到,map 在插入新元素之后还会进行容量检查,当超过阈值时,就是调用 resize() 进行扩容处理。其主要作用:初始化桶数组或对数组进行 2 倍的扩容。

if (e.next == null)
    // 数组当前位置未发生碰撞,直接分配到新位置
    newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
    // 分为 lo、hi 两个新链表
    Node<K,V> loHead = null, loTail = null;
    Node<K,V> hiHead = null, hiTail = null;
    Node<K,V> 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);
    // lo 链表放到新数组现有位置
    if (loTail != null) {
        loTail.next = null;
        newTab[j] = loHead;
    }
    // hi 链表放到 +oldCap 的位置
    if (hiTail != null) {
        hiTail.next = null;
        newTab[j + oldCap] = hiHead;
    }
}

由上面一段扩容方法中的核心代码可知,由于数组扩容前后长度均是 2 的次幂,则看节点 hash 值新增的那一位是 0 还是 1,若是 0 则在新数组索引下标不变,是 1 则变成当前下标 j + oldCap。
在这里插入图片描述

如此巧妙的设计不仅能把冲突节点随机分配到数组不同位置上,又省略了重新计算节点位置下标的时间,该设计思路也常用于数据库的分库分表中。

11、JDK 1.8 中有哪些优化?

  • 数据结构

将原来数组+链表改成了数组+链表或红黑树的结构,用于降低查找操作的时间复杂度。

  • 插入方式

put 操作链表的插入方式从头插法改成了尾插法,简单说法就是 1.7 将新元素直接放到链表头部,而 1.8 插入到链表尾部。这是因为头插法扩容时会使链表发生反转,多线程环境下会产生环。

ref 并发下HashMap头插会造成死循环情况说明

  • reHash

扩容时 1.7 需要对数组中所有节点重新 hash 计算在新数组中的位置,而 1.8 采用了更简单的位运算方式,提高扩容的效率。

  • 扩容时机

1.7 在插入时会先判断是否需要扩容,再插入,而 1.8 会先进行插入,再判断是否需要扩容。

  • hash函数

1.7 做了四次移位和四次异或,而 1.8 只需要一次。因为 4 次的边际效用也不大,改为一次可提升效率。

12、HashMap 是否线程安全以及如何解决?

HashMap 是非线程安全的,可能会发生这些问题:

  • 多线程下扩容死循环。由 JDK 1.7 的头插法导致,1.8 中以解决。
  • 多线程的 put 可能导致元素的丢失。

如果两个线程计算出来的索引位置是相同的,那会造成前一个 key 被后一个 key 覆盖,从而导致前一个元素的丢失。

  • put 和 get 并发时,可能导致 get 为 null。

线程 1 执行 put 时,因为元素个数超出 threshold 而导致 rehash,线程 2 此时执行 get,有可能导致这个问题。这个问题在 JDK 1.7 和 JDK 1.8 中都存在。

如何解决 HashMap 非线程安全问题

我们可以使用 Java 中线程安全的 Map,如 HashTable、Collections.SynchronizedMap 以及 ConcurrentHashMap 等。

  • HashTable 是直接在操作方法上加 synchronized 关键字,锁住整个table数组,粒度比较大;
  • Collections.SynchronizedMap 是使用 Collections 集合工具的内部类,通过传入 Map 封装出一个 SynchronizedMap 对象,内部定义了一个对象锁,方法内通过对象锁实现;
  • ConcurrentHashMap 在 JDK 1.7 中使用分段锁,在 JDK 1.8 中使用 CAS + Synchronized。

13、如何设计一个 HashMap?

无需慌张,这题本质是考察 Hash 冲突的处理。

3.2 ConcurrentHashMap

1、说一下 ConcurrentHashMap 的实现?

首先 ConcurrentHashMap 在 JDK 1.7 和 1.8 中实现方式是完全不同的。其中 1.7 四基于分段锁实现,而 1.8 是基于CAS+Synchronized实现。

1.7 分段锁

在 JDK1.7 中,ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成(Segment 继承了 ReentrantLock,是一种可重入锁。HashEntry 则用于存储键值对数据)。一个 ConcurrentHashMap 里包含一个 Segment 数组,一个 Segment 里包含一个 HashEntry 数组 ,每个 HashEntry 是一个链表结构的元素,因此 JDK1.7 的 ConcurrentHashMap 依旧是一种数组+链表结构。

当对 HashEntry 数组的数据进行修改时,整个流程和 HashMap 非常类似,只不过是先定位到具体的 Segment,然后通过 ReentrantLock 去操作而已,后面的流程,就和 HashMap 基本一致。这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全(分段锁)。

在这里插入图片描述

1.8 CAS+Synchronized

JDK 1.8 中实现线程安全不是在数据结构上下功夫,它的数据结构和 HashMap 一样的是数组 + 链表 + 红黑树。它实现线程安全的关键点在于 put 流程。

在锁的实现上,采用 CAS 操作和 Synchronized 锁实现更加低粒度的锁,将锁的级别控制在了更细粒度的 table 元素级别,也就是说只需要锁住这个链表的头节点,并不会影响其他的 table 元素的读写,大大提高了并发度。

在这里插入图片描述

2、为什么使用 Synchronized 替换 ReentrantLock?

  • Java 开发者从未放弃过 Synchronized 关键字的优化,在 JDK1.8 Synchronized 锁的性能得到了很大的提高,并且 Synchronized 有多种锁状态,会从无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁一步步转换,因此并非是我们认为的重量级锁。

  • 减少内存开销 。假设使用可重入锁来获得同步支持,那么每个节点都需要通过继承 AQS 来获得同步支持。但并不是每个节点都需要获得同步支持的,只有链表的头节点(红黑树的根节点)需要同步,这无疑带来了巨大内存浪费。

3、put 操作逻辑?

这里主要讲讲 JDK 1.8 中的实现。

4、get 操作是否需要加锁?

不需要加锁。因为 Node 的元素 value 和指针 next 是用 volatile 修饰的,在多线程环境下线程A修改节点的 value 或者新增节点的时候是对线程B可见的。

这也是比其它并发集合比如 Hashtable、Collections.synchronizedMap() 效率高的原因之一。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ROOGzLcn-1645970391210)(https://segmentfault.com/img/bVcOaHA)]

5、Key 和 Value 不支持 null 的原因?

我们先说 为何 value 不能为 null。因为 ConcurrentHashMap 是用于多线程的,如果 get(key) 方法到了 null,就无法判断存在的 value 是 null,还是没有找到对应的 key 而为 null,就有了二义性。

6、size 方法如何计算大小?

  • 1.7

虽然 count 变量是被 volatile 修饰的,但是并不是简单的把所有 Segment 的 count 值相加。因为有可能在累加过程中 count 值发生了改变,那么此时结果就不正确了。但是也不能直接锁住,这样效率太低。因此在 JDK1.7 中的做法是先尝试 2 次通过不锁住 Segment 的方式来统计各个 Segment 大小,如果统计的过程中,容器的 count 发生了变化,则再采用加锁的方式来统计所有Segment 的大小。

那么 ConcurrentHashMap 是如何判断在统计的时候容器是否发生了变化呢?

使用 modCount 变量,在 put、remove 方法里操作元素前都会将变量 modCount++,那么在统计大小前后比较 modCount 是否发生变化,从而得知容器的大小是否发生变化。

  • 1.8

由于没有 segment 的概念,所以只需要用一个 baseCount 变量来记录 ConcurrentHashMap 当前节点的个数。

1)先尝试通过CAS 更新 baseCount 计数;

2)如果多线程竞争激烈,某些线程 CAS 失败,那就 CAS 尝试将 cellsBusy 置 1,成功则可以把 baseCount 变化的次数暂存到一个数组 counterCells 里,后续数组 counterCells 的值会加到 baseCount 中;

3)如果 cellsBusy 置 1 失败又会反复进行 CAS baseCount 和 CAS counterCells 数组。

3.3 Others

1、HashMap 节点是否有序?

HashMap 是无序的,根据 hash 值随机插入。如果想使用有序的 Map,可以使用 LinkedHashMap 或者 TreeMap。

2、LinkedHashMap 如何实现有序?

LinkedHashMap 维护了一个双向链表以及头尾两节点,同时 LinkedHashMap 节点 Entry 内部除了继承 HashMap 的 Node 属性,还有 before 和 after 用于标识前置节点和后置节点。

在这里插入图片描述

3、TreeMap 如何实现有序?

TreeMap 是按照 Key 的自然顺序或者 Comprator 的顺序进行排序,内部是通过红黑树来实现。所以要么 key 所属的类实现 Comparable 接口,或者自定义一个实现了 Comparator 接口的比较器,传给 TreeMap 用于 key 的比较。

Java集合常见面试题集锦 Set里面不允许有重复的元素,所谓重复,即不能有两个相等(注意,不是仅仅是相同)的对象 ,即假设Set集合中有了一个A对象,现在我要向Set集合再存入一个B对象,但B对象与A对象equals相等,则B对象存储不进去。Map与List和Set不同,它是双列的集合,其中有put方法,定义如下:put(obj key,obj value),每次存储时,要存储一对key/value,不能存储重复的key,这个重复的规则也是按equals比较相等。Vector是线程安全的,而ArrayList是线程不安全的。 阅读详情

相关推荐

Java集合面试题 52道

集合容器概述 什么是集合 集合就是一个放数据的容器,准确的说是放数据对象引用的容器 集合类存放的都是对象的引用,而不是对象的本身 集合类型主要有3种:set(集)、list(列表)和map(映射)。 集合的特点 集合的特点主要有如下两点: 集合用于存储对象的容器,对象是用来封装数据,对象多了也需要存储集中式管理。 和数组对比对象的大小不确定。因为集合是可变长度的。数组需要提前定义大小 集合和数组的区别 数组是固定长度的;集合可变长度的。 数组可以存储基本数据类型,也可以存储引用数据类型;集合只能存储引用数

qq_46306282的博客 1143

java面试题-集合篇

java集合相关面试题

qq_33129875的博客 3238

基于FaceX-Zoo实现的Face Mask Adding人脸戴口罩

人脸系列文章目录 文章目录人脸系列文章目录前言一、加口罩效果二、实现原理二、代码实现 前言 目前人脸加口罩均为在人脸位置贴图式的,非常不真实。前段时间京东开源一个基于PRnet实现的3D口罩拟合人脸,加口罩非常逼真,但是只是单独一个加口罩demo,没有完整的流程。于是本人结合人脸检测,关键点对齐。PRnet加口罩实现一个完整的加口罩流程。只需一个命令简单方便调用,实现人脸加口罩。 https://github.com/JDAI-CV/FaceX-Zoo 一、加口罩效果 下图可以看出效果还是非常逼真的

zengwb的博客 3390

万字Java集合面试题总结

ArrayList的底层是动态数组,它的容量能动态增长。在添加大量元素前,应用可以使用操作增加ArrayList实例的容量。ArrayList 继承了 AbstractList ,并实现了 List 接口。分析ArrayList源码主要从三个方面去翻阅:成员变量,构造函数,关键方法以下源码都来源于jdk1.8二叉树,顾名思义,每个节点最多有两个“叉”,也就是两个子节点,分别是左子节点和右子节点。不过,二叉树并不要求每个节点都有两个子节点,有的节点只有左子节点,有的节点只有右子节点。

weixin_60800735的博客 387

香橙派R1 Plus LTS变身无线AP/客户端:手把手教你用RTL8811CU和MT7601U网卡刷OpenWRT

本文详细介绍了如何将香橙派R1 Plus LTS开发板通过RTL8811CU和MT7601U两款USB无线网卡刷入OpenWRT系统,实现无线AP热点发射与客户端接入的双重功能。从硬件选型、驱动安装到双网卡协同配置,提供完整的实战指南,帮助用户快速搭建稳定高效的无线网络解决方案。

weixin_30919429的博客 408

【NVIDIA Jetson Orin系列 - 安装NVIDIA SDK Manager】

NVIDIA Jetson Orin - 安装NVIDIA SDK Manager

dally2的博客 2690

一文整理总结常见Java后端面试题系列——Java基础篇(2022最新版)

为什么会出现自动拆装箱Java 语言是一个面向对象的语言,但是 Java 中的基本数据类型却是不面向对象的,这在实际使用时存在很多的不便,为了解决这个不足,在设计类时为每个基本数据类型设计了一个对应的类进行代表,这样八个和基本数据类型对应的类统称为包装类(Wrapper Class)。使得基本数据类型也具有了对象的性质,并且为其添加了属性和方法,丰富了基本类型的操作。

程序猿周周的博客 2万+

变压器铁心100Hz振动噪声的COMSOL磁致伸缩仿真全流程

磁致伸缩是铁磁材料在磁场作用下发生尺寸变化的现象,也是变压器铁心振动与噪声产生的根本原因。由于应变与磁通密度近似成平方关系,50Hz电网激励下铁心振动基频为100Hz。借助COMSOL多物理场仿真,可将磁场计算结果映射为结构应力和变形,完成“电磁—结构”单向耦合分析。该技术广泛应用于变压器振动噪声预测、结构优化及故障诊断等工程场景。本文从磁致伸缩物理机理出发,系统梳理了材料参数、等效建模、边界条件设置和结果校核等关键步骤,帮助工程师掌握从磁致伸缩到结构变形的完整仿真链路,解决实际工程对标难题。

weixin_33796177的博客 312

一文整理总结常见Java后端面试题系列——Spring篇(2022最新版)

一、基本概念 1、Spring 是什么? Spring 是一个轻量级 Java 开发框架,最早有 Rod Johnson 创建,目的是为了解决企业级应用开发的业务逻辑层和其他各层的耦合问题。它是一个分层的 JavaSE/JavaEE full-stack(一站式)轻量级开源框架,为开发 Java 应用程序提供全面的基础架构支持。Spring 负责基础架构,因此 Java 开发者可以专注于应用程序的开发。Spring最根本的使命是解决企业级应用开发的复杂性,即简化Java开发。 2、Spring 带来哪些好处

程序猿周周的博客 2万+

Linux性能调优之CPU时间分布(亲和性、带宽、权重)配置

带宽权重亲和性进程级别Cgroup-v2服务级别systyemd我所渴求的,無非是將心中脫穎語出的本性付諸生活,為何竟如此艱難呢 ------赫尔曼·黑塞《德米安》

山河已无恙 1711

LeRobot 入门教程(十六)Isaac Lab LeIsaac 叠衣服、夹橙子仿真环境

LeRobotEnvHub现已支持通过LeIsaac进行机器人模仿学习,提供摘橙子、搬立方体等日常任务仿真环境。用户可通过HuggingFace共享环境,使用一行代码加载任务,支持随机动作测试和远程操控。系统提供云端仿真方案,无需本地GPU即可运行。当前支持4种任务环境,通过make_env函数切换,其中折叠布料任务需先调用initialize()函数。该平台集成了数据采集、转换和训练全流程,为机器人学习提供便捷工具链。

2506_90492529的博客 1483

一文整理总结常见Java后端面试题系列——Java并发篇(2022最新版)

初始(NEW):新创建了一个线程对象,但还没有调用start()方法;运行(RUNNABLE):Java 线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”;线程对象创建后,其它线程(如 main 线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取 CPU的 使用权,此时处于就绪状态(ready)。就绪状态的线程在获得 CPU 时间片后变为运行中状态(running)。阻塞(BLOCKED):表示线程阻塞于锁;

程序猿周周的博客 1万+

龙腾CMS downloadFile接口任意文件读取漏洞复现 [附POC]

大连网龙网络科技有限公司专注于在信息科技领域中向客户提供商业解决方案,是专业的互联网服务解决方案提供商和网络营销服务专家,为用户提供一体化的网络营销整体解决方案,龙腾CMS downloadFile接口存在任意文件读取漏洞。

薛定谔嘚喵博客 720

Java 基础高频面试题(2022年最新版)

最新 Java 基础高频面试题

程序员囧辉 51万+

LeRobot 入门教程(十三)集成自定义的机器人、相机等硬件

本文详细介绍了如何将自定义机器人集成到LeRobot生态系统的完整流程。主要内容包括:1)硬件准备与通信接口选择;2)继承Robot基类实现标准接口;3)定义观察特征和动作特征;4)处理设备连接/断开逻辑;5)支持校准与配置;6)实现核心传感器读取和动作发送功能;7)可选遥操作器集成;8)遵循4项规范打包自定义设备(命名前缀、类名对应、文件结构、模块暴露);9)提供了完整实现示例。通过该方案,开发者可让自定义机器人兼容LeRobot的数据采集、控制管道和策略训练等全套工具链。

2506_90492529的博客 1902

廖雪峰Python教程[完整版]

廖雪峰的python教程整理的,已经排好版了,而且还是完整的,适合小白

一文整理总结常见Java后端面试题系列——设计模式篇(2022最新版)

设计模式是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性、程序的重用性。

程序猿周周的博客 1万+

假人有限元模型

假人模型,车辆碰撞假人有限元模型,适合做乘员安全的研究者利用

一文整理总结常见Java后端面试题系列——Java虚拟机篇(2022最新版)

JDK 1.8 以前的 HotSpot 有个叫方法区的内存区域,也叫永久代(permanent generation)。而从 JDK 1.7 开始,方法区的部分数据就被移除:符号引用(Symbols)移至 Native heap,字面量(interned strings)和静态变量(class statics)移至 Java heap。

程序猿周周的博客 1万+

老版QQ原生表情gif动态

这是老版的QQ表情,不过全是动态的的,Windows照片查看器是不会动的,可以右击用浏览器打开查看,再说一遍是老版的,只是分享给宁愿用老版也要动态的人。

复分析 阿尔福斯(Ahlfors) 第三版答案 英文

答案分为十章,有部分习题没有答案,全英文复分析阿尔福斯(Ahlfors) 第三版答案

一文整理常见Java后端面试题系列——Netty篇(2022最新版)

Netty 是由 JBOSS 提供的一个 Java 开源框架。Netty 提供异步的、基于事件驱动的网络应用程序框架,用以快速开发高性能、高可靠性的网络 IO 程序,是目前最流行的 NIO 框架,Netty 在互联网领域、大数据分布式计算领域、游戏行业、通信行业等获得了广泛的应用,知名的 Elasticsearch 、Dubbo 框架内部都采用了 Netty。Channel。

程序猿周周的博客 9989

问遍了身边的面试官朋友,我整理出这份 Java 集合高频面试题(2022年最新版)

看完这篇,对线阿里面试官问题不大

程序员囧辉 5万+

Java面试题及答案整理(2022最新版)

Java面试题及答案(2022版),适用于应届生、用工作经验的程序员,每道都是认真筛选出的高频面试题,助力大家能找到满意的工作! 下载链接:全部面试题及答案PDF 1.什么是B/S架构?什么是C/S架构 B/S(Browser/Server),浏览器/服务器程序; C/S(Client/Server),客户端/服务端,桌面应用程序 2.Java都有那些开发平台? JAVA SE :主要用在客户端开发 JAVA EE :主要用在web应用程序开发 JAVA ME :主要用在嵌入式应用程序开发 3.什么是JVM

码农code之路 1万+

一文整理常见Java后端面试题系列——Dubbo篇(2022最新版)

🐶⌨️ 短视频小厂BUG攻城狮🤺 如果文章对你有帮助,,你的支持将成为我最大的动力本文是《后端面试小册子》系列的第 1️⃣3️⃣ 篇文章,该系列将整理和梳理笔者作为 Java 后端程序猿在日常工作以及面试中遇到的实际问题,通过这些问题的系统学习,也帮助笔者顺利拿到阿里、字节、华为、快手等多个大厂 Offer,也祝愿大家能够早日斩获自己心仪的 Offer。

程序猿周周的博客 6015

【2022最新版】Java基础面试题总结(70道题含答案解析)

最近面试的小伙伴很多,对此整理了一份Java面试题手册:基础知识、Java集合/泛型面试题、Java异常面试题、Java中的IO与NIO面试题、Java反射、Java序列化、Java注解、多线程&并发、JVM、Mysql、Redis、Memcached、MongoDB、Spring、SpringBoot、SpringCloud、RabbitMQ、等等。可以分享给大家学习。 【持续更新中】 ......

m0_58479954的博客 1万+

一文整理常见Java后端面试题系列——Kafka篇(2022最新版)

Kafka 是分布式发布-订阅消息系统,它最初是由 LinkedIn 公司开发的,之后成为 Apache 项目的一部分,Kafka是一个分布式,可划分的,冗余备份的持久性的日志服务,它主要用于处理流式数据。

程序猿周周的博客 4658

一文整理常见Java后端面试题系列——RocketMQ篇(2022最新版)

🐶⌨️ 短视频小厂BUG攻城狮🤺 如果文章对你有帮助,,你的支持将成为我最大的动力本文是《后端面试小册子》系列的第 1️⃣6️⃣ 篇文章,该系列将整理和梳理笔者作为 Java 后端程序猿在日常工作以及面试中遇到的实际问题,通过这些问题的系统学习,也帮助笔者顺利拿到阿里、字节、华为、快手等多个大厂 Offer,也祝愿大家能够早日斩获自己心仪的 Offer。

程序猿周周的博客 4676
上一篇: 一文整理总结常见Java后端面试题系列——Java基础篇(2022最新版)
下一篇: 一文整理总结常见Java后端面试题系列——Java并发篇(2022最新版)
程序猿周周
程序猿周周 新星创作者: Java技术领域 新星创作者: Java技术领域
博客等级 码龄10年 1万+粉丝 125原创
评论 3
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序猿周周

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值