广告
返回顶部
首页 > 资讯 > 后端开发 > Python >java面试常见问题---ConcurrentHashMap
  • 900
分享到

java面试常见问题---ConcurrentHashMap

2024-04-02 19:04:59 900人浏览 薄情痞子

Python 官方文档:入门教程 => 点击学习

摘要

1、请你描述一下ConcurrentHashMap存储数据结构是什么样子呢? ConcurrentHashMap 内部的 map 结构和 HashMap 是一致的,都是由:

1、请你描述一下ConcurrentHashMap存储数据结构是什么样子呢?

  • ConcurrentHashMap 内部的 map 结构和 HashMap 是一致的,都是由:数组 + 链表 + 红黑树 构成。
  • ConcurrentHashMap 存储数据的单元和 HashMap 也是一致的,即,node 结构:
    
    static class Node<K,V> implements Map.Entry<K,V> {
        // hash值
        final int hash;
        // key
        final K key;
        // value
        volatile V val;
        // 后驱节点
        volatile Node<K,V> next;    
        ....
    }
    
  • ConcurrentHashMap 和 HashMap 区别就在于前者支持并发扩容,其内部通过加(自旋锁 + CAS + synchronized + 分段锁)来保证线程安全

2、请问ConcurrentHashMap的负载因子可以新指定吗?

  • 普通的 HashMap 的负载因子可以修改,但是 ConcurrentHashMap 不可以,因为它的负载因子使用 final关键字修饰,值是固定的 0.75 :

// 负载因子:表示散列表的填满程度~ 在ConcurrentHashMap中,该属性是固定值0.75,不可修改~

private static final float LOAD_FACTOR = 0.75f;

3、请问节点的 Node.hash 字段一般情况下必须 >=0 这是为什么?

或者说,Node 节点的 hash 值有几种情况?针对不同情况分析一下?

  • 如果 Node.hash = -1,表示当前节点是 **FWD(ForWardingNode) **节点(表示已经被迁移的节点)。
  • 如果 Node.hash = -2,表示当前节点已经树化,且当前节点为 TreeBin 对象,TreeBin 对象代理操作红黑树。
  • 如果 Node.hash > 0,表示当前节点是正常的 Node 节点,可能是链表,或者单个 Node。

4、请你简述 ConcurrentHashMap 中 sizeCtl 字段的作用(不同情况下的含义)?

sizeCtr 即 Size Control,这个字段一定要仔细去理解一下,这个字段看不懂,可能会整个 ConcurrentHashMap 源码都一脸懵逼。

① sizeCtl == 0 时,表示的是什么情况?
  • izeCtl == 0,是默认值,表示在真正第一次初始化散链表的时候使用默认容量 16 进行初始化。
② sizeCtl == -1 时,表示什么情况呢?
  • sizeCtl == -1表示当前散链表正处于初始化状态。有线程正在对当前散列表(table) 进行初始化操作。
  • ConcurrentHashMap 的散链表是延迟初始化的,在并发条件下必须确保只能初始化一次,所以 sizeCtl == -1 就相当于一个"标识锁",防止多个线程去初始化散列表。
  • 具体初始化操作就是在initTable()方法中,会通过 CAS 的方式去修改 sizeCtl 的值为 -1,表示已经有线程正在对散链表进行初始化,其他线程不可以再次初始化,只能等待!
    
    // SIZECTL:期望值,初始为0
    // this 就表示当前 ConcurrentHashMap对象
    // sc 表示要修改的 sizeCtrl 
    // -1 表示将 sc 修改为 -1
    U.compareAndSwapint(this, SIZECTL, sc, -1);
    
  • 如果 CAS 修改 sizeCtl = -1 操作成功的线程,可以接着执行对散链表初始化的逻辑。而 CAS 修改失败的线程,在这里会不断的自旋检查,直到散链表初始化结束。
  • 这里 CAS 失败的线程会走如下逻辑,即自旋的线程会通过Thread.yield();方法,短暂释放 CPU 资源,把 CPU 资源让给更饥饿的线程去使用。目的是为了减轻自旋对CPU 性能的消耗。
③ 初始化完散列表后,map.sizeCtl > 0 时,表示什么情况呢?
  • sizeCtl > 0 时,表示初始化或扩容完成后下一次触发扩容的阈值。比如,sizeCtl = 12 时,当散链表中数据的个数 >=12 时,就会触发扩容操作。
