ConcurrentHashMap是如何实现线程安全的

news2025/1/14 18:27:05

目录

原理:

初始化数据结构时的线程安全

 put 操作时的线程安全


原理:

        多段锁+cas+synchronize

初始化数据结构时的线程安全

在 JDK 1.8 中,初始化 ConcurrentHashMap 的时候这个 Node[] 数组是还未初始化的,会等到第一次 put() 方法调用时才初始化

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;
		// 判断Node数组为空
		if (tab == null || (n = tab.length) == 0)
			// 初始化Node数组
            tab = initTable();
        ......
}

此时会有并发问题的,如果多个线程同时调用 initTable() 初始化 Node[] 数组怎么办?看看 Doug Lea 大师是如何处理的

private final Node<K,V>[] initTable() {
	Node<K,V>[] tab; int sc;
	// 每次循环都获取最新的Node[]数组引用
    while ((tab = table) == null || tab.length == 0) {
    	// sizeCtl是一个标记位,若为-1,代表有线程在进行初始化工作了
		if ((sc = sizeCtl) < 0)
			// 让出CPU时间片
			Thread.yield(); 
		// 此时,代表没有线程在进行初始化工作,CAS操作,将本实例的sizeCtl变量设置为-1	
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
        	// 如果CAS操作成功了,代表本线程将负责初始化工作
        	try {
        		// 再检查一遍数组是否为空
            	if ((tab = table) == null || tab.length == 0) {
            		// 在初始化ConcurrentHashMap时,sizeCtl代表数组大小,默认16
          			// 所以此时n默认为16
                	int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                    @SuppressWarnings("unchecked")
                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                    // 将其赋值给table变量
                    table = tab = nt;
                    // 通过位运算,n减去n二进制右移2位,相当于乘以0.75
          			// 例如16经过运算为12,与乘0.75一样,只不过位运算更快
                    sc = n - (n >>> 2);
                }
            } finally {
            	// 将计算后的sc(12)直接赋值给sizeCtl,表示达到12长度就扩容
        		// 由于这里只会有一个线程在执行,直接赋值即可,没有线程安全问题,只需要保证可见性
            	sizeCtl = sc;
			}
            break;
		}
	}
	return tab;
}

总结:就算有多个线程同时进行 put 操作,在初始化 Node[] 数组时,使用了 CAS 操作来决定到底是哪个线程有资格进行初始化,其他线程只能等待。用到的并发技巧如下

  • volatile 修饰 sizeCtl 变量:它是一个标记位,用来告诉其他线程这个坑位有没有线程在进行初始化工作,其线程间的可见性由 volatile 保证
  • CAS 操作:CAS 操作保证了设置 sizeCtl 标记位的原子性,保证了在多线程同时进行初始化 Node[] 数组时,只有一个线程能成功

 put 操作时的线程安全

public V put(K key, V value) {
	return putVal(key, value, false);
}
    
