ConcurrentHashMap基本介绍

news2024/9/21 19:07:22

介绍

ConcurrentHashMap是线程安全高效的HashMap。

为什么要使用ConcurrentHashMap

线程不安全的HashMap

HashMap多线程情况下put操作会出现并发安全问题,包括死循环、数据丢失(jdk7)以及数据覆盖(jdk8)

jdk7中,多线程put操作扩容采用头插法,是导致死循环(链表成环)以及数据丢失问题的根本原因(具体是transfer方法中的两行代码导致);

jdk8中,jdk8中将扩容操作改为尾插法,解决了死循环和数据丢失问题但是数据覆盖问题仍未解决,因此HashMap是线程不安全的。

    final HashMap<String, String> map = new HashMap<>(2);
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10000; i++) {
                    new Thread(new Runnable() {
                        @Override
                        public void run() {
                            map.put(UUID.randomUUID().toString(), "");
                        }
                    }, "ftf" + i).start();
                }
            }
        }, "ftf");
        t.start();
        // 正常情况:线程执行结束,会从这返回 (join方法)
        // 但是由于jdk7多线程HashMap#put导致死循环,程序永远不会停止
        t.join();

jdk7中执行上述代码,HashMap进行put操作会引起死循环,导致CPU利用率接近100%。

效率低下的Hashtable

Hashtable容器使用synchronized来保证线程安全,在线程竞争激烈的情况下HashTable的效率非常低下。

image-20230730102325326

image-20230730102349363

当一个线程访问Hashtable的同步方法,其他线程也访问HashTable的同步方法时,会进入阻塞或轮询状态。如线程1使用put进行元素添加,线程2不但不能使用put方法添加元素,也不能使用get方法来获取元素,竞争越激烈效率越低。

JDK1.8 版本 ConcurrentHashMap 做的改进

在 JDK1.7 版本中,ConcurrentHashMap 由数组 + Segment + 锁分段实现,其内部分为一个个段(Segment)数组,Segment 通过继承 ReentrantLock 来进行加锁,通过每次锁住一个 segment 来降低锁的粒度而且保证了每个 segment 内的操作的线程安全性,从而实现全局线程安全。

假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效提高并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术。

image-20230730163337594

这么做的缺陷就是每次通过 hash 确认位置时需要 2 次才能定位到当前 key 应该落在哪个槽:

  • 通过 hash 值和 段数组长度-1 进行位运算确认当前 key 属于哪个段,即确认其在 segments 数组的位置。
  • 再次通过 hash 值和 table 数组长度 - 1进行位运算确认其所在。

image-20230730164119134

为了进一步优化性能,在 jdk1.8 版本中,对 ConcurrentHashMap 做了优化,取消了分段锁的设计,取而代之的是通过 cas 操作和 synchronized 关键字来实现优化,而扩容的时候也利用了一种分而治之的思想来提升扩容效率,在 JDK1.8 中 ConcurrentHashMap 的存储结构和 HashMap 基本一致(数组 + 链表 +红黑树)

image-20230730164356815

JDK1.8 ConcurrentHashMap原理

JDK1.7 ConcurrentHashMap的put()方法

    public V put(K key, V value) {
        Segment<K,V> s;
        if (value == null)
            throw new NullPointerException();
        int hash = hash(key);
        int j = (hash >>> segmentShift) & segmentMask;
        if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
             (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
            s = ensureSegment(j);
        return s.put(key, hash, value, false);
    }

	// 求key的hash值
    private int hash(Object k) {
        int h = hashSeed;

        if ((0 != h) && (k instanceof String)) {
            return sun.misc.Hashing.stringHash32((String) k);
        }

        h ^= k.hashCode();

        // 元素的hashCode再散列,让数字每一位都参与到散列运算中
        h += (h <<  15) ^ 0xffffcd7d;
        h ^= (h >>> 10);
        h += (h <<   3);
        h ^= (h >>>  6);
        h += (h <<   2) + (h << 14);
        return h ^ (h >>> 16);
    }

求key的hash值时,会对元素的hashCode进行一次再散列(能让数字的每一位都参加到散列运算当中)。目的是减少散列冲突,使元素能够均匀地分布在不同的Segment上,从而提高容器的存取效率。

JDK1.8 volatile修饰的节点数组

// volatil修饰,保证多线程可见性,禁止指令重排
transient volatile Node<K,V>[] table;

JDK1.8 ConcurrentHashMap的put()方法

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

    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<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            // tab未被初始化,则先将tab初始化,结束本轮循环,进入下轮循环
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            // 第二轮循环,通过key的hash值来判断table中是否存在相同的key,如果不存在,执行casTabAt()方法。
            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;                   // no lock when adding to empty bin
            }
            // tab的状态,MOVED表示在扩容中,如果在扩容中,帮助其扩容。
            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<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) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);
        return null;
    }

