Java进阶(ConcurrentHashMap)——面试时ConcurrentHashMap常见问题解读 & 结合源码分析 & 多线程CAS比较并交换 初识

这篇具有很好参考价值的文章主要介绍了Java进阶(ConcurrentHashMap)——面试时ConcurrentHashMap常见问题解读 & 结合源码分析 & 多线程CAS比较并交换 初识。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

Java进阶(ConcurrentHashMap)——面试时ConcurrentHashMap常见问题解读 & 结合源码分析 & 多线程CAS比较并交换 初识,Java,java,面试,开发语言

前言

List、Set、HashMap作为Java中常用的集合,需要深入认识其原理和特性。

本篇博客介绍常见的关于Java中线程安全的ConcurrentHashMap集合的面试问题,结合源码分析题目背后的知识点。

关于List的博客文章如下:

  • Java进阶(List)——面试时List常见问题解读 & 结合源码分析

关于的Set的博客文章如下:

  • Java进阶(Set)——面试时Set常见问题解读 & 结合源码分析

关于HaseMap的博客文章如下:

  • Java进阶(HashMap)——面试时HashMap常见问题解读 & 结合源码分析

其他关于 数据结构 以及 多线程 的文章如下:

  • 数据结构与算法(Data Structures and Algorithm)——跟着Mark Allen Weiss用Java语言学习数据结构与算法
  • 【合集】Java进阶——Java深入学习的笔记汇总 & 再论面向对象、数据结构和算法、JVM底层、多线程、类加载 …

Java进阶(ConcurrentHashMap)——面试时ConcurrentHashMap常见问题解读 & 结合源码分析 & 多线程CAS比较并交换 初识,Java,java,面试,开发语言

引出


1.从结构上来说jdk7中,ConcurentHashMap是采用Segment段数组 + Entry数组 + 单链表结构;
2.从结构上来说jdk8中,ConcurentHashMap 与 HashMap的结构是一模一样的
3.ConcurrentHashMap 不支持 key 或者 value 为 null ,避免歧义;
4.JDK1.7和1.8的区别:

  1. 数据结构:取消了 Segment 分段锁的数据结构,取而代之的是数组+链表+红黑树的结构。
  2. 保证线程安全机制:JDK1.7 采用 Segment 的分段锁机制实现线程安全,其中 Segment 继承自 ReentrantLock 。JDK1.8 采用CAS+sizeCtl+synchronized保证线程安全。
  3. 锁的粒度:JDK1.7 是对需要进行数据操作的 Segment 加锁,JDK1.8 调整为对每个数组元素加锁(Node)。
  4. 链表转化为红黑树:定位节点的 hash 算法简化会带来弊端,hash 冲突加剧,因此在链表节点数量到达 8(且数据总量大于等于 64)时,会将链表转化为红黑树进行存储。
  5. 查询时间复杂度:从 JDK1.7的遍历链表O(n), JDK1.8 变成遍历红黑树O(logN)。

5.CAS是一种乐观锁机制,也被称为无锁机制文章来源地址https://www.toymoban.com/news/detail-735860.html

核心:线程安全

ConcurentHashMap 在JDK1.7 和JDK1.8的区别?ConcurentHashMap 是怎么保证线程安全的?

  1. 从结构上来说jdk7中,ConcurentHashMap是采用Segment段数组 + Entry数组 + 单链表结构

分段,每个段里面都是hashmap,本质还是以hashmap为单位上锁