④ sizeCtl < 0 && sizeCtl != -1 时,代表什么情况呢?
  • sizeCtl < 0 && sizeCtl != -1 时,表示当前散链表正处于扩容状态。-(1 + nThreads),表示有n个线程正在一起扩容。这时候,sizeCtl 的高 16 位表示扩容标识戳,低 16 位表示参与并发扩容线程数:1 + nThread, 即当前参与并发扩容的线程数量为 n 个。

5、请你说一下扩容标识戳的作用及其计算方式?

  • 根据老表的长度 tab.length 去获取扩容唯一标识戳。
  • 假设 16 -> 32 这样扩容,那么扩容标识戳的作用就是在多线程并发扩容条件下,所有 16 -> 32 扩容的线程都可以参与并发扩容。
    
    // 固定值16,与扩容相关,计算扩容时会根据该属性值生成一个扩容标识戳
    private static int RESIZE_STAMP_BITS = 16;
    
    
    static final int resizeStamp(int n) {
        // RESIZE_STAMP_BITS:固定值16,与扩容相关,计算扩容时会根据该属性值生成一个扩容标识戳
        return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
    }
  • sizeCtl < 0 && sizeCtl != -1 时,这时候sizeCtl 的高 16 位就表示扩容标识戳,低 16 位表示参与并发扩容线程数:1 + nThread, 即当前参与并发扩容的线程数量为 n 个。

6、ConcurrentHashMap如何保证写数据线程安全?

这个问题其实就是问,向 ConcurrentHashMap 中添加数据确保线程安全是如何实现的。

  • ConcurrentHashMap 内部通过加锁(自旋锁 + CAS + synchronized + 分段锁)来保证线程安全。

添加数据具体流程如下:

  • ① 首先,先判断散链表是否已经初始化,如果没初始化则先初始化散链表,再进行写入操作。
  • ② 当向桶位中写数据时,先判断桶中是否为空,如果是空桶,则直接通过 CAS 的方式将新增数据节点写入桶中。如果 CAS 写入失败,则说明有其他线程已经在当前桶位中写入数据,当前线程竞争失败,回到自旋位置,自旋等待。
  • 如果当前桶中不为空,就需要判断当前桶中头结点的类型:
  • ③ 如果桶中头结点的 hash 值 为 -1,表示当前桶位的头结点为 FWD 结点,目前散链表正处于扩容过程中。这时候当前线程需要去协助扩容。
  • ④ 如果 ②、③ 条件不满足,则表示当前桶位的存放的可能是一条链表,也可能是红黑树的代理对象 TreeBin。这种情况下会使用 synchronized 锁住桶中的头结点,来保证桶内的写操作是线程安全的。

7、描述一下ConcurrentHashMap中的hash寻址算法

ConcurrentHashMap 的寻址算法和 HashMap 差别不大:

  • 首先是通过 Node 节点的 Key 获取到它的 HashCode 值,再将 HashCode 值通过 spread(int h)方法进行绕道运算,进而得到最终的 Hash 值。
    
    
    static final int spread(int h) {
        // 让原来的hash值异或^原来hash值的右移16位,再与&上HASH_BITS(0x7fffffff: 二进制为31个1)
        return (h ^ (h >>> 16)) & HASH_BITS;
    }
    
  • 获取到最终的 hash 值后,再通过寻址公式:index = (tab.length -1) & hash 获得桶位下标。

8、ConcurrentHashMap如何统计当前散列表中的数据量?

ConcurrentHashMap 统计存储数据的数量是通过 addCount(long x, int check) 方法实现的,本质上是借助了 LongAdder 原子类。

ConcurrentHashMap为什么不采用 ConcurrentHashMap为什么不采用 AtomicLong 统计散列表数据量呢?统计散列表数据量呢?

  • 因为 AtomicLong 原子类自增操作是基于 CAS 实现的,基于 CAS 实现会导致一个问题,就是当大量线程同时执行 CAS 操作时,只能有一个线程执行成功,而其他所有线程都会因为失败而进入自旋状态,自旋本身就是一个 while(true) 的循环,非常耗费系统资源。

那么 LongAdder 是如何保证大并发量下,性能依旧高效呢?

先看下LongAdder的操作原理图:

