ConcurrentHashMap为什么是线程安全的?

2023-11-10

1、ConcurrentHashMap的原理和结构

我们都知道Hash表的结构是数组加链表,就是一个数组中,每一个元素都是一个链表,有时候也把会形象的把数组中的每个元素称为一个“桶”。在插入元素的时候,首先通过对传入的键(key),进行一个哈希函数的处理,来确定元素应该存放于数组中哪个一个元素的链表中。
这种数据结构在很多计算机语言中都能找到其身影,在Java中如HashMap,ConcurrentHashMap等都是这种数据结构。
但是这中数据结构在实现HashMap的时候并不是线程安全的,因为在HashMap扩容的时候,是会将原先的链表迁移至新的链表数组中,在迁移过程中多线程情况下会有造成链表的死循环情况(JDK1.7之前的头插法);还有就是在多线程插入的时候也会造成链表中数据的覆盖导致数据丢失。

所以就出现了线程安全的HashMap类似的hash表集合,典型的就是HashTable和ConcurrentHashMap.

HashTable实现线程安全的代价比较大,那就是所有有可能产生竞争的方法里都加上了synchronized,这就导致在出现竞争时,只能一个线程对整个HashTable进行操作,其他线程都需要阻塞等待当前取到锁的线程执行完成,这样效率非常低。

而ConcurrentHashMap解决线程安全的方式,它避免了对整个Map进行加锁,从而提高了并发的效率

JDK1.7版本的ConcurrentHashMap采用分段锁的形式,每一段分一个Segment类,他内部类似HashMap的结构,内部有一个Entry数组,数组的每一个元素是一个链表,同时Segment继承自

ReentrantLock。

结构如下:

在HashEntry中采用volatile来修饰,HashEntry的当前值和next元素的值。所以get方法在获取数据的时候是不需要加锁的,这样就大大提高了执行效率

在执行put()方法的时候先尝试获取锁(tryLock()),如果获取失败,说明存在竞争,那么通过scanAndLockForPut()方法自旋,当自旋次数达到MAX_SCAN_RETRIES时会执行阻塞锁,直到获取锁成功。

static final int MAX_SCAN_RETRIES =
            Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;

final V put(K key, int hash, V value, boolean onlyIfAbsent) {
    // 首先尝试获取锁,获取失败则执行自旋,自旋次数超过最大长度,后改为阻塞锁,直到获取锁成功。
     HashEntry<K,V> node = tryLock() ? null :
         scanAndLockForPut(key, hash, value);
     V oldValue;
     try {
         HashEntry<K,V>[] tab = table;
         int index = (tab.length - 1) & hash;
         HashEntry<K,V> first = entryAt(tab, index);
         for (HashEntry<K,V> e = first;;) {
             if (e != null) {
                 K k;
                 if ((k = e.key) == key ||
                     (e.hash == hash && key.equals(k))) {
                     oldValue = e.value;
                     if (!onlyIfAbsent) {
                         e.value = value;
                         ++modCount;
                     }
                     break;
                 }
                 e = e.next;
             }
             else {
                 if (node != null)
                     node.setNext(first);
                 else
                     node = new HashEntry<K,V>(hash, key, value, first);
                 int c = count + 1;
                 if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                     rehash(node);
                 else
                     setEntryAt(tab, index, node);
                 ++modCount;
                 count = c;
                 oldValue = null;
                 break;
             }
         }
     } finally {
         unlock();
     }
     return oldValue;
 }

JDK1.8之后的ConcurrentHashMap

在JDK1.8版本中采用了CAS+synchronized的方法来保证并发,线程安全

put的源码如下:

public V put(K key, V value) {
        return putVal(key, value, false);
    }

    /** Implementation for put and putIfAbsent */
    final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
		//1、计算出hash值
        int hash = spread(key.hashCode());
        int binCount = 0;
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
			//2、判断当前数据结构是否从未放过数据,即是否未初始化,为空则先执行初始化
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
			//3、通过key的hash判断当前位置是否为null
            //(通过数组长度减一和hash做与运算得到要判断的当前数组位置)
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
				//如果当前位置为null,则通过CAS写入,如果CAS写入失败,通过自旋保证写入成功
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
			//4、当前hash值等于MOVED(-1)时,需要进行扩容
            else if ((fh = f.hash) == MOVED)
				//扩容
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
				//5、当上面的内容都不满足时,采用synchronized阻塞锁,来将数据进行写入
                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;
                            }
                        }
                    }
                }
                if (binCount != 0) {
					//6、如果数量大于TREEIFY_THRESHOLD(8),需要转化为红黑树
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);
        return null;
    }

