ConcurrentHashMap

简介: Java7版本的ConcurrentHashMapJava8版本的ConcurrentHashMap红黑树的特点是,每个节点都是带有颜色属性的二叉查找树,红黑树的本质是对二叉查找树BST的一种平和策略。红黑树的一些其他特点:每个节点要么是红色的,要么是黑色的,但根节点永远是黑色的

Java7版本的ConcurrentHashMap

Java8版本的ConcurrentHashMap

红黑树的特点是,每个节点都是带有颜色属性的二叉查找树,红黑树的本质是对二叉查找树BST的一种平和策略。

红黑树的一些其他特点:

每个节点要么是红色的,要么是黑色的,但根节点永远是黑色的

红色节点不能连续,也就是说,红色节点的子和父都不能是红色的

从任一节点到其每个叶子节点的路径都包含相同数量的黑色节点。

红黑树有自平衡的特点,即便是在极端的情况下,查询也比较快。

分析Java8版本的ConcurrentHashMap的重要源码

Node 节点
我们先来看看最基础的内部存储结构 Node,这就是一个一个的节点,如这段代码所示:

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    volatile V val;
    volatile Node<K,V> next;
    // ...
}

可以看出,每个 Node 里面是 key-value 的形式,并且把 value 用 volatile 修饰,以便保证可见性,同时内部还有一个指向下一个节点的 next 指针,方便产生链表结构。

下面我们看两个最重要、最核心的方法。

put 方法源码分析

put 方法的核心是 putVal 方法,为了方便阅读,我把重要步骤的解读用注释的形式补充在下面的源码中。我们逐步分析这个最重要的方法,这个方法相对有些长,我们一步一步把它看清楚。

final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) {
        throw new NullPointerException();
    }
    //计算 hash 值
    int hash = spread(key.hashCode());
    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();
        }
        // 找该 hash 值对应的数组下标
        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;
            }
        }
        //hash值等于 MOVED 代表在扩容
        else if ((fh = f.hash) == MOVED) {
            tab = helpTransfer(tab, f);
        }
        //槽点上是有值的情况
        else {
            V oldVal = null;
            //用 synchronized 锁住当前槽点,保证并发安全
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    //如果是链表的形式
                    if (fh >= 0) {
                        binCount = 1;
                        //遍历链表
                        for (Node<K, V> e = f; ; ++binCount) {
                            K ek;
                            //如果发现该 key 已存在,就判断是否需要进行覆盖,然后返回
                            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;
                            //到了链表的尾部也没有发现该 key,说明之前不存在,就把新值添加到链表的最后
                            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;
                        //调用 putTreeVal 方法往红黑树里增加数据
                        if ((p = ((TreeBin<K, V>) f).putTreeVal(hash, key,
                                value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent) {
                                p.val = value;
                            }
                        }
                    }
                }
            }
            if (binCount != 0) {
                //检查是否满足条件并把链表转换为红黑树的形式,默认的 TREEIFY_THRESHOLD 阈值是 8
                if (binCount >= TREEIFY_THRESHOLD) {
                    treeifyBin(tab, i);
                }
                //putVal 的返回是添加前的旧值,所以返回 oldVal
                if (oldVal != null) {
                    return oldVal;
                }
                break;
            }
        }
    }
    addCount(1L, binCount);
    return null;
}

通过以上的源码分析,我们对于 putVal 方法有了详细的认识,可以看出,方法中会逐步根据当前槽点是未初始化、空、扩容、链表、红黑树等不同情况做出不同的处理。

get 方法源码分析
get 方法比较简单,我们同样用源码注释的方式来分析一下:

public V get(Object key) {
    Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
    //计算 hash 值
    int h = spread(key.hashCode());
    //如果整个数组是空的,或者当前槽点的数据是空的,说明 key 对应的 value 不存在,直接返回 null
    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;
        }
        //如果头结点 hash 值小于 0,说明是红黑树或者正在扩容,就用对应的 find 方法来查找
        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 值,并由此值找到对应的槽点;
如果数组是空的或者该位置为 null,那么直接返回 null 就可以了;
如果该位置处的节点刚好就是我们需要的,直接返回该节点的值;
如果该位置节点是红黑树或者正在扩容,就用 find 方法继续查找;
否则那就是链表,就进行遍历链表查找。

