01Hashmap并发问题-ConcurrentHashMap-线程安全集合类-并发编程(Java)

news2024/11/27 0:33:27

1 概述

线程安全类可以分为3个大类:

  • 遗留的的线程安全集合Hashtable,Vector
  • 使用Collections装饰的线程安全集合,如:
    • Collections.syncronizedCollection
    • Collections.syncronizedList
    • Collections.syncronizedMap
    • Collections.syncronizedSet
  • java.util.concurrent.*;
    • ConcurrentHashMap
    • CopyOnWriteArrayList

遗留的线程安全集合,方法全部加了synchronized,并发性能很低,不推荐。

Collections装饰的线程安全集合,以Collections.syncronizedMap为例,我们看下,它是怎么把线程不安全的集合变成线程安全map。

private static class SynchronizedMap<K,V>
        implements Map<K,V>, Serializable {
        private static final long serialVersionUID = 1978198479659022715L;

        private final Map<K,V> m;     // Backing Map
        final Object      mutex;        // Object on which to synchronize

        SynchronizedMap(Map<K,V> m) {
            this.m = Objects.requireNonNull(m);
            mutex = this;
        }

        SynchronizedMap(Map<K,V> m, Object mutex) {
            this.m = m;
            this.mutex = mutex;
        }

        public int size() {
            synchronized (mutex) {return m.size();}
        }
        public boolean isEmpty() {
            synchronized (mutex) {return m.isEmpty();}
        }
        public boolean containsKey(Object key) {
            synchronized (mutex) {return m.containsKey(key);}
        }
        public boolean containsValue(Object value) {
            synchronized (mutex) {return m.containsValue(value);}
        }
        public V get(Object key) {
            synchronized (mutex) {return m.get(key);}
        }

        public V put(K key, V value) {
            synchronized (mutex) {return m.put(key, value);}
        }
 // ...
 }

包装后Collections.SyncronizedMap相当于在原因Map方法上加syncronized,并发性能并没有提升。

下面以ConcurrentHashMap为例详解下它与原HashMap相比如果提高并发性能。首先我们先看下原HashMap存在的一些并发问题。

2 HashMap并发问题

2.1 JD7并发死链问题

jd7在新增某个桶下标链表元素时,默认会插入链表头,源代码如下所示:

void createEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];
    table[bucketIndex] = new Entry<>(hash, key, value, e);
    size++;
}

当jd7扩容时,可能引发死链问题,测试示例如下:

public class TestDeadLink {
    public static void main(String[] args) {
        HashMap<Integer, Integer> map = new HashMap<>();
        // 1, 35, 16, 50 当map大小为16时,它们在同一桶内
        // 放12个元素,容量的3/4
        map.put(2, null);
        map.put(3, null);
        map.put(4, null);
        map.put(5, null);
        map.put(6, null);
        map.put(7, null);
        map.put(8, null);
        map.put(9, null);
        map.put(10, null);
        map.put(16, null);
        map.put(35, null);
        map.put(1, null);
        System.out.println("扩容前大小[main]:" + map.size());
        new Thread() {
            @Override
            public void run() {
                // 放入第13个元素,发生扩容
                map.put(50, null);
                System.out.println("扩容后大小[thread-0]:" + map.size());
            }
        }.start();
        new Thread() {
            @Override
            public void run() {
                // 放入第13个元素,发生扩容
                map.put(50, null);
                System.out.println("扩容后大小[thread-1]:" + map.size());
            }
        }.start();
    }
}

说明:

HashMap默认容量16,当大小超过容量的3/4时,会扩容,扩容为原来容量的2倍,同时把HashMap中旧数组元素迁移至新数组中。

死链复现步骤:

调试工具使用的idea

在HashMap 590行加断点:即扩容时,迁移数据的方法中

void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length; // 590行代码
    for (Entry<K,V> e : table) {
        while(null != e) {
            Entry<K,V> next = e.next;
            if (rehash) { // 594行
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            int i = indexFor(e.hash, newCapacity);
            e.next = newTable[i];
            newTable[i] = e;
            e = next;
        }
    }
}

断点条件如下,目的是让HashMap扩容为32时,线程为Thread-0或者Thread-1时停下来

newTable.length==32&&(Thread.currentThread().getName().equals("Thread-0")||Thread.currentThread().getName().equals("Thread-1"))

如图所示:在这里插入图片描述

断点暂停方式选择Thread,开始调试程序。

长度为16时,桶下标为1的key
1
16
35
50
长度为32时,桶下标为1的key
1
35
扩容前大小[main]:12

在HashMap源码594行加断点,(条件Thread.currentThread().getName().equals(“Thread-0”)),这是为了观察e结点和next结点的状态。

Thread-0单步执行到594行,可以在Variables面板观察e和next变量,使用view as -> Object 查看结点状态

e	(1)->(35)-(16)->null
next (35)->(16)->null

如图所示:在这里插入图片描述

在Threads面板选择Thread-1恢复运行,控制台输出如下:

