【Java】学习J.U.C中的ConCurrentHashmap源码分析

简介: 学习了HashMap知识点,了解到HashMap不是线程安全的,因为HashMap在高并发情况下可能会出现环形链表,造成下一次读操作形成死循环。所以,要使用ConcurrentHashMap线程安全的方法。不多说了直接学习。每天进步一点点,加油。

一、ConcurrentHashMap是什么?

在面试的过程面试问完HashMap还会问安全的方法有哪些?HashMap 是线程不安全的集合类,在并发情况下可能由于线程争用导致程序获取不正确的结果。而ConcurrentHashMap 是对 HashMap 的功能增强,使 HashMap 支持高并发下的读写线程安全。看了ConcurrentHashMap源码发现很多方法和代码跟HashMap相似。
只是思路相同,实现不同而已。

二、ConcurrentHashMap方法部分源码

1.put()方法源码

    /**
     * Maps the specified key to the specified value in this table.
     * Neither the key nor the value can be null.
     */
    public V put(K key, V value) {
        return putVal(key, value, false);
    }
   final V putVal(K key, V value, boolean onlyIfAbsent) {
    // 不允许插入空值或空键
    // 允许value空值会导致get方法返回null时有两种情况:
    // 1. 找不到对应的key2. 找到了但是value为null;
    // 当get方法返回null时无法判断是哪种情况,在并发环境下containsKey方法已不再可靠,
    // 需要返回null来表示查询不到数据。允许key空值需要额外的逻辑处理,占用了数组空间,且并没有多大的实用价值。
    // HashMap支持键和值为null,但基于以上原因,ConcurrentHashMap是不支持空键值。
    if (key == null || value == null) throw new NullPointerException();
    // 高低位异或扰动hashcode,和HashMap类似
    int hash = spread(key.hashCode());
    // bincount表示链表的节点数
    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
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            // 重点:采用CAS进行插入
            if (casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null)))
                break;
        }
        // 情况三:数组正在扩容,帮忙迁移数据到新的数组
        // 同时会新数组,下次循环就是插入到新的数组
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        // 情况四:直接对节点进行加锁,插入数据
        // 下面代码很多,但逻辑和HashMap插入数据大同小异
        // 因为已经上锁,不涉及并发安全设计
        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;
                        }
                    }
                    else if (f instanceof ReservationNode)
                        throw new IllegalStateException("Recursive update");
                }
            }
            // 判断是否需要转化为红黑树,和返回旧数值
            if (binCount != 0) {
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    // 总数+1
    addCount(1L, binCount);
    return null;
}

从上面的源码可以看出ConcurrentHashMap 不支持使用 null 作为 key 或者 value。
ConcurrentHashMap添加数据时,采取了CAS+synchronize结合策略。首先会判断该节点是否为null,如果为null,尝试使用CAS添加节点;如果添加失败,说明发生了并发冲突,再对节点进行上锁并插入数据。在并发较低的情景下无需加锁,可以显著提高性能。同时只会CAS尝试一次,也不会造成线程长时间等待浪费cpu时间的情况。ConcurrentHashMap的put方法整体流程如下(并不是全部流程):
在这里插入图片描述

流程如下:

1.首先会判断数组是否已经初始化,如若未初始化,会先去初始化数组;
2.如果当前要插入的节点为null,尝试使用CAS插入数据;
3.如果不为null,则判断节点hash值是否为-1;-1表示数组正在扩容,会先去协助扩容,再回来继续插入数据。
4.最后会执行上锁,并插入数据,最后判断是否需要返回旧值;
5.如果不是覆盖旧值,需要更新map中的节点数,也就是图中的addCount方法。

注意到源码中有两个关键方法:初始化数组的initTable(),修改map中节点总数的addCount。是put插入的关键方法。

2.get()方法的源码

   public V get(Object key) {
        Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
        int h = spread(key.hashCode());
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (e = tabAt(tab, (n - 1) & h)) != null) {
            if ((eh = e.hash) == h) {
                if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                    return e.val;
            }
            else if (eh < 0)
                return (p = e.find(h, key)) != null ? p.val : null;
            while ((e = e.next) != null) {
                if (e.hash == h &&
                    ((ek = e.key) == key || (ek != null && key.equals(ek))))
                    return e.val;
            }
        }
        return null;
    }

通过上面的get()方法源码可以看出,首先要计算 hash 值,然后再根据hash 值找到数组对应位置: (n - 1) & h。最后根据该位置处结点性质进行相应找。如果该位置为 null,那么直接返回 null 。如果该位置处的节点刚好就是需要的值,就返回该节点的值。如果该位置节点的 hash 值小于 0,说明容量不足正在扩容,或者是红黑树进行遍历对比查询数据。

三、可能碰到的一些问题

有了解过ConcurrentHashMap吗?它的存储结构是什么样的?

作为Java开发工程师一定要学习和查看ConcurrentHashMap源码数据结构知识点,
ConcurrentHashMap数据结构跟HashMap一样数组+链表+红黑树实现的。

ConcurrentHashMap1.7和1.8的区别

1.7 版本数组+链表,Segment+HashEntry。锁的粒度基于segment,包含多个HashEntry。锁的粒度比较大。
1.8 版本数组+链表+红黑树(Node+CAS+Synchronized),锁的粒度就是node,大大降低了锁的粒度。

1.8版本ConcurrentHashMap使用什么技术来保证线程安全?

采用了Node+CAS+Synchronized锁的方式来保证线程安全

ConcurrentHashMap默认初始容量是多少?

