【Java并发】聊聊不安全的HashMap以及ConcurrentHashMap

在实际的开发中,hashmap是比较常用的数据结构,如果所开发的系统并发量不高,那么没有问题,但是一旦系统的并发量增加一倍,那么就可能出现不可控的系统问题,所以在平时的开发中,我们除了需要考虑正常的情况,还需要考虑异常情况,高并发的场景,这样写的代码才具备稳定性。否则就是随时就是定时炸弹。只是目前没有触发而已。

hashmap为什么不安全

造成线程不安全的原因在于竞态读写共享资源,对于hashmap来说其实就是table数组以及数组中链表。
get:读操作 put:写操作,以及扩容和树化等。

1.读操作与读操作、写操作、扩容、树化之间是否线程安全
读和读之间显然不会有线程安全问题,读是从table中获取对应下标遍历链表,而写直接写在最后,也不存在。

读和扩容之间存在线程安全问题,扩容的基本流程是会创建一个新的table数组,然后将当前table引用指向新的数组,然后在将旧的table数组遍历迁移到新的数组中。所以在这个过程中,可能导致读操作获取不到原来旧数组中的某些值,从而导致出现数据丢失。

在这里插入图片描述
读操作与树化不存在线程安全问题,原因在于 链表的节点和树化的节点是不同的,需要创建新的树节点,而之前是不需要修改链表节点。在树化完全执行完毕之后,才会更新对应的引用。是一个写时复制操作。

2.写操作与写操作,扩容、树化之间是否线程安全
写与写操作是存在线程安全的,因为同时对链表进行尾部插入,如果同时有两个线程操作,那么就会出现丢失数据的情况。
同样写与扩容来说,一边写和扩容,并行操作。写与树化操作,因为是对链表操作,而在树化结束之后,没来得记更新,所以就会出现写操作无效。
3.扩容与扩容、树化操作之间是否安全
扩容与扩容 在同时操作不同的数据肯定会丢失数据。
4.树化与树化之间是否线程安全

在这里插入图片描述

ConcurrentHashMap

在这里插入图片描述

如上所示,hashmap是线程不安全的,所以在实际的开发中,我们会更多的使用concurrentHashMap。hashtable和Synchroinzed的原理其实是通过对全局的操作进行加一把锁,整体的并发粒度比较粗。

    public synchronized int size() {
        return count;
    }

而ConcurrentHashMap采用了分段锁的思想,按照table的粒度进行划分,如果是8个那么默认就是8个锁,这样对于数据的操作可以提升并发性能。

get实现原理