final V putVal(K key, V value, boolean onlyIfAbsent) {
	// K,V 都不能为空
	if (key == null || value == null) throw new NullPointerException();
	// 取得 key 的 hash 值
	int hash = spread(key.hashCode());
	// 用来计算在这个节点总共有多少个元素,用来控制扩容或者转换为树
	int binCount = 0;
	// 数组的遍历,自旋插入结点,直到成功
	for (Node<K,V>[] tab = table;;) { 
		Node<K,V> f; int n, i, fh;
		// 当Node[]数组为空时,进行初始化
		if (tab == null || (n = tab.length) == 0)    			
			tab = initTable();
		// Unsafe类volatile的方式取出hashCode散列后通过与运算得出的Node[]数组下标值对应的Node对象
    	// 此时 Node 位置若为 null,则表示还没有线程在此 Node 位置进行插入操作,说明本次操作是第一次
		else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
			// 如果这个位置没有元素的话,则通过 CAS 的方式插入数据
			if (casTabAt(tab, i, null, 
					// 创建一个 Node 添加到数组中,null 表示的是下一个节点为空
					new Node<K,V>(hash, key, value, null)))
				// 插入成功,退出循环	
                break;         
		}
		// 如果检测到某个节点的 hash 值是 MOVED,则表示正在进行数组扩容     
		else if ((fh = f.hash) == MOVED)    
			// 帮助扩容
			tab = helpTransfer(tab, f);
		// 此时,说明已经有线程对Node[]进行了插入操作,后面的插入很有可能会发生Hash冲突
        else {
			V oldVal = null;
			// ----------------synchronized----------------
            synchronized (f) {
            	// 二次确认此Node对象还是原来的那一个
                if (tabAt(tab, i) == f) {
                	// ----------------table[i]是链表结点----------------
                    if (fh >= 0) {
                    	// 记录结点数,超过阈值后,需要转为红黑树,提高查找效率
                    	binCount = 1;            
                        // 遍历这个链表
                        for (Node<K,V> e = f;; ++binCount) {
                        	K ek;
                            // 要存的元素的 hash 值和 key 跟要存储的位置的节点的相同的时候,替换掉该节点的 value 即可
                            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;
                            // 如果不是同样的 hash,同样的 key 的时候,则判断该节点的下一个节点是否为空
                            if ((e = e.next) == null) { 
                            	// ----------------“尾插法”插入新结点----------------
                               	pred.next = new Node<K,V>(hash, key,
                                                              value, null);
                                break;
                            }
						}
					}
					// ----------------table[i]是红黑树结点----------------
                    else if (f instanceof TreeBin) { 
                    	Node<K,V> p;
                        binCount = 2;
                        // 调用putTreeVal方法,将该元素添加到树中去
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                           value)) != null) {
                        	oldVal = p.val;
                            if (!onlyIfAbsent)
                            	p.val = value;
                        }
					}
				}
			}
			if (binCount != 0) {
				// 当在同一个节点的数目达到8个的时候,则扩张数组或将给节点的数据转为tree
				if (binCount >= TREEIFY_THRESHOLD)
					// 链表 -> 红黑树 转换
                	treeifyBin(tab, i);    
                // 表明本次put操作只是替换了旧值,不用更改计数值	
                if (oldVal != null)
                	return oldVal;
                break;
			}
		}
	}
	addCount(1L, binCount);// 计数值加1
	return null;
}

总结:

put() 方法的核心思想:由于其减小了锁的粒度,若 Hash 完美不冲突的情况下,可同时支持 n 个线程同时 put 操作,n 为 Node 数组大小,在默认大小 16 下,可以支持最大同时 16 个线程无竞争同时操作且线程安全

当 Hash 冲突严重时,Node 链表越来越长,将导致严重的锁竞争,此时会进行扩容,将 Node 进行再散列,下面会介绍扩容的线程安全性。总结一下用到的并发技巧

  • 减小锁粒度:将 Node 链表的头节点作为锁,若在默认大小 16 情况下,将有 16 把锁,大大减小了锁竞争(上下文切换),就像开头所说,将串行的部分最大化缩小,在理想情况下线程的 put 操作都为并行操作。同时直接锁住头节点,保证了线程安全
  • 使用了 volatile 修饰 table 变量,并使用 Unsafe 的 getObjectVolatile() 方法拿到最新的 Node
  • CAS 操作:如果上述拿到的最新的 Node 为 null,则说明还没有任何线程在此 Node 位置进行插入操作,说明本次操作是第一次
  • synchronized 同步锁:如果此时拿到的最新的 Node 不为 null,则说明已经有线程在此 Node 位置进行了插入操作,此时就产生了 hash 冲突;此时的 synchronized 同步锁就起到了关键作用,防止在多线程的情况下发生数据覆盖(线程不安全),接着在 synchronized 同步锁的管理下按照相应的规则执行操作

参考:

【精选】ConcurrentHashMap是如何实现线程安全的_concurrenthashmap如何保证线程安全-CSDN博客

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

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

