我的编程空间,编程开发者的网络收藏夹
学习永远不晚

ConcurrentHashMap原理详解(太细了)

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

ConcurrentHashMap原理详解(太细了)

一、什么是ConcurrentHashMap

ConcurrentHashMapHashMap一样,是一个存放键值对的容器。使用hash算法来获取值的地址,因此时间复杂度是O(1)。查询非常快。
同时,ConcurrentHashMap是线程安全的HashMap。专门用于多线程环境。

二、ConcurrentHashMap和HashMap以及Hashtable的区别

2.1 HashMap

HashMap是线程不安全的,因为HashMap中操作都没有加锁,因此在多线程环境下会导致数据覆盖之类的问题,所以,在多线程中使用HashMap是会抛出异常的。

2.2 HashTable

HashTable是线程安全的,但是HashTable只是单纯的在put()方法上加上synchronized。保证插入时阻塞其他线程的插入操作。虽然安全,但因为设计简单,所以性能低下。

2.3 ConcurrentHashMap

ConcurrentHashMap是线程安全的,ConcurrentHashMap并非锁住整个方法,而是通过原子操作和局部加锁的方法保证了多线程的线程安全,且尽可能减少了性能损耗。

由此可见,HashTable可真是一无是处…

三、ConcurrentHashMap原理

这一节专门介绍ConcurrentHashMap是如何保证线程安全的。如果想详细了解ConcurrentHashMap的数据结构,请参考HashMap

3.1 volatile修饰的节点数组

请看源码

//ConcurrentHashMap使用volatile修饰节点数组,保证其可见性,禁止指令重排。transient volatile Node[] table;

再看看HashMap是怎么做的

//HashMap没有用volatile修饰节点数组。transient Node[] table;

显然,HashMap并不是为多线程环境设计的。

3.2 ConcurrentHashMap的put()方法

//put()方法直接调用putVal()方法public V put(K key, V value) { return putVal(key, value, false);}//所以直接看putVal()方法。final V putVal(K key, V value, boolean onlyIfAbsent) {    if (key == null || value == null) throw new NullPointerException();    int hash = spread(key.hashCode());    int binCount = 0;    for (Node[] tab = table;;) {        Node f; int n, i, fh;        if (tab == null || (n = tab.length) == 0)            tab = initTable();        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {            if (casTabAt(tab, i, null,                         new Node(hash, key, value, null)))                break;                   // no lock when adding to empty bin        }        else if ((fh = f.hash) == MOVED)            tab = helpTransfer(tab, f);        else {            V oldVal = null;            synchronized (f) {                if (tabAt(tab, i) == f) {                    if (fh >= 0) {                        binCount = 1;                        for (Node 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 pred = e;if ((e = e.next) == null) {    pred.next = new Node(hash, key,  value, null);    break;}                        }                    }                    else if (f instanceof TreeBin) {                        Node p;                        binCount = 2;                        if ((p = ((TreeBin)f).putTreeVal(hash, key,                           value)) != null) {oldVal = p.val;if (!onlyIfAbsent)    p.val = value;                        }                    }                }            }            if (binCount != 0) {                if (binCount >= TREEIFY_THRESHOLD)                    treeifyBin(tab, i);                if (oldVal != null)                    return oldVal;                break;            }        }    }    addCount(1L, binCount);    return null;}

我来给大家讲解一下步骤把。

