记一次JDK1.8HashMap resize()方法线程不安全验证

HashMap的扩容机制:JDK1.7,JDK1.8 参考文章: http://www.importnew.com/20386.html https://blog.csdn.net/z69183787/article/details/64920074?locationNum=15&fps=1 以前看HashMap的时候,resize扩容部分没有仔细看,今天挤出时间,看之。 1. 确定hash桶数组的索引位置 我们当然希望这个Hash... 阅读详情

HashMap线程安全性问题算是老生常谈的问题了,HashMap的线程不安全性主要就体现在resize()方法中了,本文就针对HashMap resize()方法做一些线程安全性的测试,注意JDK版本为1.8,1.7不适用

1.先上JDK1.8 HashMap源代码

final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

本文只针对前半部分做测试(30行以前),具体扩容部分下次有时间补上,我们可以看见源码中主要流程是先创建一个新的哈希表,前面代码主要是确定新的哈希表的大小以及负载大小(即扩容大小),有了这两个参数,就可以创建一个哈希表了,紧接着就让原本的table(原哈希表指针)指向新创建的哈希表,然后再搬运数据到新的哈希表中,那么这里很自然就有一个问题了,由于是先让table指向新的哈希表再开始搬运数据,如果在table指向新引用后而数据搬运之前有一个线程来访问哈希表,此时新的哈希表中是没有数据的,原本旧的哈希表的数据是不是会取不到呢?于是就有了下面的测试(原本打算继承HashMap重写resize方法在搬运数据之前让线程睡眠来验证的,结果发现resize方法是final修饰的,坑爹):

public class ResizeTest {
	private static HashMap<Integer, Integer> map = new HashMap<Integer, Integer>();
	public static void main(String[] args) throws FileNotFoundException {
		File file = new File("D:/testResize.txt");
		final PrintStream pw = new PrintStream(new FileOutputStream(file));
		new Thread(){
			public void run(){
				System.setOut(pw);
				while(true){
					if(map.get(1) == null){
						System.out.println("why?");
					}
					System.out.print(map.get(1));
				}
			}
		}.start();
		
		for(int i = 0; i < 2<<29; i++){
			map.put(i, i);
		}
		System.out.println("main end!");
	}
	
	
}

我们把数据全打印到一个文件里便于查找我们想要的数据,测试很简单,就是主线程不断的向hashmap中写数据(循环的次数一定要大以来引发足够多的扩容次数,从而增大引发并发问题的概率),再起一个线程不断的读取一个key的值,如果不存在线程安全的问题,那么只要读key的线程第一次读取到数据后就不会存在读取不到的情况(因为主线程没有去删除key的操作),再说明白点如果hashmap是线程安全的,那我们文件中的数据是不应该出现“why?"字样的(当然也可能出现在文件开头,但是概率很小),真实测试结果呢?

可以看见出现了why?字样(这里有些不明白为什么只出现一次,反复测试都只出现一次,猜测可能是因为写文件操作比较费时,耗时远远大于哈希重排,所以写完一次时哈希重排早已完成),这就证明了我们前面的猜测是正确的,也就是hashmap的确存在线程安全问题,当然在搬运数据时也存在并发问题,下次再针对搬运数据的并发问题作进一步测试

2.思考

上面的测试用例只能针对jdk1.8的,因为jdk1.7是先搬运数据到新的哈希表然后再让原有的table指向新的哈希表,乍一看jdk1.7的设计似乎更合理,其实不然,因为resize是导致hashmap并发问题的罪魁祸首,而我们知道哈希重排只用一个线程就可以完成了,参与哈希重排的线程越多就越容易引发并发问题,而jdk1.7由于是先搬运数据(哈希重排最耗时的操作)再改变引用指针,那就意味着在搬运的过程中就可能会有更多的线程进入到resize方法,而jdk1.8由于在搬运数据之前就把table指向一个空的哈希表,当其他线程在put操作时,直接插入到新的哈希表了,就不用再去参与哈希重排了,而缺点就是可能出现“不可重复读”(就像上面的测试结果一样),总之jdk1.8的思想就是参与哈希重排的线程越少越好

深入理解HashMap(三)resize方法解析 前言 上一篇讲了put方法,在put方法的最后,我们需要检查容量是否超过阈值,,如果越界需要进行扩容。resize方法就是对hashmap进行扩容。那么hashmap的扩容需要哪些步骤呢? 假如我是设计的作者,我会先创建一个更大容量的数组,然后遍历原table,重新计算所有的节点的hash值对应的下标,然后将节点转移到新table中。那么这其中存在这几个问题: 什么时候进行resize操作? 扩... 阅读详情