前面所有分支都不满足,进入最后一个分支:

            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;
                            }
                        }
                    }
                }
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }

keyhash值位置不为null,表示发生了hash冲突,此时节点就要通过链表的形式存储这个插入的新值(更新或者插入链表末尾)。

该分支,首先加排它锁,只有在发生hash冲突的时候才加了排它锁

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

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

如果不是,开始新节点操作。

        // 遍历链表 
		for (Node<K,V> e = f;; ++binCount) {
             K ek;
             // 找到一个hash值相同,且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;
             // 如果未找到,往链表最后插入这个新节点。
             if ((e = e.next) == null) {
                 pred.next = new Node<K,V>(hash, key,
                                           value, null);
                 break;
             }
         }

由于更新或者插入操作在排他锁中,这些操作都可以直接操作(无需考虑线程安全问题)。

小结

JDK1.8 put流程:

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

JDK1.8 ConcurrentHashMap的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操作无需加锁,tablevolatile关键字修饰,保证每次获取值都是最新的。直接循环查找链表满足节点即可。

使用场景

多线程环境下,读多写少,性能比较高

乐观锁,认为更新操作不会被其他线程影响。因此在更新少的情况下性能高。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/811990.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基于stm32单片机的直流电机速度控制——LZW

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 目录 一、实验目的二、实验方法三、实验设计1.实验器材2.电路连接3.软件设计&#xff08;1&#xff09;实验变量&#xff08;2&#xff09;功能模块a&#xff09;电机接收信号…

Github git clone 和 git push 特别慢的解决办法

1.在本地上使用 SSH 命令无法git push 上传 github 项目 2.使用 git clone 下载项目特别慢总是加载不了 解决办法1 将 *** 的连接模式换成&#xff1a;D-i-r-e-c-t&#xff08;好像不太有用&#xff09; 后面再找找能不能再G-l-o-b-a-l 下解决该问题 解决办法 2 mac下直接设…

Python 日志记录:6大日志记录库的比较

Python 日志记录&#xff1a;6大日志记录库的比较 文章目录 Python 日志记录&#xff1a;6大日志记录库的比较前言一些日志框架建议1. logging - 内置的标准日志模块默认日志记录器自定义日志记录器生成结构化日志 2. Loguru - 最流行的Python第三方日志框架默认日志记录器自定…

SpringBoot内嵌的Tomcat:

SpringBoot内嵌Tomcat源码&#xff1a; 1、调用启动类SpringbootdemoApplication中的SpringApplication.run()方法。 SpringBootApplication public class SpringbootdemoApplication {public static void main(String[] args) {SpringApplication.run(SpringbootdemoApplicat…

python浅浅替代ps?实现更改照片尺寸,以及更换照片底色

前言 大家早好、午好、晚好吖 ❤ ~欢迎光临本文章 如何用代码来p证件照并且更换底色&#xff1f; 有个小姐姐给我扔了张照片&#xff0c;叫我帮忙给她搞成证件照的尺寸还得换底色 可惜电脑上没有ps只有pycharm&#xff0c;但下载又卸载多麻烦呀 于是&#xff0c;我就用代码来…

RT1052 的周期定时器

文章目录 1 PIT 周期中断定时器2 PIT定时器的使用3 PIT定时器配置3.1 PIT 时钟使能。3.1.1 CLOCK_EnableClock 3.2 初始化 PIT 定时器3.2.1 PIT_Init 3.3 设置 通道 0 的 加载值3.3.1 PIT_SetTimerPeriod 3.4 使能 通道 0 的中断3.4.1 PIT_EnableInterrupts 3.5 开启 PIT 定时器…

在登录界面中设置登录框、多选项和按钮(HTML和CSS)

登录框&#xff08;Input框&#xff09;的样式&#xff1a; /* 设置输入框的宽度和高度 */ input[type"text"], input[type"password"] {width: 200px;height: 30px; }/* 设置输入框的边框样式、颜色和圆角 */ input[type"text"], input[type&q…

测试|测试分类

测试|测试分类 文章目录 测试|测试分类1.按照测试对象分类&#xff08;部分掌握&#xff09;2.是否查看代码&#xff1a;黑盒、白盒灰盒测试3.按开发阶段分&#xff1a;单元、集成、系统及验收测试4.按实施组织分&#xff1a;α、β、第三方测试5.按是否运行代码&#xff1a;静…

