线程安全原理简析及HashMap多线程并发5种场景异常分析(3)

简介: 线程安全原理简析及HashMap多线程并发5种场景异常分析(3)

hashmap插入


(1)table==null? 初始化线程A执行check操作后,发生线程切换,B也check table==null操作,A、B都会resize()更新table,产生更新丢失!


if ((tab = table) == null || (n = tab.length) == 0)//(1)线程切换
    n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)//(2)线程切换
    tab[i] = newNode(hash, key, value, null);


(2)tab[i]==null?  A 线程和 B 线程计算出相同的哈希值对应了相同的数组位置,此时该位置还没数据,然后对同一个数组位置,两个线程会同时 写入新的头结点,那B的写入操作就会覆盖 A 的写入,造成 A 的写入操作丢失。


hashmap扩容


HashMap 插入后超过阈值会触发扩容resize操作,new一个新容量cap的数组,对原数组的键值对重新进行计算hash并写入新数组,然后指向新数组。


if (++size > threshold)// 线程切换
    resize();


当A、B线程同时进来,检测到总数量超过阈值的时候就会同时触发 resize 操作,各自生成新的数组并 rehash 后赋给该 map 底层的数组,结果最终只有最后一个线程生成的新数组被赋给该 map 底层,其他线程的均会丢失。


hashmap删除


删除这一块可能会出现两种线程安全问题


image.png


1、线程A判断得到了指定的数组位置i并进入了循环,此时,线程B已经删掉位置i数据了,然后线程A那边就没了。但是删除的话,没了倒问题不大,只是A返回的就是

null


2、当A、B线程同时操作同一个数组位置的时候,也都会先取得现在状态下该位置存储的头结点,然后各自去进行计算操作,之后再把结果写会到该数组位置去,其实写回的时候可能其他的线程已经就把这个位置给修改过了,就会覆盖其他线程的修改。


jdk7下HashMap的扩容和链表死循环发生的场景


在addEntry的方法中有以下代码。resize(2*table.length);可以看出是将数组扩容成原来数组的两倍。先从判断语句开始看。执行扩容的条件是当HashMap创建的节点数大于阈值的时候并且该索引位置不为空才会进行扩容。也就是说16的默认阈值是12的情况下,前十二个索引都被使用了,第十三次在索引为空的地方创建新的节点,那就暂时不需要扩容,先把这个索引位置的节点名额用了再说。


if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }


扩容完成后就将要put的key通过hash算法和indexFor求出索引,注意这时候indexFor中的table.lengh参数应该是老数组长度的两倍,扩容过后的新数组。下面主要来看resize扩容方法。


在resize中发现它根据newCapacity创建了一个新的数组,而这个newCapacity就是2*table.length,在创建完成新的数组后,将老数组中的内容转移到新数组内。通过transfer方法。在transfer方法中遍历了table数组,当e(这里的e是老数组中的e)不为空的时候进行转移操作,这里rehash默认是false,没有什么特殊情况,方法体不会被执行


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


单线程下的扩容过程[头插法]

首先是要获得e节点的next指针,然后重新通过hash算法和indexFor方法计算得到新数组的索引,得到索引后开始转移。大致的画一下,在老数组中可以看到e,还有计算新的索引之前把老数组e的next指针所指向的值给了Entry<K,V> next


image.png


image.png


image.png


多线程扩容过程


假设有两个线程,从是否需要扩容判断那里开始,两个都同时都需要扩容,进入resize方法,在resize方法中两个线程都创建了各自新的数组,大小相同。然后再到transfer方法中准备转移,遍历老数组,对他们来说老数组是公共的,一样的。遍历后进入while循环,当执行到Entry<K,V> next = e.next;的时候开是发生不同,线程一有了它自己e和next,线程二也会有他自己独立的e和next


image.png


image.png


image.png


image.png


参考文章