对比JDK1.7和1.8的异同点和优缺点

数据结构jdk1.7采用的是Segment分段锁来实现

jdk1.8中的ConcurrentHashMap使用数组+链表+红黑树。

并发度:

Java7:每个Segment独立加锁,最大并发个数就是Segment的个数(默认是16)

Java8:每个node是独立的,锁力度更细,数组的长度就是最大并发数,并发度比之前有提高。

保证并发安全的原理

Java7,采用Segment分段锁来保证安全,Segment是继承了ReenLock

Java8,采用Node+CAS+synchronized保证安全

遇到Hash碰撞

Java7:使用拉链法

Java8:先使用拉链法,在链表长度超过一定阀值时,将链表转换为红黑树

查询时间复杂度

Java7:遍历链表的时间复杂度为O(n),n为链表长度

Java8:如果变成遍历红黑树,时间复杂度降低为O(log(n)),n为树的节点个数。

二、为什么 Map 桶中超过 8 个才转为红黑树?

拉链法,当链表长度大于或等于阀值(默认8)的时候,如果同时还满足容量大于等于MIN_TREEIFY_CAPACITY(默认为64)的要求就会把链表转换为红黑树

当红黑树的节点小于或等于6个以后又会恢复为链表形态。

为什么需要转换:

每次遍历一个链表,平均查找的时间复杂度是O(n),n是链表的长度

链表还不是很长,O(n)和O(log(n))的区别不大

如果链表越来越长,那么这种区别便会有所体现

为了提升查找性能,需要把链表转化为红黑树的形式。

默认链表长度达到8就转化为红黑树,而当长度降到6就转换回去

体现了时间和空间平衡个思想

当链表长度越来越长,需要红黑树的形式就可以保证查询的效率

对于为什么是8,是时间和空间的一种平衡,通常情况下没必要转化为红黑树,所以就选择了概率非常小,也就是长度为8的概率,如果平时开发中发现HashMap或是ConcurrentHashMap内部出现了红黑树的结构,往往就说明我们的哈希算法出现了问题,是不是HashCode分布不均匀。

三、ConcurrentHashMap和 Hashtable的区别

它们都是线程安全的,但是他们的区别是

1、出现的版本不同

Hashtable:JDK1.0中出现,JDK1.2版本中实现了Map接口

ConcurrentHashMap:

JDK1.5中出现的,后出现的在性能上存在较大的不同

2、实现线程安全方式的不同

Hashtable实现并发安全是通过synchronized关键字

而ConcurrentHashMap实现的原理,却有大大的不同,实现原理是CAS+synchronized+Node节点的方式。

3、性能不同

Hashtable,线程数量增加的时候性能会极具下降

每一次修改都会需要锁住整个对象,其他线程在此期间不能操作

会有额外的上下文切换等开销。多线程情况下,效率不一定高于单线程

ConcurrentHashMap,

仅会对一部分上锁而不是全部上锁

并发效率上,ConcurrentHashMap比Hashtable提高了很多。

4、迭代时修改的不同

Hashtable(包括HashMap),不允许在迭代期间修改内容,否则会抛出ConcurrentModificationException 异常,其原理是检测 modCount 变量,迭代器的 next() 方法的代码如下:

ConcurrentHashMap 即便在迭代期间修改内容,也不会抛出ConcurrentModificationException。