100行代码写一个简易QT点名程序

照例演示一下: 分享一个简易的Qt点名程序&#xff0c;满打满算一百行代码&#xff08;还要什么自行车&#xff09;。 UI界面比较丑&#xff0c;按钮是自己做的&#xff0c;背景是AI作画生成的&#xff0c;大家可以自行更换背景以及按钮。 内容也是非常的简单&#xff0c;就是…

JWT登录认证

JWT认证流程 跨域认证解决方案&#xff0c;JWT的流程为&#xff1a; 客户端发送账号和密码请求服务端收到请求&#xff0c;验证用户名密码是否通过验证成功后&#xff0c;服务端会生成唯一的token&#xff0c;将其返回给客户端客户端收到token&#xff0c;会将其存储在cookie…

拓扑排序详解(带有C++模板)

目录 介绍&#xff1a; 实现原理&#xff1a; 简答来说&#xff1a; 例子 模板&#xff08;C&#xff09; 介绍&#xff1a; 拓扑排序&#xff08;Topological Sorting&#xff09;是一种针对有向无环图&#xff08;DAG&#xff09;的节点进行排序的算法。DAG是一个图&…

Android 之 使用 MediaRecord 录音

本节引言 本节是Android多媒体基本API调用的最后一节&#xff0c;带来的是MediaRecord的简单使用&#xff0c; 用法非常简单&#xff0c;我们写个例子来熟悉熟悉~ 1.使用MediaRecord录制音频 运行结果&#xff1a; 实现代码&#xff1a; 布局代码&#xff1a;activity_main.…

自动化测试如何做?真实企业自动化测试流程,自动化测试分类...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 企业自动化测试流…

一文讲清楚地图地理坐标系

前言 我最近在做一个和地图有关的项目&#xff0c;这里本人地图采用的是mapbox&#xff0c;其中涉及一个功能需要根据用户输入的地点直接定位到地图上的对应的位置&#xff0c;本人开始想的是直接调用百度的接口根据地名直接获取坐标&#xff0c;发现在地图上的位置有偏移不够…

C++——模板的作用2:特例化

目录 模板的形式&#xff1a; 一.模板的多参数应用&#xff1a; 例&#xff1a; 错误使用1&#xff1a;使用不标准的模板形参表 ​编辑 错误使用2&#xff1a;使用变量作为实参传递给函数模板 二.模板的特例化&#xff1a; 类模板&#xff1a; 针对模板的特化步骤&am…

乌班图22.04安装wireguard实现异地组网

1. 前言&#xff1a; wireguard是新型的异地组网工具&#xff0c;在此之前&#xff0c;又已经被抛弃的pptp&#xff0c;l2tp&#xff0c;有配置复杂的openvpn&#xff0c;wireguard被linux作者linus赞叹优雅&#xff0c;于linux 5.6合并至linux内核主线。 2. 安装过程&#…

文心一言 VS 讯飞星火 VS chatgpt (68)-- 算法导论6.5 7题

文心一言 VS 讯飞星火 VS chatgpt &#xff08;68&#xff09;-- 算法导论6.5 7题 七、试说明如何使用优先队列来实现一个先进先出队列&#xff0c;以及如何使用优先队列来实现栈(队列和栈的定义见 10.1 节。) 文心一言&#xff1a; 优先队列是一种数据结构&#xff0c;其中…

干货|一次完整的性能测试,测试人员需要做什么?

作者介绍 邓宝菊&#xff08;Kiki Deng&#xff09;&#xff0c;10 年软件测试经验&#xff0c;4 年团队管理经验&#xff0c;当前任职研发部架构质量工程部&#xff0c;整体负责研发部测试团队的效能、工具流程建设和人才培养。 前言 一、 规范性能测试实施流程的意义 规范…

Qt: 查看qmake相关参数设置

Qt开发中&#xff0c;经常会遇到qmake相关问题&#xff0c;比如同时安装了多个Qt版本的情况。比如我的情况是系统自带了Qt 5.12.8, 但是开发中遇到一些兼容性问题&#xff0c;于是又手动安装了5.9.8。 查看qmake版本&#xff0c;qmake -v, 虽然项目中已经指定了5.9.8, 但是系统…

通过String字符生成base64编码在生成图片

* base64转图片 //对字节数组字符串进行Base64解码并生成图片 * param base64str base64码 * return // param savePath 图片路径private static final String savePath"image_ver\\verifyCode"; 判断是否为base64编码 public static void mainDD…