相关推荐

面试准备——jdk1.7与jdk1.8的HashMap(只重点分析了哈希算法、下标计算、get、put、resize)

写在前面,只是为了面试而准备的笔记,其实之前就有在分析HashMap源码,但是那篇只写了一部分,还躺在草稿箱,不过那篇写的感觉很舒服,等稳定下来想好好写。这篇写的不怎么满意,内容也只有一部分,做个存稿吧。 HashMap 数据结构 jdk1.7 数组 + 链表 jdk1.8 数组 + 链表 + 红黑树 当一个结点的链表长度大于8时,链表会转换成红黑树,提高查询效率,而链表长度小于6时又会退化成链表...

weixin_42907817的博客 4230

HashMap 1.8全流程分析,详解put、resize、遍历等相关函数。以及其内部的二进制原理

从常用属性->构造方法->添加->扩容->查询->删除->遍历的顺序进行`HashMap`源码阅读,涉及到一些二进制的用法以及意义也会进行说明。

Pumpkin's Blog 1140

jdk8之HashMap resize方法详解(深入讲解为什么1.8中扩容后的元素新位置为原位置+原数组长度)

缘起 在此之前,笔者对HashMap的工作原理进行过简单的解析, jdk8中的HashMap相对于jdk7有比较大的更新,本文主要是对改动后的resize()方法进行详细解析。看本文需要一定的HashMap基础,如果没有基础的同学建议先看笔者的另一篇文章 resize()方法的作用 resize()方法会在HashMap的键值对达到“阈值”后进行数组扩容,而扩容时会调用resize()方法,此外,...

诺浅的专栏 1万+

HashMap为什么线程不安全

学习java集合时,老师都会说下hashMap是线程不安全的;现在就详细说下为什么不安全? 1.put操作造成线程不安全 这个问题比较好想象,比如有两个线程A和B,首先A希望插入一个key-value对到HashMap中,首先计算记录所要落到的桶的索引坐标,然后获取到该桶里面的链表头结点,此时线程A的时间片用完了,而此时线程B被调度得以执行,和线程A一样执行,只不过线程B成功将记录插到了桶里面,假设线程A插入的记录计算出来的桶索引和线程B要插入的记录计算出来的桶索引是一样的,那么当线程B成功插入之后,线

guangyingposuo的博客 326

基于PDF文档结构的数字隐写

PDF隐写

xiongshivigor的博客 1426

静态时序分析(Static Timing Analysis, STA):简介及内容导航

静态时序分析(简称STA)是用来验证数字设计时序的技术之一,另外一种验证时序的方法是时序仿真,时序仿真可以同时验证功能和时序。“时序分析”这个术语就是用来指代“静态时序分析“或”时序仿真“这两种方法之一,简单来说,时序分析的目的就是为了解决设计中的各种时序问题。STA被称为静态的原因是其对于设计的分析是静态地执行的,并不依赖于施加在输入端口上的激励。相比之下,时序仿真则可以被视作动态地执行对设计的分析,具体过程描述如下:施加一组激励,观察在这组激励下电路行为是否符合要求,然后换一组激。

Starry__的博客 1995

JDK1.8和JDK1.7的HashMap源码分析以及线程不安全问题

分析HashMap在JDK1.7和JDK1.8的源码以及其暴露的线程不安全问题

蒋大钊的博客 999

国产蓝牙MCU选型避坑指南:从汇顶到杰理,这7家厂商的实测体验

本文基于一线开发者对汇顶、泰凌微、沁恒、博通集成、奉加微、杰理、翱捷等七家主流国产蓝牙MCU厂商的实测体验,提供了一份详实的选型避坑指南。文章不仅对比了各厂商芯片的SDK质量、文档、开发工具与技术支持等“软实力”,还深入剖析了实际开发中可能遇到的功耗、射频、版本兼容性等核心痛点,并针对不同项目需求给出了具体的选型决策建议,旨在帮助工程师高效完成国产蓝牙MCU选型。

u5v6w7的博客 338

HashMap线程不安全-两种情况

HashMap会进行resize扩容操作,在resize扩容操作的时候会造成线程不安全。下面将举两个可能出现线程不安全的地方。 1、put的时候导致的多线程数据不一致。 这个问题比较好想象,比如有两个线程A和B,首先A希望插入一个key-value对到HashMap中,首先计算记录所要落到的桶的索引坐标,然后获取到该桶里面的链表头结点,此时线程A的时间片用完了,而此时线程B被调度得以执行,和线程A一样执行,只不过线程B成功将记录插到了桶里面,假设线程A插入的记录计算出来的桶索引和线程B要插入的记录计算出来的