public V put(K key, V value) {

首先,put()方法是没有用synchronized修饰的。

for (Node[] tab = table;;)

新插入一个节点时,首先会进入一个死循环
情商高的就会说,这是一个乐观锁
进入乐观锁后,

if (tab == null || (n = tab.length) == 0)    tab = initTable();

如果tab未被初始化,则先将tab初始化。此时,这轮循环结束,因为被乐观锁锁住,开始下一轮循环。
第二轮循环,此时tab已经被初始化了,所以跳过。

else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {    if (casTabAt(tab, i, null,                 new Node(hash, key, value, null)))        break;                   // no lock when adding to empty bin}

接下来通过keyhash值来判断table中是否存在相同的key,如果不存在,执行casTabAt()方法。
注意,这个操作时不加锁的,看到里面的那行注释了么// no lock when adding to empty bin。位置为空时不加锁。
这里其实是利用了一个CAS操作。

CAS(Compare-And-Swap):比较并交换

这里就插播一个小知识,CAS就是通过一个原子操作,用预期值去和实际值做对比,如果实际值和预期相同,则做更新操作。
如果预期值和实际不同,我们就认为,其他线程更新了这个值,此时不做更新操作。
而且这整个流程是原子性的,所以只要实际值和预期值相同,就能保证这次更新不会被其他线程影响。

好了,我们继续。
既然这里用了CAS操作去更新值,那么就存在两者情况。

  1. 实际值和预期值相同
    相同时,直接将值插入,因为此时是线程安全的。好了,这时插入操作完成。使用break;跳出了乐观锁。循环结束。
  2. 实际值和预期值不同
    不同时,不进行操作,因为此时这个值已经被其他线程修改过了,此时这轮操作就结束了,因为还被乐观锁锁住,进入第三轮循环。

第三轮循环中,前面的判断又会重新执行一次,我就跳过不说了,进入后面的判断。

 else if ((fh = f.hash) == MOVED)    tab = helpTransfer(tab, f);

这里判断的是tab的状态,MOVED表示在扩容中,如果在扩容中,帮助其扩容。帮助完了后就会进行第四轮循环。
终于,来到了最后一轮循环。

else {    V oldVal = null;    synchronized (f) {        if (tabAt(tab, i) == f) {            if (fh >= 0) {                binCount = 1;                for (Node 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 pred = e;                    if ((e = e.next) == null) {                        pred.next = new Node(hash, key,                      value, null);                        break;                    }                }            }            else if (f instanceof TreeBin) {                Node p;                binCount = 2;                if ((p = ((TreeBin)f).putTreeVal(hash, key,                   value)) != null) {                    oldVal = p.val;                    if (!onlyIfAbsent)                        p.val = value;                }            }        }    }    if (binCount != 0) {        if (binCount >= TREEIFY_THRESHOLD)            treeifyBin(tab, i);        if (oldVal != null)            return oldVal;        break;    }}

上面的判断都不满足时,就会进入最后的分支,这条分支表示,keyhash值位置不为null(之前的判断是hash值为null时直接做插入操作),表示发生了hash冲突,此时节点就要通过链表的形式存储这个插入的新值。Node类是有next字段的,用来指向链表的下一个位置,新节点就往这插。

synchronized (f) {

看,终于加排它锁了,只有在发生hash冲突的时候才加了排它锁。

if (tabAt(tab, i) == f) {if (fh >= 0) {

重新判断当前节点是不是第二轮判断过的节点,如果不是,表示节点被其他线程改过了,进入下一轮循环,
如果是,再次判断是否在扩容中,如果是,进入下一轮循环,
如果不是,其他线程没改过,继续走,

for (Node e = f;; ++binCount) {

for循环,循环遍历这个节点上的链表,

if (e.hash == hash &&    ((ek = e.key) == key ||     (ek != null && key.equals(ek)))) {    oldVal = e.val;    if (!onlyIfAbsent)        e.val = value;    break;}

找到一个hash值相同,且key也完全相同的节点,更新这个节点。
如果找不到

if ((e = e.next) == null) {pred.next = new Node(hash, key,  value, null);    break;}

往链表最后插入这个新节点。因为在排他锁中,这些操作都可以直接操作。终于到这插入就基本完成了。

总结

做插入操作时,首先进入乐观锁,
然后,在乐观锁中判断容器是否初始化,
如果没初始化则初始化容器,
如果已经初始化,则判断该hash位置的节点是否为空,如果为空,则通过CAS操作进行插入。
如果该节点不为空,再判断容器是否在扩容中,如果在扩容,则帮助其扩容。
如果没有扩容,则进行最后一步,先加锁,然后找到hash值相同的那个节点(hash冲突),
循环判断这个节点上的链表,决定做覆盖操作还是插入操作。
循环结束,插入完毕。

3.3 ConcurrentHashMap的get()方法

//ConcurrentHashMap的get()方法是不加锁的,方法内部也没加锁。public V get(Object key)

看上面这代码,ConcurrentHashMapget()方法是不加锁的,为什么可以不加锁?因为tablevolatile关键字修饰,保证每次获取值都是最新的。

//Hashtable的get()是加锁的,所以性能差。public synchronized V get(Object key) 

再看看Hashtable,差距啊。

四、使用场景

嗯,多线程环境下,更新少,查询多时使用的话,性能比较高。
乐观锁嘛,认为更新操作时不会被其他线程影响。所以时候再更新少的情况下性能高。

对你有帮助吗?点个赞吧~

来源地址:https://blog.csdn.net/qq_42068856/article/details/126091526

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

ConcurrentHashMap原理详解(太细了)

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

下载Word文档

猜你喜欢

ConcurrentHashMap原理详解(太细了)

一、什么是ConcurrentHashMap ConcurrentHashMap和HashMap一样,是一个存放键值对的容器。使用hash算法来获取值的地址,因此时间复杂度是O(1)。查询非常快。 同时,ConcurrentHashMap是
2023-08-17

Redis核心原理详细解说

目录1、Redis为什么这么快2、Redis网络模型3、Redis数据结构4、Redis持久化RDB快照(snapshot)AOF(append-only file)RDB与AOF区别Redis数据备份策略5、Redis管道(Pipelin
2022-07-21

JavaHashMap算法原理详细讲解

在java开发中,HashMap是最常用、最常见的集合容器类之一,文中通过示例代码介绍HashMap为啥要二次Hash,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习吧
2023-02-08

MySQL读写分离原理详细解析

目录一、读写分离的概念二、引入中间件MyCat三、http://www.cppcns.comMyCat服务端口和管理端口一、读写分离的概念读写分离是基于主从复制来实现的。在实际的应用环境中,肯定是读操作多,就像我们在电商平台上去购买东西,
2022-07-14

Yolov8原理详细解析!一文看懂

引言 Yolo(You Only Look Once)是一种one-stage目标检测算法,即仅需要 “看” 一次就可以识别出图片中物体的class类别和边界框。Yolov8是Ultralytics公司最新推出的Yolo系列目标检测算法,可
2023-08-30

Java@Autowired注解底层原理详细分析

@Autowired注解可以用在类属性,构造函数,setter方法和函数参数上,该注解可以准确地控制bean在何处如何自动装配的过程。在默认情况下,该注解是类型驱动的注入
2022-11-13

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录