相关文章

常见面试题-MySQL专栏(三)MVCC、BufferPool

typora-copy-images-to: imgs 了解 MVCC 吗&#xff1f; 答&#xff1a; MVCC&#xff08;Multi-Version Concurrency Control&#xff09; 是用来保证 MySQL 的事务隔离性的&#xff0c;对一行数据的读和写两个操作默认是不会通过加锁互斥来保证隔离性&#xff0c;避免了频…

【Mybatis小白从0到90%精讲】07:Mybatis 传递参数方式详解

文章目录 前言一、序号传参二、@Param注解传参三、对象传参单个参数多个参数四、万能Map传参单个参数多个参数总结前言 Mybatis传递参数的方式,或者说 获取参数的方式,非常灵活,支持多种方式,所以为了彻底搞懂,今天我们来总结一下Mybatis传参方式! 一、序号传参 Mapper接…

老电脑升级内存、固态硬盘、重新装机过程记录

基础环境&#xff1a; 电脑型号&#xff1a;联想XiaoXin700-15ISK系统版本&#xff1a;Windows10 家庭中文版 版本22H2内存&#xff1a;硬盘&#xff1a; 升级想法&#xff1a; 内存升级&#xff0c;固态硬盘升级&#xff0c;系统重装&#xff08;干净一点&#xff09; 升级内存…

c++类和对象(八) static成员 友元

1.1 概念 声明为static的类成员称为类的静态成员&#xff0c;用static修饰的成员变量&#xff0c;称之为静态成员变量&#xff1b;用static修饰的成员函数&#xff0c;称之为静态成员函数。静态成员变量一定要在类外进行初始化。 面试题&#xff1a;实现一个类&#xff0c;计算…

Leetcode—187.重复的DNA序列【中等】

2023每日刷题&#xff08;二十&#xff09; Leetcode—187.重复的DNA序列 实现代码 class Solution { public:const int L 10;vector<string> findRepeatedDnaSequences(string s) {unordered_map<string, int> str;vector<string> ans;int len s.size()…

第 370 场 LeetCode 周赛题解