qq_44691484的博客 495

Zynq UltraScale+ MPSoC-dp14 standalone

EDID HDCP AR# 64652 How to port the DisplayPort 1.4 TX subsystem example to a custom board dp v2.1 example design color format change DisplayPort-DP接口知识

zhuxl24的博客 2671

万达电影 com.wandafilm.app headers check

万达电影 安卓逆向 抓包 逆向 check

weixin_43786514的博客 1438

谈谈HashMap线程不安全的体现

为什么80%的码农都做不了架构师?>>> ...

weixin_33946605的博客 476

MDK开发篇:基于立创逻辑派FPGA-G1与Cortex-M1软核的按键控制LED实战

本文详细介绍了基于立创逻辑派FPGA-G1开发板与高云Cortex-M1软核的完整开发流程。通过Gowin云源软件配置软核系统与GPIO外设,并在Keil MDK环境中编写按键控制LED的C程序,最终实现程序固化与上电自启动。教程涵盖硬件配置、软件开发、在线调试与固化部署全步骤,帮助STM32开发者快速上手FPGA软核开发。

weixin_42103128的博客 149

JDK1.8 HashMap中resize()方法解释

JDK1.8 HashMap中resize()方法解析 * 初始化或者是将table大小加倍。如果为空,则按threshold分配空间,否则,加倍后,每个容器中的元素在新table中要么呆在原索引处,要么有一个2的次幂的位移 * @return the table */ final Node[] resize() { Node[] oldTab

journey_fan的专栏 3347

基于GEC6818开发板+Linux+Qt设计的智能养老院出入管理系统(195)

当前,传统的养老院出入管理模式普遍存在信息更新滞后、数据统计繁琐、实时监控能力不足等问题,难以满足现代养老机构对安全管理与服务质量提升的需求。本系统设计正是针对这些问题,通过集成先进的硬件设备如高性能GEC6818开发板以及RFID刷卡模块,结合稳定高效的嵌入式Linux操作系统和QT开发框架,构建一套功能齐全、操作直观、响应迅速的智能出入管理系统。 该系统能自动记录并实时显示老人的进出时间,方便管理人员及时了解每位老人的活动轨迹,同时具备强大的后台管理功能,支持管理员便捷地进行老人信息的增删改查操作。通

1070

AI在生物学领域的应用:基因序列分析和人工智能生物学

好的,非常感谢您提供的这个精彩的主题和详细的要求。我会全力以赴,遵循您的要求写一篇内容丰富、结构清晰、论述深入的专业技术文章。现在,就让我们开始这篇关于"AI在生物学领域的应用:基因序列分析和人工智能生物学"的博客文章吧! 1. 背景介绍 1.1 生物信息学的兴起

AI天才研究院 1417

【Java集合】HashMap的resize()源码详解以及JDK1.7与JDK1.8的区别

HashMap的扩容源码详解,以及JDK1.7和JDK1.8的resize()方法的源码比较。

小七的博客 2755

C# WinForm 基于DirectShow.dll编写的程序,可边预览边保存视频,音频和图片

基于微软的DirectShow.dll的二次开发1.自动识别连接的摄像头跟麦克风且可以对其进行选择(默认选中第一个)2.可录制wmv和avi两种格式的视频并且都带有音频,avi可以录制的分辨率和帧数进行选择,wmv默认进行了淸晰度优化如不喜欢可去除代码中有相关提示3.可在预览点开之后进行拍照,也可边录制视频边拍照4.添加了视频和音频的压缩功能(酌情使用)这次二次开发发现C#对DirectShow的相关信息太少,这次做完也不敢藏私共享出来,还望大家指正

JDK1.8---HashMap的resize()扩容方法源码详解

JDK1.8---HashMap的resize()扩容方法源码详解resize()方法触发时机下面开始根据源码介绍HashMap的扩容原理下面进行分步分析扩容 resize()方法触发时机 首先总结一下JDK1.8的HashMap都在什么时候触发resize()方法,根据阅读源码总结了三个时机触发扩容,这里只做介绍,后面根据源码详细分析 HashMap是由数组+链表+红黑树构成的,数组就称之为桶了...

qq_36856024的博客 4282

假人有限元模型

假人模型,车辆碰撞假人有限元模型,适合做乘员安全的研究者利用

Statistics and Machine Learning Toolbox

Statistics and Machine Learning Toolbox(带书签),MATLAB统计学习工具

JDK1.8中HashMap的resize()方法详解

resize()方法的代码比较长,我们先用文字来总结一下该方法具体做了什么。首先记录当前数组信息,当前数组、数组长度还有扩容阈值。接着就到了一个if-elseif-else的代码块,这些就是用来判断当前是进行初始化操作还是扩容操作,如果是扩容操作则需要进行双倍扩容,如果是初始化数组则需要设置数组容量。如果是扩容操作或者初始化的时候用户指定了初始容量,则要⽤新数组的大小重写计算扩容阈值重新生成一个数组(无论是扩容操作还是初始化操作都需要)。

frozenpenguin的博客 1885

锂电池充电器电路设计 里面有电路

锂电池充电器电路设计 里面有电路 3.7V升压到5V电路设计。

HashMap死循环及JDK1.8的resize()如何维护链表顺序

HashMap死循环 我第一次听到HashMap会有死循环的问题是两年前,我们团队有一个学长实习回来给我们说有个同事因为没有注意到HashMap在多线程可能会造成死循环而造成了一些损失。后面就对这个问题很感兴趣,然后趁着今天整理资料就顺便记录一下,虽然偷懒了,逃 = -= 什么造成的 我们知道HashMap不是线程安全的,那么具体到代码,是哪部分代码的原因呢? 我本来想写一篇博客介绍如

乐天派 2709

面试必问的HashMap源码put()方法和resize()方法——基于JDK1.8

目录一、前言二、 HashMap 的构造方法三、 put()方法(一)、源码注释(二)、流程图四、 resize()方法(一)、源码注释(二)、流程图(三)、 resize方法的注意事项 一、前言 HashMap太太太常用了,不做过多的介绍。进入正文直接冲源码。 本文主要是基于1.8的HashMap讲述部分源码,主要重点是put()方法和resize()方法 二、 HashMap 的构造方法 /** * 设置初始化容量,调用的是下面的重载方法 */ public HashMap(int initialCap

爱读史的小马的博客 2571

源码-JDK1.8HashMap扩容方法resize()解析

1、准备知识 HashMap的底层数据结构 Java语言的基本数据结构可以分为两种,一种是数组,另一种的模拟指针/引用,Java语言中涉及到的数据结构都是这两种的扩充。JDK1.8之前HashMap是数组+链表结合的链表散列。JDK1.8在解决哈希冲突上发生了变化,当链表长度大于阈值/默认8的时候,会将链表转化为红黑树,减少搜索时间。 hash算法 我们希望HashMap的元素位置尽量分散,最好是每个位置只有一个元素,这样用hash算法求得该位置后可以直接返回结果,不用再遍历链表/红黑树。 H

CodeDabaicai的博客 483

jdk1.8 hashmap多线程put不会造成死循环

jdk1.8 hashmap多线程put不会造成死循环

qq_27007251的博客 1万+

HashMap为什么不是线程安全?

前言 该试题从互联网获得,真实性没有考究,加上本人学识浅薄,所以面试题参考为主,解析分享为主。 若对解析有不同看法,还请评论指正。谢谢。 HashMap为什么不是线程安全? 以JDK1.8的HashMap为例,引用作者: 一字马胡 所写文章中的一张图: 上图为 ...

大雄君的博客 2万+

hashMap resize原理 jdk1.8

jdk1.7与1.8之间的hashMap区别之一就是在于resize()方法解决了1.7多线程状态下扩容可能出现的链表死循环的问题,并取消reshash的操作,提升了运行时间。 使用idea的快捷键 CTRL+F12 打开hashMap的代码,搜索resize得到如下代码: final Node<K,V>[] resize() { Node<K,V>[] o...

hhhhhhhhh的博客 585

JDK1.8中HashMap集合源码解析

文章目录简介内部结构源码分析1. 构造方法2. put方法3. get方法4. resize方法 简介 HashMap 主要用来存放键值对,它基于哈希表的 Map 接口实现,是常用的 Java 集合之一。 JDK1.8 之前 HashMap 由 数组+链表 组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。 JDK1.8 之后 HashMap 的组成多了红黑树,在满足下面两个条件之后,会执行链表转红黑树操作,以此来加快搜索速度。 HashMap是线程不安全

浪人与酒 2353
上一篇: java return与finally执行顺序测试
下一篇: 排序算法之归并排序
coder_no
博客等级 码龄9年 1粉丝 8原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值