LongAdder采用"分段"的方式降低CAS失败的频次,典型的用空间换时间:

  • LongAdder有一个全局变量volatile long base;值,当并发不高的情况下都是通过CAS来直接操作base值,如果CAS失败,则针对LongAdder中的Cell[]数组中的Cell进行CAS操作,减少失败的概率。

如当前类中base = 10,有三个线程进行CAS原子性的 加1操作,线程一执行成功,此时base=11,线程二、线程三执行失败后开始针对于Cell[]数组中的Cell元素进行加1操作,同样也是CAS操作,此时数组index=1index=2Cellvalue都被设置为了1。

执行完成后,统计累加数据:sum = 11 + 1 + 1 = 13,利用LongAdder进行累加的操作就执行完了,流程图如下:

9、触发扩容条件的线程,执行的预处理工作都有哪些?

  • 首先,触发扩容条件的线程,要做的第一件事就是通过 CAS 的方式修改 sizeCtl 字段值,使其高 16 位为扩容唯一标识戳,低 16 位为(参与扩容的线程数 + 1),表示有线程正在进行扩容逻辑!
    • 注意:这里高 16 位的扩容唯一标识戳是根据当前散链表的长度计算得来,其最高位是 1。那么最终得到的 sizeCtl 就应该是一个负数。
  • 然后,当前触发扩容条件的线程会创建一个新的散链表,大小为原来旧散链表的 2 倍。并且将新散链表的引用赋给 map.nextTable 字段。
  • 又因为 ConcurrentHashMap 是支持多线程并发扩容的,所以需要让协助扩容的线程知道旧散链表数据迁移到新散链表的进度。为此 ConcurrentHashMap 提供了一个 transferIndex 字段,用于记录旧散链表数据的总迁移进度!迁移工作进度是从 高位桶开始,一直迁移到下标是 0 的桶位。

10、旧散链表中迁移完毕后的桶,如何做标记?

  • 旧散链表中迁移完毕的桶,需要用 ForwardingNode 对象表示桶内节点,这种 Node 比较特殊,是用来表示当前桶中的数据已经被迁移到新散链表的桶中去了。

ForwardingNode 有哪些作用?

  • ForwardingNode 对象内部提供了一个用于向新散链表中查询目标数据的find()方法。
  • 当此时某个线程刚好在旧散链表中查询目标元素时,刚好遇到当前桶位中存放的是 FWD 节点,那么就可以通过 FWD 节点的 find() 方法重新定向到新散链表中去查询目标元素!

11、如果散列表正在库容时,再来新的写入请求该如何处理呢?

这里要分两种情况考虑:

  • 如果当前线程执行写入操作时,根据寻址算法访问到的桶中不是 FWD 节点(即,当前桶中数据没有被迁移)。那么此时先判断桶中是否为空,如果为空则 CAS 方式写入数据。而如果桶不为空,则可能是链表或者 TreeBin,这时候需要通过 synchronized 关键字锁住桶的头结点再进行写入操作。
  • 而如果如果当前线程执行写入操作时,根据寻址算法访问到的桶中是 FWD 节点(即,当前桶中数据已经被迁移)。
    • 碰到 FWD 节点,说明此时散链表正在进行扩容,这时候需要当前线程也加入进去,去协助散链表扩容(helpTransfer(tab, f);协助扩容是为了尽量减少扩容所花费在数据迁移上的时间)。
    • 当前线程加入到协助扩容中后,ConcurrentHashMap 会根据全局的transferIndex字段去给当前线程分配迁移工作任务(需要负责迁移旧散链表的桶位区间)。例如,让当前线程负责迁移旧散链表中 【0-4】桶位上的数据到新散链表。
    • 一直到当前线程分配不到要负责迁移的任务时,则退出协助扩容,即扩容结束。这时候,当前线程就可以继续执行写入数据的逻辑了!

12、扩容期间,扩容工作线程如何维护sizeCtl的低16位呢?

  • 每一个执行扩容任务的线程(包含协助扩容),它在开始工作之前,都会更新 sizeCtl的低 16 位,即让低 16 位 +1,说明又加入一个新的线程去执行扩容。
  • 每个执行扩容的线程都会被分配一个迁移工作任务区间,如果当前线程所负责的任务区间迁移工作完成了,没有再被分配迁移任务区间,那么此时当前线程就可以退出协助扩容了,这时候更新 sizeCtl的低 16 位,即让低 16 位 -1,说明有一个线程退出并发扩容了。
  • 如果 sizeCtl 低 16 位-1后的值为 1,则说明当前线程是最后一个退出并发扩容的线程。