目录
相关文章
|
3天前
|
人工智能 自然语言处理 Shell
深度评测 | 仅用3分钟,百炼调用满血版 Deepseek-r1 API,百万Token免费用,简直不要太爽。
仅用3分钟,百炼调用满血版Deepseek-r1 API,享受百万免费Token。阿里云提供零门槛、快速部署的解决方案,支持云控制台和Cloud Shell两种方式,操作简便。Deepseek-r1满血版在推理能力上表现出色,尤其擅长数学、代码和自然语言处理任务,使用过程中无卡顿,体验丝滑。结合Chatbox工具,用户可轻松掌控模型,提升工作效率。阿里云大模型服务平台百炼不仅速度快,还确保数据安全,值得信赖。
129301 24
深度评测 | 仅用3分钟,百炼调用满血版 Deepseek-r1 API,百万Token免费用,简直不要太爽。
|
5天前
|
人工智能 API 网络安全
用DeepSeek,就在阿里云!四种方式助您快速使用 DeepSeek-R1 满血版!更有内部实战指导!
DeepSeek自发布以来,凭借卓越的技术性能和开源策略迅速吸引了全球关注。DeepSeek-R1作为系列中的佼佼者,在多个基准测试中超越现有顶尖模型,展现了强大的推理能力。然而,由于其爆火及受到黑客攻击,官网使用受限,影响用户体验。为解决这一问题,阿里云提供了多种解决方案。
16158 37
|
13天前
|
机器学习/深度学习 人工智能 自然语言处理
PAI Model Gallery 支持云上一键部署 DeepSeek-V3、DeepSeek-R1 系列模型
DeepSeek 系列模型以其卓越性能在全球范围内备受瞩目,多次评测中表现优异,性能接近甚至超越国际顶尖闭源模型(如OpenAI的GPT-4、Claude-3.5-Sonnet等)。企业用户和开发者可使用 PAI 平台一键部署 DeepSeek 系列模型,实现 DeepSeek 系列模型与现有业务的高效融合。
|
4天前
|
并行计算 PyTorch 算法框架/工具
本地部署DeepSeek模型
要在本地部署DeepSeek模型,需准备Linux(推荐Ubuntu 20.04+)或兼容的Windows/macOS环境,配备NVIDIA GPU(建议RTX 3060+)。安装Python 3.8+、PyTorch/TensorFlow等依赖,并通过官方渠道下载模型文件。配置模型后,编写推理脚本进行测试,可选使用FastAPI服务化部署或Docker容器化。注意资源监控和许可协议。
1182 8
|
13天前
|
人工智能 搜索推荐 Docker
手把手教你使用 Ollama 和 LobeChat 快速本地部署 DeepSeek R1 模型,创建个性化 AI 助手
DeepSeek R1 + LobeChat + Ollama:快速本地部署模型,创建个性化 AI 助手
3339 117
手把手教你使用 Ollama 和 LobeChat 快速本地部署 DeepSeek R1 模型,创建个性化 AI 助手
|
8天前
|
人工智能 自然语言处理 API
DeepSeek全尺寸模型上线阿里云百炼!
阿里云百炼平台近日上线了DeepSeek-V3、DeepSeek-R1及其蒸馏版本等六款全尺寸AI模型,参数量达671B,提供高达100万免费tokens。这些模型在数学、代码、自然语言推理等任务上表现出色,支持灵活调用和经济高效的解决方案,助力开发者和企业加速创新与数字化转型。示例代码展示了如何通过API使用DeepSeek-R1模型进行推理,用户可轻松获取思考过程和最终答案。
|
5天前
|
人工智能 自然语言处理 程序员
如何在通义灵码里用上DeepSeek-V3 和 DeepSeek-R1 满血版671B模型?
除了 AI 程序员的重磅上线外,近期通义灵码能力再升级全新上线模型选择功能,目前已经支持 Qwen2.5、DeepSeek-V3 和 R1系列模型,用户可以在 VSCode 和 JetBrains 里搜索并下载最新通义灵码插件,在输入框里选择模型,即可轻松切换模型。
846 14
|
12天前
|
API 开发工具 Python
阿里云PAI部署DeepSeek及调用
本文介绍如何在阿里云PAI EAS上部署DeepSeek模型,涵盖7B模型的部署、SDK和API调用。7B模型只需一张A10显卡,部署时间约10分钟。文章详细展示了模型信息查看、在线调试及通过OpenAI SDK和Python Requests进行调用的步骤,并附有测试结果和参考文档链接。
1863 9
阿里云PAI部署DeepSeek及调用
|
9天前
|
人工智能 数据可视化 Linux
【保姆级教程】3步搞定DeepSeek本地部署
DeepSeek在2025年春节期间突然爆火出圈。在目前DeepSeek的网站中,极不稳定,总是服务器繁忙,这时候本地部署就可以有效规避问题。本文以最浅显易懂的方式带读者一起完成DeepSeek-r1大模型的本地部署。
|
11天前
|
缓存 自然语言处理 安全
快速调用 Deepseek API!【超详细教程】
Deepseek 强大的功能,在本教程中,将指导您如何获取 DeepSeek API 密钥,并演示如何使用该密钥调用 DeepSeek API 以进行调试。

热门文章

最新文章