扩容后大小[thread-1]:13

此时

newTable[1]	(35)->(1)->null

这是Thread-0还停留在594处,Variables面板变量状态发生改变

e	(1)->null
next (35)->(1)->null

Thread-1虽然结果正确,但Thread-0还要继续执行。接下来单步调试观察死链产生,下一轮循环到594,将e迁移到newTable链表头

newTable[1]	(1)->null
e	(35)->(1)->null
next	(1)->null
// 下一个循环
newTable[1]	(35)->(1)->null
e	(1)->null
next null
// 下一个循环
newTable[1]	(35)<=>(1)
e null

newTable[1]形成环。如图所示:在这里插入图片描述

进而程序卡死,如图:在这里插入图片描述

总结:

  • 究其原因,是因为在多线程环境下视野非线程安全的map集合
  • jdk8虽然将扩容算法做了调整,不再将新元素加入链表头(而是保持插入顺序,即练尾),但在多线程环境下会出现其他问题。

2.2常见问题

改变HashMap存在数据丢失可能,以Jdk8新增为例,源代码如下:

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

在 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); 下标i新增第一个链表元素时,如果多线程并发新增元素都在桶下标i处,那么后执行的会覆盖掉先执行,引起数据丢失;其他类型的新增同样存在数据丢失的可能。

在删除数据的时候,多线程情况下如果要删除同一key导致可能删除对应key的后继结点元素或者其他情况。

总之线程不安全的HashMap在并发情况下,可能出现各种意外情况,特别当Jdk版本为7时,可能引发并发死链问题。

下面我们将详解ConcurrentHashMap,以jd8中为例。

3 后记

如有问题,欢迎交流讨论。

❓QQ:806797785

⭐️源代码仓库地址:https://gitee.com/gaogzhen/concurrent

参考:

[1]黑马程序员.黑马程序员深入学习Java并发编程,JUC并发编程全套教程[CP/OL].2020-01-18/2022-12-12.p274~p280.

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

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

相关文章

Linux系列文章 —— vim的基本操作(误入vim退出请先按「ESC」再按:q不保存退出,相关操作请阅读本文)

系列文章目录 文章目录系列文章目录前言一、vim的基本概念二、vim的基本操作1.退出vim编辑器2.进入vim编辑器3.模式功能及切换三、vim命令模式命令集1.光标移动2.查找字符3.复制粘贴删除4.撤销、重做与重复做5.插入模式6.保存与退出7.环境修改四、总结1.vim的三种基本模式2.vim…

springboot之webmvc和webflux浅析

webmvc和webflux作为spring framework的两个重要模块&#xff0c;代表了两个IO模型&#xff0c;阻塞式和非阻塞式。 1、webmvc webmvc是基于servlet的阻塞式模型&#xff0c;一个请求到达服务器后会单独分配一个线程去处理请求&#xff0c;如果请求包含IO操作&#xff0c;线程…

车载以太网 - DoIP时间参数 - 06

时间参数在所有的协议定义中都无法被忽略的一块重要部分,之前的CAN&CANFD诊断协议总,ISO 16765就有相关的诊断时间参数的定义;在DoIP中,也有同样的时间参数定义,不过他是放在ISO 13400 - 2中,今天我们一起来看下这一块的内容。 ISO 13400原文文档 中文释义 时间参数定…

北大硕士LeetCode算法专题课-基础算法之排序

接连上篇&#xff1a;北大硕士LeetCode算法专题课---算法复杂度介绍_骨灰级收藏家的博客-CSDN博客 冒泡排序 冒泡排序&#xff08;Bubble Sort&#xff09;是一种很原始的排序方法&#xff0c;就是通过不断地交换“大数”的位置达到排序的目的。 因为不断出现“大数”类似于水…

HTML实现狗屁不通文章生成器