步骤:

1、计算出hash值

2、判断当前数据结构是否从未放过数据,即是否未初始化,为空则先执行初始化

3、通过key的hash判断当前位置是否为null

4、如果当前位置为null,则通过CAS写入,如果CAS写入失败,通过自旋保证写入成功

5、当前hash值等于MOVED(-1)时,需要进行扩容

6、当上面的内容都不满足时,采用synchronized阻塞锁,来将数据进行写入

7、如果数量大于TREEIFY_THRESHOLD(8),需要转化为红黑树

JDK1.8ConcurrentHashMap的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;
    }

1、根据key的hash寻找到具体的位置

2、如果是红黑树就按照红黑树的方式去查找数据

3、如果是链表就按照链表的方式去查找数据

总结:

1、JDK1.7给Segment添加ReentrantLock锁来实现线程安全

2、JDK1.8通过CAS或者synchronized来实现线程安全

详细解释:

1、ConcurrentHashMap在JDK1.7中使用的是数组加链表的结构,其中数组分两大类,大数组segment,小数组HashEntry,而加锁是通过给Segment加ReentrantLock重入锁来保证线程安全

2、ConcurrentHashMap在JDK1.8中使用的是数组加链表加红黑树的结构,它通过CAS或synchronized来保证线程安全的,并且缩小了锁的粒度,查询性能也更高

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

ConcurrentHashMap为什么是线程安全的? 的相关文章

  • c++模板元

    模版元 主要解决递归加速 单纯的递归会反复调用 函数等待 返回 所需时间多 模版元编译的时候慢 代码会增加 把运行时间节约在编译时 template