Java进阶(ConcurrentHashMap)——面试时ConcurrentHashMap常见问题解读 & 结合源码分析 & 多线程CAS比较并交换 初识,Java,java,面试,开发语言

  1. 从结构上来说jdk8中,ConcurentHashMap 与 HashMap的结构是一模一样的

  2. 从线程安全来说,jdk7中,ConcurentHashMap 内部维护的是一个Segment(段)数组,Segment数组中每个元素又是一个HashEntry数组

    Java进阶(ConcurrentHashMap)——面试时ConcurrentHashMap常见问题解读 & 结合源码分析 & 多线程CAS比较并交换 初识,Java,java,面试,开发语言

    Segment继承了ReentrantLock这个类,所以segment自然就可以扮演锁的角色,每一个segment相当于一把锁,这就是分段锁

    当其他线程在需要进行put操作时,需要先去获取该对象的锁资源,然而当发现锁资源被占用的时候,该线程会先去进行节点的创建避免线程的空闲,这种思想也叫作预创建的思想

    因为segment在初始化后是不会扩容的,HashEntry数组是会扩容的,与HashMap机制一样,所以HashEntry是依靠于segment锁来维护安全,所以HashEntry的扩容也是线程安全的

1.什么时候初始化

jdk8中,ConcurentHashMap 因为结构变为了 数组+链表+红黑树 结构,所以维护线程安全的机制页相对发生了一些变化,和HashMap同样的,ConcurentHashMap 在put第一个元素时,才会执行初始化

Java进阶(ConcurrentHashMap)——面试时ConcurrentHashMap常见问题解读 & 结合源码分析 & 多线程CAS比较并交换 初识,Java,java,面试,开发语言

final V putVal(K key, V value, boolean onlyIfAbsent) {
     if (key == null || value == null) throw new NullPointerException();
     int hash = spread(key.hashCode()); //根据KEY计算hash值
     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(); //如果数组为null时,表示第一次put,则先进行初始化
         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
         }
         ...

2.如何保证初始化安全 sizeCtl

Java进阶(ConcurrentHashMap)——面试时ConcurrentHashMap常见问题解读 & 结合源码分析 & 多线程CAS比较并交换 初识,Java,java,面试,开发语言

此处核心为sizeCtl

  • sizeCtl的不同值表示不同的含义
  • sizeCtl = 0 代表数组还未初始化
  • sizeCtl > 0 如果数组已经初始化,那么表示 扩容阈值
  • sizeCtl = -1 表示数组正在初始化
  • sizeCtl < -1 表示数组正在扩容,并且正在被多线程初始化中

sizeCtl 这个值,就保证了多线程并发状态下,数组的初始化安全

Java进阶(ConcurrentHashMap)——面试时ConcurrentHashMap常见问题解读 & 结合源码分析 & 多线程CAS比较并交换 初识,Java,java,面试,开发语言

核心为compareAndSwapInt,比较主存中数据和当前内存中数据是否相同,如果不同代表有别的线程正在操作这个数据那么就返回false,退回重新争取时间片,此处就是保证并发时线程安全的核心。

Java进阶(ConcurrentHashMap)——面试时ConcurrentHashMap常见问题解读 & 结合源码分析 & 多线程CAS比较并交换 初识,Java,java,面试,开发语言

private final Node<K,V>[] initTable() {
     Node<K,V>[] tab; int sc;
 //循环判断数组是否为空/null
     while ((tab = table) == null || tab.length == 0) {
         //此处核心为sizeCtl
         /*
             sizeCtl的不同值表示不同的含义
             sizeCtl = 0 代表数组还未初始化
             sizeCtl > 0 如果数组已经初始化,那么表示 扩容阈值
             sizeCtl = -1 表示数组正在初始化
             sizeCtl < -1 表示数组正在扩容,并且正在被多线程初始化中
         */
         if ((sc = sizeCtl) < 0) //此处表示数组正在被某个线程初始化
             Thread.yield(); // lost initialization race; just spin //释放CPU时间片
         //核心为compareAndSwapInt,比较主存中数据和当前内存中数据是否相同,如果不同代表有别的线程正在操作这个数据那么就返回false,退回重新争取
         //时间片
         //此处就是保证并发时线程安全的核心
         else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { 
             try {
                 if ((tab = table) == null || tab.length == 0) {
                     int n = (sc > 0) ? sc : DEFAULT_CAPACITY; //得到了数组长度是否为自己设置还是默认16
                     @SuppressWarnings("unchecked")
                     Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n]; //new出数组
                     table = tab = nt;//数组赋值
                     sc = n - (n >>> 2);
                 }
             } finally {
                 sizeCtl = sc;  
             }
             break;
         }
     }
     return tab;
 }

