线程安全原理简析及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
相关文章
|
27天前
|
安全
List并发线程安全问题
【10月更文挑战第21天】`List` 并发线程安全问题是多线程编程中一个非常重要的问题,需要我们认真对待和处理。只有通过不断地学习和实践,我们才能更好地掌握多线程编程的技巧和方法,提高程序的性能和稳定性。
131 59
|
6天前
|
安全 Java 开发者
Java 多线程并发控制:深入理解与实战应用
《Java多线程并发控制:深入理解与实战应用》一书详细解析了Java多线程编程的核心概念、并发控制技术及其实战技巧,适合Java开发者深入学习和实践参考。
|
18天前
|
存储 设计模式 分布式计算
Java中的多线程编程:并发与并行的深度解析####
在当今软件开发领域,多线程编程已成为提升应用性能、响应速度及资源利用率的关键手段之一。本文将深入探讨Java平台上的多线程机制,从基础概念到高级应用,全面解析并发与并行编程的核心理念、实现方式及其在实际项目中的应用策略。不同于常规摘要的简洁概述,本文旨在通过详尽的技术剖析,为读者构建一个系统化的多线程知识框架,辅以生动实例,让抽象概念具体化,复杂问题简单化。 ####
|
27天前
|
缓存 监控 Java
Java 线程池在高并发场景下有哪些优势和潜在问题?
Java 线程池在高并发场景下有哪些优势和潜在问题?
|
1月前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
48 1
C++ 多线程之初识多线程
|
29天前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
19 3
|
29天前
|
Java 开发者
在Java多线程编程中,选择合适的线程创建方法至关重要
【10月更文挑战第20天】在Java多线程编程中,选择合适的线程创建方法至关重要。本文通过案例分析,探讨了继承Thread类和实现Runnable接口两种方法的优缺点及适用场景,帮助开发者做出明智的选择。
17 2
|
29天前
|
Java
Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口
【10月更文挑战第20天】《JAVA多线程深度解析:线程的创建之路》介绍了Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口。文章详细讲解了每种方式的实现方法、优缺点及适用场景,帮助读者更好地理解和掌握多线程编程技术,为复杂任务的高效处理奠定基础。
30 2
|
29天前
|
Java 开发者
Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点
【10月更文挑战第20天】Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点,重点解析为何实现Runnable接口更具灵活性、资源共享及易于管理的优势。
34 1
|
29天前
|
安全 Java 开发者
Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用
本文深入解析了Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用。通过示例代码展示了如何正确使用这些方法,并分享了最佳实践,帮助开发者避免常见陷阱,提高多线程程序的稳定性和效率。
37 1