随机推荐

  • 深度解析V-REP Remote API (MATLAB) 的应用

    OS Win10 x64 V REP V REP PRO EDU 3 5 0 MATLAB 2016b 下面我们来聊一聊V REP中MATLAB远程API的应用 如果你只对V REP有基本了解 对V REP的远程API不熟悉 强烈建议你先阅
  • LeetCode高频算法刷题记录10

    文章目录 1 旋转图像 中等 1 1 题目描述 1 2 解题思路 1 3 代码实现 2 组合总和 中等 2 1 题目描述 2 2 解题思路 2 3 代码实现 3 回文链表 简单 3 1 题目描述 3 2 解题思路 3 3 代码实现 4 字符
  • 基于YOLOv5的血细胞识别和计数

    VOC格式标注转为yolov5格式 原数据格式是xml文件对目标细胞注释 现在需要将这种注释转换为yolov5所需的格式 即每个图像对应一个txt文件 文件中存储该图像中全部细胞的类别和坐标 一行存储一个细胞的信息 如下图 编写脚本进行注释
  • [Unity]各种Debug方法笔记

    无论是萌新还是Dalao 遇到Bug总是难免的 拒绝反驳 所以一些好的Debug方法就显得尤为重要 这篇文章既写给自己 也给看到文章的大家一个参考 内容主 quan 要 bu 是脚本的Debug方法 ps 如有出错漏记得以我能看到的方式指出
  • COCO数据处理(二)根据自己提取的类的json文件生成对应的mask二值图并画在原图上

    文章目录 COCO数据集根据json文件生成mask二值图 文件目录 目录说明 代码 一 生成mask图 代码 二 将mask图画在原图上 效果图 COCO数据集根据json文件生成mask二值图 文件目录 目录说明 data coco a
  • java中JDBC当中请给出一个DataSource的HelloWorld例子

    马克 to win 在前面 的jdbc的Helloworld程序当中 我们用DriverManager来获取数据库连接 事实上通过这种方法获取数据库连接 是比较耗费计算机资 源的 当然了 这也是没有办法的事儿 就像我们买贵书必须花大价钱一样
  • 【Android】App开发-布局篇

    UI的开发离不开各个组件的精准布局 在我们学习了控件之后 控件篇 我们就需要对这些控件进一一排布 让它们在各个指定的位置 目录 LinearLayout线性布局 RelativeLayout布局 FrameLayout布局 TableLay
  • 【Python爬虫】将爬下来的数据保存到redis数据库里面

    redis库中的Redis类对Hash数据类型操作的常用方法 方法名 具体说明 hset name key value 哈希中添加一个键值对 hmset name mapping 设置哈希中的多个键值对 hmget name keys ar
  • 逻辑架构和物理架构

    逻辑架构和物理架构 理论上划分了5种架构视图 分别是 逻辑架构 开发架构 运行架构 物理架构 数据架构 逻辑架构 逻辑架构关注的是功能 包含用户直接可见的功能 还有系统中隐含的功能 或者更加通俗来描述 逻辑架构更偏向我们日常所理解的 分层
  • HTML学习(二)HTML基础

    以这个为例 h1 我的第一个标题 h1 p 我的第一个段落 p DOCTYPE 前用来申明这是一个html 这里的html不区分大小写 HTML标题 HTML 标题 Heading 是通过 h1 h6 标签来定义的 h1 1级标题 h1 H
  • R语言优雅地修改列名称

    R语言优雅地修改列名称 在R语言中 修改数据框 DataFrame 或矩阵 Matrix 的列名称是一项常见的任务 通过优雅地修改列名称 可以提高代码的可读性和可维护性 在本文中 我将介绍几种优雅的方法来修改列名称 并提供相应的源代码示例
  • GPU计算

    文章目录 GPU计算 1 GPU和CPU的区别 2 GPU的主要参数解读 3 如何在pytorch中使用GPU 4 市面上主流GPU的选择 GPU计算 1 GPU和CPU的区别 设计目标不同 CPU基于低延时 GPU基于高吞吐 CPU 处理
  • 95-34-025-Context-AbstractChannelHandlerContext

    文章目录 1 概述 2 继承体系 3 类签名 4 关键字段 5 构造方法 6 ChannelRead事件 6 1 findContextInbound 7 invokeHandler 1 概述 2 继承体系
  • tensorrt转换模型进行了哪些操作

    对于网络layer graph进行的操作 消除输出未使用的层 消除相当于无操作的操作 卷积 偏置和ReLU运算的融合 具有足够相似参数和相同源张量的运算聚合 例如 GoogleNet v5的初始模块中的1x1卷积 inception结构中同
  • DW9718AF.c

    Copyright C 2015 MediaTek Inc This program is free software you can redistribute it and or modify it under the terms of
  • ERP系统总体解决方案 附下载地址

    企业资源计划即 ERP Enterprise Resource Planning 由美国 Gartner Group 公司于1990年提出 企业资源计划是 MRP II 企业制造资源计划 下一代的制造业系统和资源计划软件 除了MRP II
  • 大数据学习框架及指南

    Hadoop生态圈 一 采集 数据从哪里来 主要包括flume等 一 存储 海量的数据怎样有效的存储 主要包括hdfs Kafka 二 计算 海量的数据怎样快速计算 主要包括MapReduce Spark storm等 三 查询 海量数据怎
  • 数据结构与算法笔记2(线性表)

    1 线性表 1 1线性表是一种逻辑关系 见绪论 1 2定义 是具有相同类型的n个元素的有限序列 其中n为表长 n 0时为空表 关键词 相同类型 一般处理的数据元素都是相同类型 比如一个人那么都是人 而不会把人与车放在一起 关键词 有限序列
  • Java泛型知识点整理

    Java泛型知识点整理 Java泛型 泛型提供了编译时类型安全检测机制 该机制允许程序员在编译时检测到非法的类型 泛型的本质是参数化类型 也就是说所操作的数据类型被指定为一个参数 比如我们要写一个排序方法 能够对整型数组 字符串数组甚至其他
  • ConcurrentHashMap为什么是线程安全的?

    1 ConcurrentHashMap的原理和结构 我们都知道Hash表的结构是数组加链表 就是一个数组中 每一个元素都是一个链表 有时候也把会形象的把数组中的每个元素称为一个 桶 在插入元素的时候 首先通过对传入的键 key 进行一个哈希