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

news2024/11/26 0:54:40

在实际的开发中,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的核心流程。主要介绍了扩容的机制/

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

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

相关文章

深度学习中小知识点系列(六) 解读SPP / SPPF / SimSPPF / ASPP / RFB / SPPCSPC

SPP与SPPF 一、SPP的应用的背景 在卷积神经网络中我们经常看到固定输入的设计&#xff0c;但是如果我们输入的不能是固定尺寸的该怎么办呢&#xff1f; 通常来说&#xff0c;我们有以下几种方法&#xff1a; &#xff08;1&#xff09;对输入进行resize操作&#xff0c;让他…

跨标签页通信的8种方式(下)

跨标签页通信是指在浏览器中的不同标签页之间进行数据传递和通信的过程。在传统的Web开发中&#xff0c;每个标签页都是相互独立的&#xff0c;无法直接共享数据。然而&#xff0c;有时候我们需要在不同的标签页之间进行数据共享或者实现一些协同操作&#xff0c;这就需要使用跨…

算法刷题-动态规划3(未完待续---------

算法刷题-动态规划3&#xff09; 01背包问题最后一块石头的重量 01背包问题 一篇文章吃透背包问题 大佬讲解什么是背包问题 问题分析&#xff1a; 面对这么多的物品&#xff0c; 选择一个个地来装入背包&#xff0c;背包的承重量不断地增加&#xff0c;二维数组中&#xff0c;…

Linux系统编程:文件系统总结

目录和文件 获取文件属性 获取文件属性有如下的系统调用&#xff0c;下面逐个来分析。 stat:通过文件路径获取属性&#xff0c;面对符号链接文件时获取的是所指向的目标文件的属性 从上图中可以看到stat函数接收一个文件的路径字符串&#xff08;你要获取哪个文件的属性&a…

ubuntu 下载编译 opencv4.2.0并检验

如有帮助点赞收藏关注&#xff01; 如需转载&#xff0c;请注明出处&#xff01; ubuntu 的opencv4.2.0下载与编译 下载依赖开始编译安装配置OpenCV编译环境检验* 完成 下载 首先下载opencv源码网址&#xff1a; https://opencv.org/releases/page/3/ 下载成zip后&#xff0c;…

GPIO的使用--操作PF09 PF10 PF08实现呼吸灯、跑马灯、警报闪烁灯

先来个呼吸灯演示 呼吸灯 目录 一、GPIO的介绍 1.含义 2.控制原理 3.控制流程 二、LED控制 1.呼吸灯 操作代码 烧录结果 2.蜂鸣器红绿灯交替 操作代码 3.红绿灯交替闪烁 操作代码 一、GPIO的介绍 1.含义 GPIO(general porpose intput output),通用输入输出端口。…

Java游戏 王者荣耀

GameFrame类 所需图片&#xff1a; package 王者荣耀;import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.io.File; import java.util.ArrayList…

分布式事务-两阶段提交2PC

2PC协议就是两阶段提交&#xff0c;用来解决分布式事务&#xff0c;分为两个阶段&#xff0c;分别为Prepare和Commit&#xff0c;也是PC由来。 第一阶段Prepare 提交事务请求 如图所示&#xff0c;主要流程有以下三个方面 询问&#xff1a;事务协调者(Manager)向所有的事务参与…

SpringBoot : ch06 整合 web(二)

前言 SpringBoot作为一款优秀的框架&#xff0c;不仅提供了快速开发的能力&#xff0c;同时也提供了丰富的文档和示例&#xff0c;让开发者更加容易上手。在本博客中&#xff0c;我们将介绍如何使用SpringBoot来整合Web应用程序的相关技术&#xff0c;并通过实例代码来演示如何…

C++值常用集合算法

C值常用集合算法 set_intersection #include<iostream> using namespace std; #include<vector> #include<numeric> #include<algorithm>class MyPrint { public:void operator()(int val){cout << val<<" ";} };void test() {v…

电力感知边缘计算技术网关产品设计方案-硬件方案

网关硬件架构设计图: 1.配置方案 配置差异 A类网关 B类网关 CPU

Postman接口测试 —— 设置断言和集合运行

一、常见的5种断言方法 Postman是一款非常强大的API接口调式工具&#xff0c;它自带断言方法&#xff0c;不需要学习JavaScript脚本&#xff0c;非常方便。 &#xff08;1&#xff09;Status code&#xff1a;Code is 200(校验接口返回结果的状态码) &#xff08;2&#xff09…

室内定位(WiFi/UWB/蓝牙等)技术方案概述

室内无法搜索到卫星&#xff0c;这样常规的GPS/北斗定位都无法使用&#xff0c;常规免费的只有运营商的基站定位LBS&#xff0c;但这个精度实在太差&#xff0c;一般都有几十米到几百米的偏差。因此&#xff0c;室内定位一直是个老大难问题。 截至目前&#xff0c;业界比较成熟…

C++:对象模型和this指针

对象模型&#xff1a; 成员变量和成员函数分开存储 在C中&#xff0c;类内的成员变量和成员函数分开存储 只有非静态成员变量才属于类的对象上 空对象占用空间&#xff1a; 1字节 C编译器会给每个空对象也分配一个字节空间&#xff0c;是为了区分空对象占内存的位置 每个…

ESP32网络开发实例-Web页面控制直流电机

Web页面控制直流电机 文章目录 Web页面控制直流电机1、应用介绍2、软件准备3、硬件准备4、代码实现在这个 ESP32 Web务器应用中,我们将创建一个托管在 ESP32 上的网页,我们将使用该网页来控制使用 L298N 电机驱动器模块的直流电机的速度。 网页将包含一个 HTML 滑块,用于为直…

web:[NPUCTF2020]ReadlezPHP

题目 打开页面显示如下 没发现其他的线索&#xff0c;查看源代码 发现一个网址&#xff0c;访问这个页面查看 进行代码审计 这段代码是一个简单的 PHP 类&#xff0c;名为 HelloPhp。它有两个公共属性 $a 和 $b&#xff0c;并在构造函数中将它们分别初始化为字符串 "Y-m-…

第四节HarmonyOS 熟知开发工具DevEco Studio

一、设置主体样式 默认的代码主题样式是黑暗系的&#xff0c;如下图所示&#xff1a; 如果你不喜欢&#xff0c;可以按照一下步骤进行修改&#xff1a; 左上角点击Flie->Settings->Appearance&Behavior->Appearance&#xff0c;点击Theme&#xff0c;在弹出的下拉…

【TinyALSA全解析(三)】tinyplay、tincap、pcm_open源码解析

tinyplay、tincap、pcm_open源码解析 一、本文的目的二、tinyplay.c源码分析三、tinycap.c源码分析四、pcm.c如何调度到Linux Kernel4.1 pcm_open解析4.1.1 pcm_open的主要流程4.1.2 流程说明4.1.3 调用方法 4.2 pcm_write解析 /*********************************************…

土壤类型数据技术服务

一.背景介绍 土壤质地类型不同表明各地土壤表层内具有不同的矿质颗粒大小分布特点&#xff0c;而不同的颗粒大小分布特点又具有不同的辐射传输、水分平衡、热量平衡等特点。因此&#xff0c;土壤质地类型资料可广泛应用于土壤资源评价、土壤肥力估计、大气热力学及动力学方程等…

心大数据结构题型

选择题 2021 数据处理的单位&#xff1a;数据元素 矩阵压缩存储 2022 ①单链表头插法选择 ②矩阵压缩存储&#xff0c;行优先 ③删除链表节点的时间复杂度 ④稀疏矩阵存储 ⑤平衡二叉树时间复杂度 ⑥栈和队列的出队&#xff0c;问栈的大小至少多少 ⑦拓扑排序 ⑧参考书 360…