13、当桶位中链表升级为红黑树,且当前红黑树上有读线程正在访问,那么如果再来新的写线程请求该怎么处理?

写线程会被阻塞,因为红黑树比较特殊,新写入数据,可能会触发红黑树的自平衡,这就会导致树的结构发生变化,会影响读线程的读取结果!

在红黑树上读取数据和写入数据是互斥的,具体原理分析如下:

  • 我们知道 ConcurrentHashMap 中的红黑树由 TreeBin 来代理,TreeBin 内部有一个 Int 类型的 state 字段。
  • 当读线程在读取数据时,会使用 CAS 的方式将 state 值 +4(表示加了读锁),读取数据完毕后,再使用CAS 的方式将 state 值 -4。
  • 如果写线程去向红黑树中写入数据时,会先检查 state 值是否等于 0,如果是 0,则说明没有读线程在检索数据,这时候可以直接写入数据,写线程也会通过 CAS 的方式将 state 字段值设置为 1(表示加了写锁)。
  • 如果写线程检查 state 值不是 0,这时候就会park()挂起当前线程,使其等待被唤醒。挂起写线程时,写线程会先将 state 值的第 2 个 bit 位设置为 1(二进制为 10),转换成十进制就是 2,表示有写线程等待被唤醒。

反过来,当红黑树上有写线程正在执行写入操作,那么如果有新的读线程请求该怎么处理?

  • TreeBin 对象内部保留了一个链表结构,就是为了这种情况而设计的。这时候会让新来的读线程到链表上去访问数据,而不经过红黑树。

14、挂起等待的写线程后,什么时候将其唤醒再继续执行写操作呢?

  • 上一个问题中,我们分析了:当读线程在读取数据时,会使用 CAS 的方式将 state 值 +4(表示加了读锁),读取数据完毕后,再使用CAS 的方式将 state 值 -4。
  • 当 state 值减去 4 后,读线程会先检查一下 state 值是不是 2,如果是 2 ,则说明有等待被唤醒的写线程在挂起等候,这时候调用 unsafe.unpark() 方法去唤醒该写线程。

15、总结

文章会不定时更新,有时候一天多更新几篇,如果帮助您复习巩固了知识点,后续会亿点点的更新!希望大家多多关注编程网的其他内容!

--结束END--

本文标题: java面试常见问题---ConcurrentHashMap

本文链接: https://www.lsjlt.com/news/127965.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

本篇文章演示代码以及资料文档资料下载

下载Word文档到电脑,方便收藏和打印~