CAS + sizeCtl 保证了初始化数据的安全

3.putval方法存入元素,加锁

数组的初始完成后,回到putval方法存入元素

Java进阶(ConcurrentHashMap)——面试时ConcurrentHashMap常见问题解读 & 结合源码分析 & 多线程CAS比较并交换 初识,Java,java,面试,开发语言

真正存入元素时,是加入了synchronized来加锁保证线程安全

CAS + sizeCtl 和 synchronized 两者共同保证了ConcurentHashMap 的线程安全!

ConcurrentHashMap 不支持 key 或者 value 为 null 的原因?

  1. 假设 ConcurrentHashMap 允许存放值为 null 的 value,这时有A、B两个线程,线程A调用ConcurrentHashMap.get(key)方法,返回为 null ,我们不知道这个 null 是没有映射的 null ,还是存的值就是 null 。
  2. 假设此时,返回为 null 的真实情况是没有找到对应的 key。那么,我们可以用 ConcurrentHashMap.containsKey(key)来验证我们的假设是否成立,我们期望的结果是返回 false 。
  3. 但是在我们调用 ConcurrentHashMap.get(key)方法之后,containsKey方法之前,线程B执行了ConcurrentHashMap.put(key, null)的操作。那么我们调用containsKey方法返回的就是 true 了,这就与我们的假设的真实情况不符合了,这就有了二义性。

JDK1.7 与 JDK1.8 中ConcurrentHashMap 的区别?

  1. 数据结构:取消了 Segment 分段锁的数据结构,取而代之的是数组+链表+红黑树的结构。
  2. 保证线程安全机制:JDK1.7 采用 Segment 的分段锁机制实现线程安全,其中 Segment 继承自 ReentrantLock 。JDK1.8 采用CAS+sizeCtl+synchronized保证线程安全。
  3. 锁的粒度:JDK1.7 是对需要进行数据操作的 Segment 加锁,JDK1.8 调整为对每个数组元素加锁(Node)。
  4. 链表转化为红黑树:定位节点的 hash 算法简化会带来弊端,hash 冲突加剧,因此在链表节点数量到达 8(且数据总量大于等于 64)时,会将链表转化为红黑树进行存储。
  5. 查询时间复杂度:从 JDK1.7的遍历链表O(n), JDK1.8 变成遍历红黑树O(logN)。

涉及到的多线程知识

CAS是啥?

CAS(CompareAnd Swap),就是比较并交换,是解决多线程情况下,解决使用锁造成性能损耗问题的一种机制。

CAS是一种乐观锁机制,也被称为无锁机制。全称: Compare-And-Swap。它是并发编程中的一种原子操作,通常用于多线程环境下实现同步和线程安全。CAS操作通过比较内存中的值与期望值是否相等来确定是否执行交换操作。如果相等,则执行交换操作,否则不执行。由于CAS是一种无锁机制,因此它避免了使用传统锁所带来的性能开销和死锁问题,提高了程序的并发性能。

CAS包含三个操作数:

  • 变量内存位置(V)
  • 预期的变量原值(A)
  • 变量的新值(B)

当要对变量进行修改时,先会将内存位置的值与预期的变量原值进行比较,如果一致则将内存位置更新为新值,否则不做操作,无论哪种情况都会返回内存位置当前的值。

比较并交换是啥?

Java进阶(ConcurrentHashMap)——面试时ConcurrentHashMap常见问题解读 & 结合源码分析 & 多线程CAS比较并交换 初识,Java,java,面试,开发语言

package com.tianju.test2;

import java.util.concurrent.atomic.AtomicInteger;