public V get(Object key) {
    Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
    // key 所在的 hash 位置
    int h = spread(key.hashCode());
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (e = tabAt(tab, (n - 1) & h)) != null) {
        // 如果指定位置元素存在,头结点hash值相同
        if ((eh = e.hash) == h) {
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                // key hash 值相等,key值相同,直接返回元素 value
                return e.val;
        }
        else if (eh < 0)
            // 头结点hash值小于0,说明正在扩容或者是红黑树,find查找
            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.根据hash值计算位置,
2.找到指定位置,如果是头节点直接返回。
3.如果头节点hash值小于0,说明正在扩容或者是红黑树,查找
4.如果是链表,直接遍历链表。

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) {
    // key 和 value 不能为空
    if (key == null || value == null) throw new NullPointerException();
    int hash = spread(key.hashCode());
    int binCount = 0;
    for (Node<K,V>[] tab = table;;) {
        // f = 目标位置元素
        Node<K,V> f; int n, i, fh;// fh 后面存放目标位置的元素 hash 值
        if (tab == null || (n = tab.length) == 0)
            // 数组桶为空,初始化数组桶(自旋+CAS)
            tab = initTable();
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            // 桶内为空,CAS 放入,不加锁,成功了就直接 break 跳出
            if (casTabAt(tab, i, null,new Node<K,V>(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 加锁加入节点
            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;
}

写操作的过程 其实是分两种情况,如果table[i] 为空,则使用cas方式将数据写入对应的节点,如果table[i]不为空 ,通过syn的方式枷锁实现。

树化操作,写入和扩容的同时会丢失数据,所以需要使用syn枷锁

扩容
ConcurrentHashMap使用的是分段锁,也就是一个table[i] 一个锁,那么在实际扩容的时候是怎么样的。
实际上也是通过扩容操作也是分段加锁执行的。整体就是写时复制、复制替代搬移
在这里插入图片描述
1.操作的时候,会对每个数组进行枷锁处理,复制、然后解锁,并且是多个线程同时处理。比如A线程复制1-3,B线程复制4-6。所以整体的流程就是已经完成复制的(已复制未加锁)、在复制中加锁、未复制未加锁。
2.因此在复制的过程中,对于已经复制的链表应该使用新的table数组,而在复制和没有复制的应该使用旧的table数组。
ForwardingNode 节点就是标记是否已复制未加锁,所以在已经复制的节点,会使用 ForwardingNode的nextTable指向新的数组。

static final class ForwardingNode<K, V> extends Node<K, V> {
        final Node<K, V>[] nextTable;

        ForwardingNode(Node<K, V>[] tab) {
            super(MOVED, null, null, null);
            this.nextTable = tab;
        }
    }

3.在实际的扩容中,多个线程可以同时进行对数组进行扩容,通过tranferIndex,初始值为tab.length,通过CAS进行竞争获取。
在这里插入图片描述
4.最终谁来执行将table引用指向新数组,通过sizeCtl来判断,谁执行到最后 等于0 的时候,就负责处理。
在这里插入图片描述

小结

本篇主要介绍了 hashmap不安全的原因,在扩容、树化、put操作之间。以及介绍了ConcurrentHashMap 在8中的版本get、put的核心流程。主要介绍了扩容的机制/

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

IDEA懒人必备插件:自动生成单元测试!

IDEA懒人必备插件&#xff1a;自动生成单元测试&#xff01; 前言1、打开设置 File-->settings-->Plugins&#xff0c; 搜索 Squaretest2、安装完成后重启idea &#xff0c;你会发现&#xff0c;导航栏位置已经多了一个选项3、接着就在你想要测试的类中 用快捷键 altInse…

小程序如何进行版本回退

当商家决定回退小程序版本时&#xff0c;可能是因为新版本出现了一些问题或者不符合预期&#xff0c;需要恢复到之前的稳定版本。下面具体介绍怎么回退小程序的版本。 在小程序管理员后台->版本设置处&#xff0c;点击版本回退。确认后&#xff0c;小程序会回退到上一次的版…

瑞数五代ast反混淆笔记二

第一部分 瑞数五代ast反混淆笔记一 第二部分 瑞数五代ast反混淆笔记二 文章目录 前言一、分析思路二、轨迹合并思路三、避免重复调用一个轨迹四、自己调用自己所在的函数五、语句中包含if的处理六、语句中包含try的处理七、节点中包含影响自身值的操作总结 前言 当if转为switc…

PS修容美白插件Portraiture2024

Portraiture 4是一款强大的PS和Lightroom插件&#xff0c;能快速发现照片中的人脸和皮肤&#xff0c;支持全身皮肤部分识别&#xff0c;并升级支持自动识别照片中的面部特征。它结合AI人工深度学习&#xff0c;处理大尺寸原片可提高效率至少1倍以上。Portraiture能实现智能磨皮…

OCP Java17 SE Developers 复习题07

答案 答案 B, D. Iguana does not compile, as it declares a static field with the same name as an instance field. Records are implicitly final and cannot be marked abstract, which is why Gecko compiles and Chameleon does not, making option B correct. Noti…

利用ambari搭建Hbase高可用

初始环境&#xff1a; 节点名称服务名ambari-hadoop1ambari-hadoop2region serverambari-hadoop3hmater、 region server 计划为ambari-hadoop1添加hmaster&#xff0c;以避免hmaster的单点故障、 step1&#xff1a;添加备用Hmaster step2&#xff1a;选择ambari-hadoop1作为…

PostgreSQL 数据脱敏方式盘点

数据脱敏是一种广泛采用的保护敏感数据&#xff08;如信用卡&#xff0c;社保卡&#xff0c;地址等信息&#xff09;的方法。脱敏数据不仅仅是为了保护你和客户的数据安全&#xff0c;在一些情况下&#xff0c;法律也有相应要求&#xff0c;最著名的例子就是 GDPR。 市面上也有…

VR全景技术助力政务服务大厅数字化,打造全新政务服务体验

引言&#xff1a; 随着科技的飞速发展&#xff0c;虚拟现实&#xff08;VR&#xff09;技术逐渐走进人们的视野。VR全景技术作为VR领域的一项重要应用&#xff0c;以其沉浸式、交互式的特点&#xff0c;正逐渐渗透到各行各业。政务服务大厅作为相关部门与民众之间的桥梁&#…

Day43力扣打卡

打卡记录 子数组的最小值之和&#xff08;乘法原理 单调栈&#xff09; 大佬的题解 class Solution:def sumSubarrayMins(self, arr: List[int]) -> int:n len(arr)# 左边界 left[i] 为左侧严格小于 arr[i] 的最近元素位置&#xff08;不存在时为 -1&#xff09;left, s…

堆详解(C语言实现)

文章目录 写在前面1. 堆的概念和性质1.1 堆的概念1.2 堆的性质 2 堆的实现2.1 堆结构的定义2.2 堆的初始化2.3 堆的插入2.3.1 向上调整算法2.3.2 堆的插入元素过程 2.4 堆的删除2.4.1 向下调整算法2.4.2 堆的删除元素过程 2.5 获取堆顶元素2.6 获取堆元素个数2.7 判断堆是否为空…

C语言——打印出所有的“水仙花数”

所谓水仙花数,是指一个3位数,其各位数字立方和等于该数本身。水仙花数是指一个三位数&#xff0c;它的每个位上的数字的立方和等于它本身。例如&#xff0c;153是一个水仙花数&#xff0c;因为1^3 5^3 3^3 153。 #define _CRT_SECURE_NO_WARNINGS 1#include <stdio.h>…

【刷题笔记】分糖果||数组||暴力通过||符合思维方式||多案例分析

分发糖果 文章目录 分发糖果1 题目描述2 题目分析2.1 寻找波峰波谷2.2 从波底往波峰攀爬&#xff01;2.2 计算糖果 3 代码附录1 1 题目描述 https://leetcode.cn/problems/candy/ n 个孩子站成一排。给你一个整数数组 ratings 表示每个孩子的评分。 你需要按照以下要求&…

kafka的详细安装部署

简介&#xff1a; Kafka是一个分布式流处理平台&#xff0c;主要用于处理高吞吐量的实时数据流。Kafka最初由LinkedIn公司开发&#xff0c;现在由Apache Software Foundation维护和开发。 Kafka的核心是一个分布式发布-订阅消息系统&#xff0c;它可以处理大量的消息流&#…

开始使用Spring Boot Admin吧-使用Nacos注册SBA

什么是 Spring Boot Admin&#xff08;SBA&#xff09;? Spring Boot Admin 是 codecentric 公司开发的一款开源社区项目&#xff0c;目标是让用户更方便的管理以及监控 Spring Boot 应用。 应用可以通过我们的Spring Boot Admin客户端&#xff08;通过HTTP的方式&#xff0…

浙江启用无人机巡山护林模式,火灾扑救效率高

为了保护天然的森林资源&#xff0c;浙江当地林业部门引入了一种创新技术&#xff1a;林业无人机。这些天空中的守护者正在重新定义森林防火和护林工作的方式。 当下正值天气干燥的季节&#xff0c;这些无人机开始了它们的首次大规模任务。它们在指定的林区内自主巡逻&#xff…

C++二分查找或并集查找:交换得到字典序最小的数组

作者推荐 利用广度优先或模拟解决米诺骨牌 本文涉及的基础知识点 二分查找算法合集 题目 给你一个下标从 0 开始的 正整数 数组 nums 和一个 正整数 limit 。 在一次操作中&#xff0c;你可以选择任意两个下标 i 和 j&#xff0c;如果 满足 |nums[i] - nums[j]| < limi…

Ubuntu20.04部署TVM流程及编译优化模型示例

前言&#xff1a;记录自己安装TVM的流程&#xff0c;以及一个简单的利用TVM编译模型并执行的示例。 1&#xff0c;官网下载TVM源码 git clone --recursive https://github.com/apache/tvmgit submodule init git submodule update顺便完成准备工作&#xff0c;比如升级cmake版本…

Vue3中调用外部iframe链接方法

业务场景&#xff0c;点击某个按钮需要跳转到外部iframe的地址&#xff0c;但是需要在本项目内显示。以前项目中写过调用外部链接的功能&#xff0c;是有菜单的&#xff0c;但是这次是按钮&#xff0c;所以不能直接把地址配到菜单里。 实现方法&#xff1a;在本地路由文件里写个…

解决git与huggingface项目下载速度慢或者失败的问题

git clone 项目报错 比如使用git clone 下载项目&#xff1a; git clone https://github.com/ChuRuaNh0/FastSam_Awsome_TensorRT.git有时候会报以下错误&#xff1a; fatal: unable to access ‘https://github.com/xxx.git/’: Failed to connect to github.com port 443 …