https://blog.csdn.net/sarafina527/article/details/105040594/
https://blog.csdn.net/weixin_42769637/article/details/103304102
相关文章
|
4月前
|
安全 算法 Java
Java 多线程:线程安全与同步控制的深度解析
本文介绍了 Java 多线程开发的关键技术,涵盖线程的创建与启动、线程安全问题及其解决方案,包括 synchronized 关键字、原子类和线程间通信机制。通过示例代码讲解了多线程编程中的常见问题与优化方法,帮助开发者提升程序性能与稳定性。
185 0
|
30天前
|
存储 缓存 监控
什么是线程池?它的工作原理?
我是小假 期待与你的下一次相遇 ~
127 1
|
1月前
|
设计模式 消息中间件 安全
【JUC】(3)常见的设计模式概念分析与多把锁使用场景!!理解线程状态转换条件!带你深入JUC!!文章全程笔记干货!!
JUC专栏第三篇,带你继续深入JUC! 本篇文章涵盖内容:保护性暂停、生产者与消费者、Park&unPark、线程转换条件、多把锁情况分析、可重入锁、顺序控制 笔记共享!!文章全程干货!
131 1
|
2月前
|
数据采集 存储 弹性计算
高并发Java爬虫的瓶颈分析与动态线程优化方案
高并发Java爬虫的瓶颈分析与动态线程优化方案
|
3月前
|
数据采集 消息中间件 并行计算
Python多线程与多进程性能对比:从原理到实战的深度解析
在Python编程中,多线程与多进程是提升并发性能的关键手段。本文通过实验数据、代码示例和通俗比喻,深入解析两者在不同任务类型下的性能表现,帮助开发者科学选择并发策略,优化程序效率。
223 1
|
4月前
|
数据采集 监控 调度
干货分享“用 多线程 爬取数据”:单线程 + 协程的效率反超 3 倍,这才是 Python 异步的正确打开方式
在 Python 爬虫中,多线程因 GIL 和切换开销效率低下,而协程通过用户态调度实现高并发,大幅提升爬取效率。本文详解协程原理、实战对比多线程性能,并提供最佳实践,助你掌握异步爬虫核心技术。
|
5月前
|
数据采集 网络协议 前端开发
Python多线程爬虫模板:从原理到实战的完整指南
多线程爬虫通过并发请求大幅提升数据采集效率,适用于大规模网页抓取。本文详解其原理与实现,涵盖任务队列、线程池、会话保持、异常处理、反爬对抗等核心技术,并提供可扩展的Python模板代码,助力高效稳定的数据采集实践。
240 0
|
5月前
|
存储 安全 Java
Java 集合面试题从数据结构到 HashMap 源码剖析详解及长尾考点梳理
本文深入解析Java集合框架,涵盖基础概念、常见集合类型及HashMap的底层数据结构与源码实现。从Collection、Map到Iterator接口,逐一剖析其特性与应用场景。重点解读HashMap在JDK1.7与1.8中的数据结构演变,包括数组+链表+红黑树优化,以及put方法和扩容机制的实现细节。结合订单管理与用户权限管理等实际案例,展示集合框架的应用价值,助你全面掌握相关知识,轻松应对面试与开发需求。
267 3
|
Java
让星星⭐月亮告诉你,HashMap中保证红黑树根节点一定是对应链表头节点moveRootToFront()方法源码解读
当红黑树的根节点不是其对应链表的头节点时,通过调整指针的方式将其移动至链表头部。具体步骤包括:从链表中移除根节点,更新根节点及其前后节点的指针,确保根节点成为新的头节点,并保持链表结构的完整性。此过程在Java的`HashMap$TreeNode.moveRootToFront()`方法中实现,确保了高效的数据访问与管理。
106 2
|
Java 索引
让星星⭐月亮告诉你,HashMap之往红黑树添加元素-putTreeVal方法源码解读
本文详细解析了Java `HashMap` 中 `putTreeVal` 方法的源码,该方法用于在红黑树中添加元素。当数组索引位置已存在红黑树类型的元素时,会调用此方法。具体步骤包括:从根节点开始遍历红黑树,找到合适位置插入新元素,调整节点指针,保持红黑树平衡,并确保根节点是链表头节点。通过源码解析,帮助读者深入理解 `HashMap` 的内部实现机制。
177 2

热门文章

最新文章