public class CASTest {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(5);
        System.out.println(atomicInteger.compareAndSet(5,2019));
        System.out.println(atomicInteger);
        System.out.println(atomicInteger.compareAndSet(5,2020));
        System.out.println(atomicInteger.compareAndSet(2019,2020));
        System.out.println(atomicInteger);
        System.out.println(atomicInteger.compareAndSet(2020,5));
        System.out.println(atomicInteger);
    }
}

CAS的优缺点

优点:

CAS是一种无锁机制,因此它避免了使用传统锁所带来的性能开销和死锁问题,提高了程序的并发性能。

缺点:

CAS 有自旋锁,如果不成功会一直循环,可能会给 cpu 带来很大开销;

问题就是可能会造成 “ABA”;

解决方案:解决的思路就是引入类似乐观锁的版本号控制,不止比较预期值和内存位置的值,还要比较版本号是否正确。

Java进阶(ConcurrentHashMap)——面试时ConcurrentHashMap常见问题解读 & 结合源码分析 & 多线程CAS比较并交换 初识,Java,java,面试,开发语言

AtomicStampedReference atomicStampedReference = new AtomicStampedReference(5, 1);

Java进阶(ConcurrentHashMap)——面试时ConcurrentHashMap常见问题解读 & 结合源码分析 & 多线程CAS比较并交换 初识,Java,java,面试,开发语言

package com.tianju.test2;

import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicStampedReference;

public class CASTest {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(5);
        System.out.println(atomicInteger.compareAndSet(5,2019));
        System.out.println(atomicInteger);
        System.out.println(atomicInteger.compareAndSet(5,2020));
        System.out.println(atomicInteger.compareAndSet(2019,2020));
        System.out.println(atomicInteger);
        System.out.println(atomicInteger.compareAndSet(2020,5));
        System.out.println(atomicInteger);

        System.out.println(" ########################################## ");
        AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(5, 1);

        boolean flag1 = atomicStampedReference.compareAndSet(atomicStampedReference.getReference(), 2019,
                atomicStampedReference.getStamp(), 2);
        System.out.println("从5修改为2019:"+flag1);
        System.out.println(atomicStampedReference.getReference());

        boolean flag2 = atomicStampedReference.compareAndSet(atomicStampedReference.getReference(), 2020,
                atomicStampedReference.getStamp(), 3);
        System.out.println("从2019修改为2020:"+flag2);
        System.out.println(atomicStampedReference.getReference());

        boolean flag3 = atomicStampedReference.compareAndSet(2020, 5, 3, 4);
        System.out.println("从2020修改回5:"+flag3);
        System.out.println(atomicStampedReference.getReference());


    }
}

什么是CAS机制compareAndSwapInt

CAS是Java中Unsafe类里面的方法,它的全称是CompareAndSwap,比较并交换的意思。它的主要功能是能够保证在多线程环境下,对于共享变量的修改的原子性。

如下,成员变量state,默认值是0,定义了一个方法doSomething(),这个方法的逻辑是判断state是否为0,如果为0就修改成1。这个逻辑看起来没有任何问题,但是在多线程环境下,会存在原子性的问题,因为这里是一个典型的,Read-Write的操作。

一般情况下,我们会在doSomething()这个方法上加同步锁来解决原子性问题。

Java进阶(ConcurrentHashMap)——面试时ConcurrentHashMap常见问题解读 & 结合源码分析 & 多线程CAS比较并交换 初识,Java,java,面试,开发语言

package com.tianju.test2;

public class Demo1 {
    private int state = 0;
    public void doSomething(){
        if (state==0){
            state=1;
        }
    }
}

但是,加同步锁,会带来性能上的损耗,所以,对于这类场景,我们就可以使用CAS机制来进行优化

Java进阶(ConcurrentHashMap)——面试时ConcurrentHashMap常见问题解读 & 结合源码分析 & 多线程CAS比较并交换 初识,Java,java,面试,开发语言