下载Word文档
猜你喜欢
  • java面试常见问题---ConcurrentHashMap
    1、请你描述一下ConcurrentHashMap存储数据结构是什么样子呢? ConcurrentHashMap 内部的 map 结构和 HashMap 是一致的,都是由:...
    99+
    2022-11-12
  • 常见的Java面试问题
    JVMJava虚拟机(JVM)是运行 Java 字节码的虚拟机。JVM有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。什么是字节码采用字节码的好处是什么在 Java 中,J...
    99+
    2023-06-03
  • Java泛型常见面试题(面试必问)
    目录1、泛型的基础概念1.1 为什么需要泛型1.2 什么是泛型2、泛型的定义和使用2.1 泛型类\泛型接口2.2 泛型方法2.3 泛型类的继承2.4 类型通配符?及其上下限1...
    99+
    2022-11-12
  • Java常见面试题:java面试笔记
    基本数据类型有哪些?基本数据类型包括byte、int、char、long、float、double、boolean和short。 java.lang.String类是final类型的,因此不可以继承这个类、不能修改这个类。为了提高效率节省空...
    99+
    2023-06-02
  • Java Web常见面试题
    jsp 和 servlet 有什么区别? (推荐学习:java常见面试题)jsp经编译后就变成了Servlet.(JSP的本质就是Servlet,JVM只能识别java的类,不能识...
    99+
    2019-08-30
    java面试题 Java
  • Java常见的面试题
      1)Java 中能创建 volatile 数组吗  能,Java 中可以创建 volatile 类型数组,不过只是一个指向数组的引用,而不是整个数组。我的意思是,如果改变引用指向的数组,将会受到 volatile 的保护,但是如果多个线...
    99+
    2023-06-03
  • Java面试题中常见的问题有哪些
    本篇内容主要讲解“Java面试题中常见的问题有哪些”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“Java面试题中常见的问题有哪些”吧!  1、java 中会存在内存泄漏吗,请简单描述。  答:会...
    99+
    2023-06-02
  • java常见面试题(160道)
    1. JDK 和 JRE 有什么区别? JDK:Java Development Kit 的简称,Java 开发工具包,提供了 Java 的开发环境和运行环境。JRE:Java Runtime Environment 的简称,Java 运行...
    99+
    2023-09-12
    java 开发语言 面试
  • java反射常见面试题
    什么是反射?反射主要是指程序可以访问、检测和修改它本身状态或行为的一种能力Java反射: ...
    99+
    2015-05-12
    java面试题 java
  • java常见面试题整理
    面向对象的特征有哪些方面 所谓封装,也就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。封装是面向对象的特征之一,是对象和类概念的主要特性。 (推荐学习:java常见面试题)继承...
    99+
    2016-08-28
    java面试题 java
  • 常见的java string面试题
    常见的java string面试题?相信很多没有经验的人对此束手无策,为此本文总结了问题出现的原因和解决方法,通过这篇文章希望你能解决这个问题。java基本数据类型有哪些Java的基本数据类型分为:1、整数类型,用来表示整数的数据类型。2、...
    99+
    2023-06-14
  • 单片机面试常见问题
    1、中断的概念?简述中断的过程 (1)中断:指CPU在正常执行程序的过程中,由于内部/外部事件的触发或由程序的预先安排,引起CPU暂时中断当前正在运行的程序,转而执行为内部/外部事件或程序预先安排的事件的服务子程序,待中断服务子程序执行完毕...
    99+
    2023-09-04
    单片机 stm32 嵌入式硬件
  • java面试常见模式问题---代理模式
    目录1、静态代理2、动态代理面试题一:JDK动态代理和CGLIB动态代理区别?面试题二:JDK动态代理为什么只能对实现了接口的类生成代理?总结 本篇总结的是 代理设计模式,后...
    99+
    2022-11-12
  • java面试常见模式问题---单例模式
    目录1、简介2、单例模式——懒汉式3、单例模式——饿汉式总结1、简介 单例模式使⽤场景: 业务系统全局只需要⼀个对象实例,⽐如发号...
    99+
    2022-11-12
  • Java常见的基础面试题
    JDK 和 JRE 有什么区别?JDK:Java Development Kit 的简称,java 开发工具包,提供了 java 的开发环境和运行环境。JRE:Java Runtime Environment 的简称,java 运行环境,为...
    99+
    2017-11-11
    java面试题 Java
  • java容器的常见面试题
    java 容器都有哪些? (推荐学习:java常见面试题)Collection 和 Collections 有什么区别?java.util.Collection 是一个集合接口(集合类的...
    99+
    2016-04-09
    java面试题 java
  • Redis面试常见问题有哪些
    本篇内容主要讲解“Redis面试常见问题有哪些”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“Redis面试常见问题有哪些”吧!1. 什么是缓存雪崩?怎么解决?通...
    99+
    2022-10-19
  • Python常见面试问题有哪些
    这篇文章主要介绍“Python常见面试问题有哪些”,在日常操作中,相信很多人在Python常见面试问题有哪些问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”Python常见面试问题有哪些”的疑惑有所帮助!接下来...
    99+
    2023-06-04
  • Java 面试中常见的同步问题是什么?
    在 Java 开发中,多线程是一个常见的话题。多线程的出现,使得程序能够更加高效地运行。但是多线程也会引发一些问题,比如线程安全和同步问题。在 Java 面试中,同步问题也是一个经常被问到的话题。本文将介绍 Java 面试中常见的同步问题...
    99+
    2023-09-22
    面试 同步 spring
  • Vue.js 常见面试题
    什么是SPA?SAP意思是 单页面应用 。SPA 是一种应用程序,它提前下载好布局,并让页面在不同布局之间切换进而无需刷新就可以渲染整个页面。与此特点相对的,它将会从服务器中获取必要信息并替换页面中对应的内容。什么是 Vue 指令Vue 指...
    99+
    2022-11-10
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作