A 找到冠军 I 枚举求强于其他所有队的队 class Solution { public:int findChampion(vector<vector<int>> &grid) {int n grid.size();int res 0;for (int i 0; i < n; i) {int t 0;for (int j 0; j < n; j)if (j ! i)t grid[i][j];if (t n - 1) …

Linux环境基础开发工具使用(二)

&#x1f4d8;北尘_&#xff1a;个人主页 &#x1f30e;个人专栏:《Linux操作系统》《经典算法试题 》《C》 《数据结构与算法》 ☀️走在路上&#xff0c;不忘来时的初心 文章目录 一、Linux项目自动化构建工具-make/Makefile1、背景2、实例代码3、依赖关系4、依赖方法5、原理…

精品基于Python的图书借阅归还管控系统

《[含文档PPT源码等]精品基于Python的图书管控系统》该项目含有源码、文档、PPT、配套开发软件、软件安装教程、项目发布教程、包运行成功&#xff01; 软件开发环境及开发工具&#xff1a; 开发语言&#xff1a;python 使用框架&#xff1a;Django 前端技术&#xff1a;Ja…

从NetSuite Payment Link杂谈财务自动化、数字化转型

最近在进行信息化的理论学习&#xff0c;让我有机会跳开软件功能&#xff0c;用更加宏大的视野&#xff0c;来审视我们在哪里&#xff0c;我们要到哪去。 在过去20多年&#xff0c;我们的财务软件经历了电算化、网络化、目前处于自动化、智能化阶段。从NetSuite这几年的功能发…

理解 fopen的 rwa r+w+a+ 参数含义

tags: C categories: C 理解 一图胜千言 我愿称之为最强 c - Difference between r and w in fopen() - Stack Overflow; 需要注意里面的a和 a, 区别在于 a 不可以读而 a可以读. c - Difference between r and w in fopen() - Stack Overflow; ModeReadWriteCreate New Fil…

【Java基础】内部类

一、什么是内部类 在一个类的里面&#xff0c;再定义一个类。&#xff08;在一个类的内部定义的类&#xff0c;称为内部类&#xff09; 举例:在A类的内部定义B类&#xff0c;B类就被称为内部类 二、内部类的访问特点 1.内部类可以直接访问外部类的成员&#xff0c;包括…

数据处理中的中心化

数据处理中的中心化&#xff0c;就是将原数据减去平均值&#xff0c;得到新的数据&#xff0c;新的数据的平均值为0。 假设原数据是x&#xff08;x可以是多维的&#xff09;&#xff0c;其平均值是&#xff0c;新的数据&#xff0c;那么新数据的平均值是为0的。下面证明下&…

[LeetCode] 2.两数相加

一、题目描述 给你两个 非空 的链表&#xff0c;表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的&#xff0c;并且每个节点只能存储 一位 数字。 请你将两个数相加&#xff0c;并以相同形式返回一个表示和的链表。 你可以假设除了数字 0 之外&#xff0c;这两个…

网络编程 - HTTP协议

目录 HTTP协议格式 一&#xff0c;请求格式 1.1 URL的基本格式 1.2 方法(method) 1.3 请求头header 二&#xff0c;响应格式 2.1 状态码 HTTP协议格式 HTTP协议与之前讲的TCP/IP协议不同&#xff0c;HTTP协议要分为两个部分——请求和响应&#xff0c;也就是一种"一…

过了面试,后面的在线测评还会刷人吗?

过了面试&#xff0c;后面的在线测评还会刷人吗&#xff1f;完全有可能刷&#xff0c;如果不是为了刷&#xff0c;何必要给你做线上测评&#xff0c;我说的有道理不&#xff1f; 好吧&#xff0c;说到为什么在线测评要刷人&#xff0c;怎么刷&#xff1f; 怎么才能确保不被刷&…

教你快速阅读java简单字节码

我们举例代码 public class Read {public static void main(String[] args) {int a20;int b30;a1;b15;System.out.println((ab)/2);}}我们将他进行运行&#xff0c;我们就会在target 里面出现.class文件&#xff0c;我们需要进入 执行这个代码 javap -c Read.class就会出现…

【ICN综述】信息中心网络隐私安全

ICN基本原理&#xff1a; 信息中心网络也是需要实现在不可信环境下可靠的信息交换和身份认证 信息中心网络采用以数据内容为中心的传输方式代替现有IP 网络中以主机为中心的通信方式&#xff0c;淡化信息数据物理或逻辑位置的重要性&#xff0c;以内容标识为代表实现数据的查找…

基于java+springboot+vue城市轨道交通线路查询系统-公交车线路查询

项目介绍 本系统是针对目前交通管理的实际需求&#xff0c;从实际工作出发&#xff0c;对过去的市轨道交通线路查询系统存在的问题进行分析&#xff0c;完善用户的使用体会。采用计算机系统来管理信息&#xff0c;取代人工管理模式&#xff0c;查询便利&#xff0c;信息准确率…

webgoat-(A1)injection

SQL Injection (intro) SQL 命令主要分为三类&#xff1a; 数据操作语言 &#xff08;DML&#xff09;DML 语句可用于请求记录 &#xff08;SELECT&#xff09;、添加记录 &#xff08;INSERT&#xff09;、删除记录 &#xff08;DELETE&#xff09; 和修改现有记录 &#xff…

随机微分方程的分数扩散模型 (score-based diffusion model) 代码示例

随机微分方程的分数扩散模型&#xff08;Score-Based Generative Modeling through Stochastic Differential Equations&#xff09; 基于分数的扩散模型&#xff0c;是估计数据分布梯度的方法&#xff0c;可以在不需要对抗训练的基础上&#xff0c;生成与GAN一样高质量的图片。…