package com.tianju.test2;


import sun.misc.Unsafe;

public class Demo2 {
    private volatile int state = 0;
    private static final Unsafe UNSAFE = Unsafe.getUnsafe();
    private static final long stateOffset;

    static {
        try {
            stateOffset = UNSAFE.objectFieldOffset(
                    Demo2.class.getDeclaredField("state")
            );
        } catch (NoSuchFieldException e) {
            throw new RuntimeException(e);
        }
    }

    public void doSomething(){
        if (UNSAFE.compareAndSwapInt(this,stateOffset,0,1)){
            state=1;
        }
    }
}

在doSomething()方法中,我们调用了unsafe类中的compareAndSwapInt()方法来达到同样的目的,这个方法有四个参数,分别是:
当前对象实例、成员变量state在内存地址中的偏移量、预期值0、期望更改之后的值1。

CAS机制会比较state内存地址偏移量对应的值和传入的预期值0是否相等,如果相等,就直接修改内存地址中state的值为1。否则,返回false,表示修改失败,而这个过程是原子的,不会存在线程安全问题。

CompareAndSwap是一个native方法,实际上它最终还是会面临同样的问题,就是先从内存地址中读取state的值,然后去比较,最后再修改。

这个过程不管是在什么层面上实现,都会存在原子性问题。所以,CompareAndSwap的底层实现中,在多核CPU环境下,会增加一个Lock指令对缓存或者总线加锁,从而保证比较并替换这两个指令的原子性。

CAS主要用在并发场景中,比较典型的使用场景有两个:

  1. 第一个是J.U.C里面Atomic的原子实现,比如AtomicInteger,AtomicLong。
  2. 第二个是实现多线程对共享资源竞争的互斥性质,比如在AQS、ConcurrentHashMap、ConcurrentLinkedQueue等都有用到。

总结

1.从结构上来说jdk7中,ConcurentHashMap是采用Segment段数组 + Entry数组 + 单链表结构;
2.从结构上来说jdk8中,ConcurentHashMap 与 HashMap的结构是一模一样的
3.ConcurrentHashMap 不支持 key 或者 value 为 null ,避免歧义;
4.JDK1.7和1.8的区别:

  1. 数据结构:取消了 Segment 分段锁的数据结构,取而代之的是数组+链表+红黑树的结构。
  2. 保证线程安全机制:JDK1.7 采用 Segment 的分段锁机制实现线程安全,其中 Segment 继承自 ReentrantLock 。JDK1.8 采用CAS+sizeCtl+synchronized保证线程安全。
  3. 锁的粒度:JDK1.7 是对需要进行数据操作的 Segment 加锁,JDK1.8 调整为对每个数组元素加锁(Node)。
  4. 链表转化为红黑树:定位节点的 hash 算法简化会带来弊端,hash 冲突加剧,因此在链表节点数量到达 8(且数据总量大于等于 64)时,会将链表转化为红黑树进行存储。
  5. 查询时间复杂度:从 JDK1.7的遍历链表O(n), JDK1.8 变成遍历红黑树O(logN)。

5.CAS是一种乐观锁机制,也被称为无锁机制

