HashMap 线程安全问题
前言
我们紧接着上节ArrayList 线程安全问题讲下HashMap的线程安全问题
.
之前看书,书中经常会提及.HashTable是线程安全的,HashMap是线程非安全的.在多线程的情况下, HashMap会出现死循环的情况.
此外,还会推荐使用新的JUC类 ConcurrentHashMap
.
今天,我们就将这些幺蛾子一网打尽. 本章, 将主要描述"为什么HashMap
是非线程安全的? HashMap在多线程的情况下为什么会出现死循环?"
正文 - 准备工作
在讲解HashMap类的非线程安全问题
之前, 我们需要对于HashMap
的数据结构要有所了解.
通过Eclipse的outline视图
,我们瞄一瞄HashMap的源码内的对象.
通过观察,我们可以发现.HashMap
主要是维护了一个Enrty类型的数组
来存储变量.
即transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
,其中transient
是用于表示不需要序列化的标示,我们不用管他.
static class Entry<K,V> implements Map.Entry<K,V> {final K key;V value;Entry<K,V> next;int hash;}
我们在看下Entry类
的源码.通过上述的Entry
源码,我们可以发现它主要包括几个部分:key/value/hash/next
.
在JDK1.7
中的HashMap结构大致如下:
其中的每一块值,是一个Entry类型的链表.(在JDK1.8 之后,当Entry链表
的长度多于一定的值的时候,会将其转换为红黑树. PS: 为了在O(log2N)的时间内获取到结点的值. 在JDK1.8内的红黑树也是使用链表进行实现的,不必过于在意.)
单线程resize
操作
首先, 与ArrayList类
一样, 我们先瞄一瞄HashMap的put方法.
public V put(K key, V value) {if (table == EMPTY_TABLE) {// 如果table为空的table,我们需要对其进行初始化操作.inflateTable(threshold);}if (key == null)// 如果key为null的话 我们对其进行特殊操作(其实是放在table[0])return putForNullKey(value);// 计算hash值int hash = hash(key);// 根据hash值获取其在table数组内的位置.int i = indexFor(hash, table.length);// 遍历循环链表(结构图类似上图)for (Entry<K,V> e = table[i]; e != null; e = e.next) {Object k;// 如果找到其值的话,将原来的值进行覆盖(满足Map数据类型的特性.)if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {V oldValue = e.value;e.value = value;e.recordAccess(this);return oldValue;}}// 如果都没有找到相同都key的值, 那么这个值是一个新值.(直接进行插入操作.)modCount++;addEntry(hash, key, value, i);return null;}
跟随其后,我们深入addEntry(hash, key, value, i);
方法一探究竟吧.
void addEntry(int hash, K key, V value, int bucketIndex) {if ((size >= threshold) && (null != table[bucketIndex])) {// 如果其大小超过threshold 并且table[index]的位置不为空// 扩充HashMap的数据结果为原来的两倍resize(2 * table.length);hash = (null != key) ? hash(key) : 0;bucketIndex = indexFor(hash, table.length);}createEntry(hash, key, value, bucketIndex);}
PS: mountCount
/size
/threshold
的联系与区别?
紧随其后,进入人们经常说的resize()
方法.
void resize(int newCapacity) {Entry[] oldTable = table;int oldCapacity = oldTable.length;if (oldCapacity == MAXIMUM_CAPACITY) {//capacity 容量threshold = Integer.MAX_VALUE;return;}// 创建一个新的对象Entry[] newTable = new Entry[newCapacity];// 将旧的对象内的值赋值到新的对象中transfer(newTable, initHashSeedAsNeeded(newCapacity));table = newTable;// 重新赋值新的临界点threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);}void transfer(Entry[] newTable, boolean rehash) {// 新table的长度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;}}}
这么一看,主要的resize()方法
的转换操作都在transfer()方法内
.我们详细的画图了解下这个循环.
for (Entry<K,V> e : table) {}
外层循环,循环遍历旧table
的每一个值.这不难理解,因为要将旧table
内的值拷贝进新table
.- 链表拷贝操作.主要是四行代码.
e.next = newTable[i]; newTable[i] = e; e = next;
这个操作一下子比较难懂,我们画图了解一下.
这个图还是有点抽象 我们再将细节进行细分.
1 next = e.next
,目的是记录下原始的e.next
的地址.图上为Entry-1-1
.
2 e.next = newTable[i]
,也就是将newTable[3]
替代了原来的entry1-1
.目的是为了记录原来的newTable[3]
的链表头.
3 newTable[i] = e
,也就死将entry1-0
替换成新的链表头.
4. e = next;
,循环遍历指针.将e = entry1-1
.开始处理entry1-1
.
将步骤细分后,我们可以得到如下:
由上述的插入过程,我们可以看出.这是一个倒叙的链表插入过程.
(比如 1-0 -> 1-1 ->1-2 插入后将变为 1-2 -> 1-1 -> 1-0)
多线程操作 - 分析线程安全?线程非安全?
因为倒叙链表插入
的情况,导致HashMap
在resize()
的情况下,导致链表出现环的出现.一旦出现了环那么在while(null != p.next){}
的循环的时候.就会出现死循环导致线程阻塞.那么,多线程的时候,为什么会出现环状呢?让我们看下面的例子:
首先有2个线程,由上一节我们知道,在resize()
的过程中,一共有如下四个步骤:
Entry<K,V> next = e.next;
e.next = newTable[i];
newTable[i] = e;
e = next;
我们有两个线程,线程A与线程B.线程A在执行Entry<K,V> next = e.next;
之后,也就是第一个步骤之后,线程阻塞停止了.线程B之间进行了重排序的操作.此时的HashMap内部情况如下所示:
- 初始(A阻塞 B运行完毕)
- A唤醒 e=key(3)
A的执行步骤如下:- 线程阻塞前记录 next = key(7)
- 线程唤醒后执行 newTable[3]=null; key(3).next=null;
- newTable[3]=key(3);
- e=next; 即 e = key(7);
- A唤醒 e=key(7)
- next = key(3)
- newTable[3]=key(3); key(7).next=key(3);
- newTable[3]=key(7);
- e=next; 即 e = key(3);
- A唤醒 e=key(3)
- next = key(3).next; 即 next = null;
- newTable[3]=key(7); key(3).next=key(7);
- newTable[3]=key(3);
- e=next; 即 e = null; 循环结束.但是此时已经形成了环路.
Tips
链表出现环?
快,慢指针方法.判断链表中是否有环 ----- 有关单链表中环的问题
size、capacity、threshold与loadFactor?
- size为hashMap内的链表结点数目.即
Entry对象的个数
(包括链表内).- capacity为桶的数目.(即Enrty<k,v> []table的长度)
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
- loadFactor 过载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
- threshold 边界值.高于边界值,则HashMap进行
resize
操作.The next size value at which to resize (capacity * load factor).
定义int threshold;
Reference
[1]. HashMap 在 JDK 1.8 后新增的红黑树结构
[2]. (1)美团面试题:Hashmap的结构,1.7和1.8有哪些区别,史上最深入的分析
[3]. Java7/8 中的 HashMap 和 ConcurrentHashMap 全解析
[4]. HashMap?面试?我是谁?我在哪
[5]. JDK7与JDK8中HashMap的实现
[6]. 谈谈HashMap线程不安全的体现
[7]. ConcurrentHashMap源码分析(JDK1.7和JDK1.8)
[8]. HashMap中capacity、loadFactor、threshold、size等概念的解释
HashMap 线程安全问题相关推荐
- HashMap线程安全问题详细解析
1.简介 HashMap是一种非线程安全的数据结构,即在多线程环境下,无法保证其操作的原子性和一致性.在多个线程同时访问HashMap并进行修改操作时,可能会导致数据的不一致性和线程竞争条件的出现. ...
- HashMap线程安全问题以及处理方法!
一:HashMap为什么会有线程安全问题? 我们知道jdk1.7和jdk1.8中HashMap都是线程不安全的,那就具体讲一下为什么会线程不安全(两个方面). ①调用put方法 假如有两个线程A和B, ...
- 【Java】HashMap线程安全问题
一.线程不安全的原因 jdk1.7和jdk1.8中HashMap都是线程不安全的,那就具体讲一下为什么会线程不安全(两个方面). (1)调用put方法 假如有两个线程A和B,A希望插入一个key-va ...
- servlet单实例多线程 ---线程安全问题是由实例变量造成的,只要在Servlet里面的任何方法里面都不使用实例变量,那么该Servlet就是线程安全的。(所有建议不要在servlet中定义成员变
Servlet 单例多线程 Servlet如何处理多个请求访问? Servlet容器默认是采用单实例多线程的方式处理多个请求的: 1.当web服务器启动的时候(或客户端发送请求到服务器时),Servl ...
- 获取返回值作为变量_解决多线程间共享变量线程安全问题的大杀器——ThreadLocal...
微信公众号:Zhongger 我是Zhongger,一个在互联网行业摸鱼写代码的打工人! 关注我,了解更多你不知道的[Java后端]打工技巧.职场经验等- 上一期,讲到了关于线程死锁.用户进程.用户线 ...
- 关于java Servlet,Struts,springMVC 的线程安全问题
2019独角兽企业重金招聘Python工程师标准>>> 现在主流的java的前端框架有:struts1,struts2,springmvc 还有最根本的servlet; 前些天一个朋 ...
- java线程安全问题原因及解决办法
1.为什么会出现线程安全问题 计算机系统资源分配的单位为进程,同一个进程中允许多个线程并发执行,并且多个线程会共享进程范围内的资源:例如内存地址.当多个线程并发访问同一个内存地址并且内存地址保存的值是 ...
- 谈谈HashMap线程不安全的体现
转载自 谈谈HashMap线程不安全的体现 HashMap的原理以及如何实现,之前在JDK7与JDK8中HashMap的实现中已经说明了. 那么,为什么说HashMap是线程不安全的呢?它在多线程环境 ...
- java 线程安全问题_java线程安全问题原因及解决办法
1.为什么会出现线程安全问题 计算机系统资源分配的单位为进程,同一个进程中允许多个线程并发执行,并且多个线程会共享进程范围内的资源:例如内存地址.当多个线程并发访问同一个内存地址并且内存地址保存的值是 ...
最新文章
- 360金融发布Q2财报:净利6.92亿,同比增长114%,大数据与AI加持的科技服务是新亮点?
- h5页面提示只能在微信浏览器中打开_电子问卷h5怎么做?
- 最简单的Web Service实现
- flask 接口 让别人能访问_flask搭建一个前后端分离的系统
- 嵌入式OS入门笔记-以RTX为案例:四.简单的时间管理
- 用Vue.js开发微信小程序:开源框架mpvue解析
- c#中使用mysql查询语句_遇到@符合怎么办_C# Mysql 查询 Rownum的解决方法
- centos5安装mysql 5.6.19 mysql-devel_Centos5.8 安装 MySQL5.6.19
- 动态卷积:自适应调整卷积参数,显著提升模型表达能力
- tcpreplay linux,Linux——Tcpreplay
- linux 下使用 tc 模拟网络延迟和丢包-使用 linux 模拟广域网延迟 - Emulating wide area network delays with Linux...
- 使用Quads绘制函数曲线
- 2023最新仿蓝奏云合集下载页面系统源码+有PHP后台版的
- 助力湾区金融科技,巨杉数据库入选首届粤港澳大湾区金融科技飞鱼企业20强榜单
- iOS 卡顿、掉帧原因+优化
- Python常用函数及常用库整理
- 防范技巧 Windows百毒不侵的13个妙招
- DecimalFormat保留小数位
- 助力提升研发效能的“黄金三角”
- 【Linux 的开胃小菜】JumpServer来袭,开源堡垒机安装及使用教程