演示 实现 css html, body {background: radial-gradient(#181818, #000000);margin: 0;padding: 0;border: 0;-ms-overflow-style: none;}.btn {display: inline-block;color: #fff;cursor: pointer;font-size: 1em;font-weight: 400;max-weight: 20%;position: relative;tex…

【零基础】学python数据结构与算法笔记8

文章目录前言46.数据结构介绍47.列表48.栈的介绍49.栈的应用&#xff1a;括号匹配问题50.队列的介绍51.队列的实现52.队列的内置模块总结前言 学习python数据结构与算法&#xff0c;学习常用的算法&#xff0c; b站学习链接 46.数据结构介绍 数据结构是指相互之间存在着一种…

双点双向重发布以及路由策略

目录前言实验要求基础配置启动rip 及 ospf 协议双向重发布路由策略前言 1&#xff0c;由于ASBR在重发布路由条目时&#xff0c;将清除原有协议携带的度量&#xff0c;会添加新协议的度量种子&#xff0c;一旦2&#xff0c;使用双点双向重发布时&#xff0c;可能会出现选路不佳…

docker容器日志清理

最近发现linux&#xff08;Centos 7&#xff09;虚拟机的空间不够了&#xff0c;想创建新的容器都失败。剩下不到100M。之前还有好几个G。然后每天不定期查看磁盘空间&#xff0c;发现不断被蚕食。今天比昨天就少了100M&#xff1b;然后下午比上午又少了50M。谁在吞噬服务器的硬…

Nacos学习之使用Nacos作为配置中心

使用Nacos作为配置中心 简单使用 1、导入依赖 <dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-alibaba-nacos-config</artifactId></dependency>2、创建bootstrap.properties文件&#xff0c;在其中对nacos…

Vulnhub靶机:MATRIX-BREAKOUT_ 2 MORPHEUS

目录介绍信息收集主机发现主机信息探测网站探测目录爆破反弹shell提权Flag1内核提权 & Flag2介绍 系列&#xff1a;Matrix-Breakout&#xff08;此系列共1台&#xff09; 发布日期&#xff1a;2022 年 7 月 11 日 难度&#xff1a;中级 运行环境&#xff1a;Virtualbox运行…

快速搭建ChatGPT的AI聊天QQ机器人[含问题解决办法]

一、cqhttp 1. 下载cqhttp 首先安装 cqhttp 框架&#xff0c;我们从 release 界面下载最新版本的 go-cqhttp&#xff0c;需要根据不同的系统选择不同的文件 我目前是mac系统&#xff0c;暂时仅举例macOs Intel 版 Macos&#xff1a; go-cqhttp_darwin_amd64.tar.gz 2. 启动 …

jvm运行过程

VM是Java程序运行的环境,同时是一个操作系统的一个应用程序进程,因此它有自己的生命周期,也有自己的代码和数据空间. JVM体系主要是两个JVM的内部体系结构分为三个子系统和两大组件&#xff0c;分别是&#xff1a;类装载器&#xff08;ClassLoader&#xff09;子系统、执行引擎…

5.1、运输层概述

之前所介绍的计算机网络体系结构中的物理层、数据链路层以及网络层它们共同解决了将主机通过异构网络互联起来所面临的问题&#xff0c;实现了主机到主机的通信\color{red}实现了主机到主机的通信实现了主机到主机的通信。 网络层的作用范围主机到主机 但实际上在计算机网络中…

帮助聊天回复的软件

在线客服在工作时&#xff0c;时常能遇到在某一个时间段一大批客户集中咨询的情况&#xff0c;使用客服快速回复软件能够使客服在第一时间进行高效的回复。 前言 在线客服在工作时&#xff0c;时常能遇到在某一个时间段一大批客户集中咨询的情况&#xff0c;通常这种情况下也是…

P2010 [NOIP2016 普及组] 回文日期————C++

题目 [NOIP2016 普及组] 回文日期 题目背景 NOIP2016 普及组 T2 题目描述 在日常生活中&#xff0c;通过年、月、日这三个要素可以表示出一个唯一确定的日期。 牛牛习惯用 888 位数字表示一个日期&#xff0c;其中&#xff0c;前 444 位代表年份&#xff0c;接下来 222 位…

算法测试中的召回率和精确率详解

基本概念 TP&#xff08;True Positive&#xff09;&#xff1a;正确的正例&#xff0c;一个实例是正类并且也被判定成正类FN&#xff08;False Negative&#xff09;&#xff1a;错误的反例&#xff0c;漏报&#xff0c;本为正类但判定为假类FP&#xff08;False Positive&am…

基于 java springboot+mybatis二手物品网站系统设计和实现

基于 java springbootmybatis二手物品网站系统设计和实现 博主介绍&#xff1a;5年java开发经验&#xff0c;专注Java开发、定制、远程、文档编写指导等,csdn特邀作者、专注于Java技术领域 作者主页 超级帅帅吴 Java毕设项目精品实战案例《500套》 欢迎点赞 收藏 ⭐留言 文末获…

linux系统中QT里面多线程的使用方法

大家好&#xff0c;今天主要和大脚聊一聊&#xff0c;如何使用QT中的多线程的方法。 第一&#xff1a;多线程基本简介 QThread 线程类是实现多线程的核心类。Qt 有两种多线程的方法&#xff0c;其中一种是继承 QThread 的 run()函数&#xff0c;另外一种是把一个继承于 QObject…

Excel教程

目录 1.设置Excel界面以及保存Excel 1.1 自定义快速访问工具栏 1.2 自定义功能区的使用 1.2.1 自定义选项卡 1.3 自定义页面 1.4 保存时设置文件密码 2.常用的高效操作 2.1 快速填充 2.1.1 快速填充实现拆分重组 2.2 快速分析数据 2.2.1 格式化​编辑 2.2.2 图表&#xff0c…

yolov5 引入RepVGG模型结构

&#xff08;一&#xff09;前情 这个工作已经有大佬用在自己的工程里了&#xff0c;他的帖子链接&#xff1a;https://blog.csdn.net/weixin_45829462/article/details/120372921 但他的这个lite主要不是研究repvgg的&#xff0c;是做移动端的&#xff0c;但是里面加了这个re…