1.8版本中ConcurrentHashMap默认初始容量是16,可以根据业务需要设置容量大小,不设置就是取默认值。

ConCurrentHashmap 的key,value是否可以为null。

都不可以为null ,如果Key或者Value为null会报空指针异常。

ConcurrentHashMap有哪些构造函数?

有五个构造函数,分别是:无参构造函数,可传入容器大小的构造函数,可传入Map的构造函数,可设置阈值和初始容量的构造函数,可设置初始容量和阈值并发级别等五种,具体源码可查看以下部分

  //无参构造函数
    public ConcurrentHashMap() {
    }
    //可传初始容器大小的构造函数
    public ConcurrentHashMap(int initialCapacity) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException();
        int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
                   MAXIMUM_CAPACITY :
                   tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
        this.sizeCtl = cap;
    }
    //可传入map的构造函数
    public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
        this.sizeCtl = DEFAULT_CAPACITY;
        putAll(m);
    }
    //可设置阈值和初始容量
    public ConcurrentHashMap(int initialCapacity, float loadFactor) {
        this(initialCapacity, loadFactor, 1);
    }

    //可设置初始容量和阈值和并发级别
    public ConcurrentHashMap(int initialCapacity,
                             float loadFactor, int concurrencyLevel) {
        if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
            throw new IllegalArgumentException();
        if (initialCapacity < concurrencyLevel)   // Use at least as many bins
            initialCapacity = concurrencyLevel;   // as estimated threads
        long size = (long)(1.0 + (long)initialCapacity / loadFactor);
        int cap = (size >= (long)MAXIMUM_CAPACITY) ?
            MAXIMUM_CAPACITY : tableSizeFor((int)size);
        this.sizeCtl = cap;
    }
    

ConCurrentHashmap 每次扩容是原来容量的几倍

每次扩容是原来的2倍
在transfer方法里面会创建一个原数组的俩倍的node数组来存放原数据。

ConcurrentHashMap的get方法是否要加锁,为什么?

不需要,get方法采用了unsafe方法来保证线程安全

java1.8中,ConCurrentHashmap是如何计算它的size大小的?

对于size的计算,在put()的时候使用了addCount()方法。

相关文章
|
2月前
|
XML Java 编译器
Java学习十六—掌握注解:让编程更简单
Java 注解(Annotation)是一种特殊的语法结构,可以在代码中嵌入元数据。它们不直接影响代码的运行,但可以通过工具和框架提供额外的信息,帮助在编译、部署或运行时进行处理。
95 43
Java学习十六—掌握注解:让编程更简单
|
1月前
|
Java 大数据 API
14天Java基础学习——第1天:Java入门和环境搭建
本文介绍了Java的基础知识,包括Java的简介、历史和应用领域。详细讲解了如何安装JDK并配置环境变量,以及如何使用IntelliJ IDEA创建和运行Java项目。通过示例代码“HelloWorld.java”,展示了从编写到运行的全过程。适合初学者快速入门Java编程。
|
2月前
|
存储 SQL 小程序
JVM知识体系学习五:Java Runtime Data Area and JVM Instruction (java运行时数据区域和java指令(大约200多条,这里就将一些简单的指令和学习))
这篇文章详细介绍了Java虚拟机(JVM)的运行时数据区域和JVM指令集,包括程序计数器、虚拟机栈、本地方法栈、直接内存、方法区和堆,以及栈帧的组成部分和执行流程。
36 2
JVM知识体系学习五:Java Runtime Data Area and JVM Instruction (java运行时数据区域和java指令(大约200多条,这里就将一些简单的指令和学习))
|
1月前
|
JavaScript Java 项目管理
Java毕设学习 基于SpringBoot + Vue 的医院管理系统 持续给大家寻找Java毕设学习项目(附源码)
基于SpringBoot + Vue的医院管理系统,涵盖医院、患者、挂号、药物、检查、病床、排班管理和数据分析等功能。开发工具为IDEA和HBuilder X,环境需配置jdk8、Node.js14、MySQL8。文末提供源码下载链接。
|
2月前
|
小程序 Oracle Java
JVM知识体系学习一:JVM了解基础、java编译后class文件的类结构详解,class分析工具 javap 和 jclasslib 的使用
这篇文章是关于JVM基础知识的介绍,包括JVM的跨平台和跨语言特性、Class文件格式的详细解析,以及如何使用javap和jclasslib工具来分析Class文件。
52 0
JVM知识体系学习一:JVM了解基础、java编译后class文件的类结构详解,class分析工具 javap 和 jclasslib 的使用
|
2月前
|
Java
Java基础之 JDK8 HashMap 源码分析(中间写出与JDK7的区别)
这篇文章详细分析了Java中HashMap的源码,包括JDK8与JDK7的区别、构造函数、put和get方法的实现,以及位运算法的应用,并讨论了JDK8中的优化,如链表转红黑树的阈值和扩容机制。
32 1
|
2月前
|
前端开发 Java 应用服务中间件
Javaweb学习
【10月更文挑战第1天】Javaweb学习
34 2
|
2月前
|
存储 缓存 Java
【用Java学习数据结构系列】HashMap与TreeMap的区别,以及Map与Set的关系
【用Java学习数据结构系列】HashMap与TreeMap的区别,以及Map与Set的关系
41 1
|
2月前
|
存储 算法 Java
带你学习java的数组军队列
带你学习java的数组军队列
36 0
|
2月前
|
Java 大数据 开发工具
java学习——环境准备(1)
java学习——环境准备(1)
42 0