到了这里,关于Java进阶(ConcurrentHashMap)——面试时ConcurrentHashMap常见问题解读 & 结合源码分析 & 多线程CAS比较并交换 初识的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【多线程进阶】信号量,线程安全集合类,Hashtable与ConcurrentHashMap的区别,多线程常见的面试题

    前言: 大家好,我是 良辰丫 ,今天学习多线程最后一节内容,我们主要去了解信号量,线程安全集合类,Hashtable与ConcurrentHashMap的区别,多线程常见的面试题,我们需要重点去掌握,💞💞💞 🧑个人主页:良辰针不戳 📖所属专栏:javaEE初阶 🍎励志语句:生活也许会让我们遍体鳞伤,

    2023年04月27日
    浏览(39)
  • 面试-Dubbo常见问题

    Dubbo 是一个RPC框架,包含注册中心,服务提供方,服务消费方,控制台,监控中心。 Dubbo 启动时会从注册中心拉取消费者需要的提供方信息,如果依赖的服务提供方不可用,Dubbo消费方会启动失败,并且不停的向注册中心请求提供方信息,抛出异常找不到对应的提供方。可以

    2024年02月08日
    浏览(32)
  • JavaEE 面试常见问题

    Mybatis 是一种典型的半自动的 ORM 框架,所谓的半自动,是因为还需要手动的写 SQL 语句,再由框架根据 SQL 及 传入数据来组装为要执行的 SQL 。其优点为: 1. 因为由程序员自己写 SQL ,相对来说学习门槛更低,更容易入门。 2. 更方便做 SQL 的性能优化及维护。 3. 对关系型数据

    2024年02月14日
    浏览(34)
  • 项目经理岗面试常见问题

    一、注意事项   ·电面邀约确认(避免hr刷KPI): 请问贵司招聘的是什么岗位,是新建团队还是原有团队? 这边面试流程是怎样的,是 leader 直接面,还是?   ·面试前铺垫: 如果您对某部分感兴趣,请随时打断我。   ·面试中发挥: 尽量采用 STAR 原则回答,即 情境( Si

    2024年02月05日
    浏览(33)
  • 单片机面试常见问题

    1、中断的概念?简述中断的过程 (1)中断:指CPU在正常执行程序的过程中,由于内部/外部事件的触发或由程序的预先安排,引起CPU暂时中断当前正在运行的程序,转而执行为内部/外部事件或程序预先安排的事件的服务子程序,待中断服务子程序执行完毕后,CPU再返回到被

    2024年04月10日
    浏览(66)
  • JVM基础,面试常见问题

    目录 一.运行时数据区域 1.线程独享 (1)栈 (2)程序计数器 2.线程共享 (1)方法区 (2)堆 二.内存如何分配 1.指针碰撞法 2.空闲列表法 3.TLAB 三.对象在内存中的组成 1.对象头 (1)markword (2)指向类型的指针 (3)如果是数组-》数组长度 2.实例数据 3.对齐填充 四.如何访

    2024年01月23日
    浏览(36)
  • 【数据结构面试常见问题】

    数据结构作为计算机的一门基础学科,它在面试中占有很大的比重,本科阶段,我们也学过数据结构与算法,内容比较多,也比较难,尤其是图的应用以及各类查找和排序算法,这些也都是核心内容。数据结构在实际的应用中也比较多,因此,整理一些常见的笔试、面试的数

    2024年03月22日
    浏览(34)
  • docker常见面试问题详解

    在面试的时候,面试官常常会问一些问题: docker是什么,能做什么? docker和虚拟机的区别是什么呢? docker是用什么做隔离的? docke的网络类型?docker数据之间是如何通信的? docker的数据保存问题? 常用的docker命令? docker制作镜像相关? 下面,就让我来详细说明一些这些问

    2024年02月10日
    浏览(30)
  • List常见面试问题

    Java中的List是一种存放有序的、可以重复的数据的集合,它允许重复元素的存在。List中的元素都有对应的一个序列号(索引)记录着元素的位置,因此可以通过这个序列号来访问元素。 ‍ Java中的List有三种实现方式:ArrayList、LinkedList和Vector。其中,ArrayList是基于数组实现的,

    2024年02月09日
    浏览(25)
  • kubernetes常见面试问题详解

    在面试的时候,面试官常常会问一些问题: k8s是什么?有什么用? k8s由哪些组件组成? pod的启动流程? k8s里有哪些控制器? k8s的调度器里有哪些调度算法? pod和pod之间的通信过程? 外面用户访问pod数据流程? 你常用哪些命令? 容器的类型? 3种探针? pod的升级? HPA、V

    2024年02月10日
    浏览(31)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包