`
freish
  • 浏览: 83954 次
  • 性别: Icon_minigender_1
  • 来自: 摄影帝国
社区版块
存档分类
最新评论

HashMap中的元素玩起了躲猫猫

    博客分类:
  • java
阅读更多

当你明明put进了一对非null key-value进了HashMap,某个时候你再用这个key去取的时候却发现value为null,再次取的时候却又没问题,都知道是HashMap的非线程安全特性引起的,分析具体原因如下:

 

public V get(Object key) {
		if (key == null)
			return getForNullKey();
		int hash = hash(key.hashCode());

		// indexFor方法取得key在table数组中的索引,table数组中的元素是一个链表结构,遍历链表,取得对应key的value
		for (Entry<K, V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {
			Object k;
			if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
				return e.value;
		}
		return null;
	}

 

 

 再看看put方法:

 

public V put(K key, V value) {
		if (key == null)
			return putForNullKey(value);
		int hash = hash(key.hashCode());
		int i = indexFor(hash, table.length);
		for (Entry<K, V> e = table[i]; e != null; e = e.next) {
			Object k;
			if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
				V oldValue = e.value;
				e.value = value;
				e.recordAccess(this);
				return oldValue;
			}
		}

		modCount++;
		// 若之前没有put进该key,则调用该方法
		addEntry(hash, key, value, i);
		return null;
	}
 

 

 

再看看addEntry里面的实现:

 

void addEntry(int hash, K key, V value, int bucketIndex) {
		Entry<K, V> e = table[bucketIndex];
		table[bucketIndex] = new Entry<K, V>(hash, key, value, e);
		if (size++ >= threshold)
			resize(2 * table.length);
	}

 里面有一个if块,当map中元素的个数(确切的说是元素的个数-1)大于或等于容量与加载因子的积时,里面的resize是就会被执行到的,继续resize方法:

 

 

void resize(int newCapacity) {
		Entry[] oldTable = table;
		int oldCapacity = oldTable.length;
		if (oldCapacity == MAXIMUM_CAPACITY) {
			threshold = Integer.MAX_VALUE;
			return;
		}

		Entry[] newTable = new Entry[newCapacity];
		transfer(newTable);
		table = newTable;
		threshold = (int) (newCapacity * loadFactor);
	}

 

 

resize里面重新new一个Entry数组,其容量就是旧容量的2倍,这时候,需要重新根据hash方法将旧数组分布到新的数组中,也就是其中的transfer方法:

 

void transfer(Entry[] newTable) {
		Entry[] src = table;
		int newCapacity = newTable.length;
		for (int j = 0; j < src.length; j++) {
			Entry<K, V> e = src[j];
			if (e != null) {
				src[j] = null;
				do {
					Entry<K, V> next = e.next;
					int i = indexFor(e.hash, newCapacity);
					e.next = newTable[i];
					newTable[i] = e;
					e = next;
				} while (e != null);
			}
		}
	}

在这个方法里,将旧数组赋值给src,遍历src,当src的元素非null时,就将src中的该元素置null,即将旧数组中的元素置null了,也就是这一句:

 

if (e != null) {
		src[j] = null;

 此时若有get方法访问这个key,它取得的还是旧数组,当然就取不到其对应的value了。

 

 

下面,我们重现一下场景:

 

import java.util.HashMap;
import java.util.Map;
public class TestHashMap {
	public static void main(String[] args) {
		final Map<String, String> map = new HashMap<String, String>(4, 0.5f);
		
		new Thread(){
			public void run() {
				while(true) { 
					System.out.println(map.get("name1"));
					try {
						Thread.sleep(1000);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			}
		}.start();
		for(int i=0; i<3; i++) {
			map.put("name" + i, "value" + i);
		}
	}
}

Debug上面这段程序,在map.put处设置断点,然后跟进put方法中,当i=2的时候就会发生resize操作,在transfer将元素置null处停留片刻,此时线程打印的值就变成null了。

 

 

总结:HashMap在并发程序中会产生许多微妙的问题,难以从表层找到原因。所以使用HashMap出现了违反直觉的现象,那么可能就是并发导致的了

 

 

 

分享到:
评论
20 楼 tianzizhi 2011-06-18  
ConcurrentHashMap
Collections.synchronizedMap(map).

这俩不是一个等级的,
第一个是局部加锁,
第二个是整体加锁,
效率差很多
19 楼 angel243fly 2011-06-18  
jv520jv 写道
kingkan 写道
HashMap是非线程安全的。

试下用ConcurrentHashMap吧。


楼上说的对,在多线种情况下对一个线程不安全的容器进行操作显然是不对的.还是用ConcurrentHashMap这个比较好 或者Collections.synchronizedMap(map).

Collections.synchronizedMap(map)这个更好用些
18 楼 jv520jv 2011-06-17  
kingkan 写道
HashMap是非线程安全的。

试下用ConcurrentHashMap吧。


楼上说的对,在多线种情况下对一个线程不安全的容器进行操作显然是不对的.还是用ConcurrentHashMap这个比较好 或者Collections.synchronizedMap(map).
17 楼 sebatinsky 2011-06-17  
一直么有研究过,哈哈,看完QQ再看。
16 楼 freish 2011-06-17  
renwolang521 写道
freish 写道
handby123 写道
看到这我突然想弱弱地问一句:很看到几次HASHMAP通过KEY查找值得时间复杂度为O(1)
然我疑惑的是 get()方法中不是也先要遍历table数组么 难道这不算时间复杂度?

是不用遍历table数组的,数组的下标是通过indexFor迅速定位的,但是table中的元素是一个链表,如果hash的加载因子太大,就有可能出现很多元素hash得到的table索引是一样的,这就需要遍历这个链表了


通常你给一个key,通过其hashCode 值 然后 indexFor 就可以计算出其数组下标,直接定位到该元素
static int indexFor(int h, int length) {
   return h & (length-1);
}

但是 不同对象的hashCode 有可能一样,所以HashMap 中 每个key 对应的是一个链表,当两个不同key 的hashCode 相同时,那么就放入到对应的同一个链表里,当你取的时候,根据key的hashCode定位到这个链表(链表中存的是 Entry<K,V> 对象),遍历然后逐个equals key 直到找到元素(不同对象equals绝对是false)。

假如一个链表 你直接遍历 那么当链表非常大的时候,会非常慢的,但一般情况下不同对象的hashCode值是不同的,根据hashCode 和 indexFor() 直接就能找到该元素的索引,然后直接就取出来了,万一hashCode 相同,仅需要遍历一个相对小的链表即可。

所以
  1.当你 需要存取大量元素的时候,运用 hashMap 这类集合 自然比较高效
  2.当你定义一个class的时候,假如需要重写 hashCode 和 equals 方法的时候要注意这两个方法



在定义好hashCode和equals方法后,加载因子就是一个重要因素,加载因子越大,重复的可能性就越大,但table数组的利用率越高;加载因子越小,重复的可能性越小,但table数组很多空间被浪费掉了。需要在时间和空间上有一个折中
15 楼 renwolang521 2011-06-17  
freish 写道
handby123 写道
看到这我突然想弱弱地问一句:很看到几次HASHMAP通过KEY查找值得时间复杂度为O(1)
然我疑惑的是 get()方法中不是也先要遍历table数组么 难道这不算时间复杂度?

是不用遍历table数组的,数组的下标是通过indexFor迅速定位的,但是table中的元素是一个链表,如果hash的加载因子太大,就有可能出现很多元素hash得到的table索引是一样的,这就需要遍历这个链表了


通常你给一个key,通过其hashCode 值 然后 indexFor 就可以计算出其数组下标,直接定位到该元素
static int indexFor(int h, int length) {
   return h & (length-1);
}

但是 不同对象的hashCode 有可能一样,所以HashMap 中 每个key 对应的是一个链表,当两个不同key 的hashCode 相同时,那么就放入到对应的同一个链表里,当你取的时候,根据key的hashCode定位到这个链表(链表中存的是 Entry<K,V> 对象),遍历然后逐个equals key 直到找到元素(不同对象equals绝对是false)。

假如一个链表 你直接遍历 那么当链表非常大的时候,会非常慢的,但一般情况下不同对象的hashCode值是不同的,根据hashCode 和 indexFor() 直接就能找到该元素的索引,然后直接就取出来了,万一hashCode 相同,仅需要遍历一个相对小的链表即可。

所以
  1.当你 需要存取大量元素的时候,运用 hashMap 这类集合 自然比较高效
  2.当你定义一个class的时候,假如需要重写 hashCode 和 equals 方法的时候要注意这两个方法
14 楼 freish 2011-06-17  
tianzizhi 写道
楼主只是解释说明一个现象的背后产后的原因,至于为什么不安全用什么安全这个大家都是知道的,呵呵,支持


终于有个明白人
13 楼 tianzizhi 2011-06-16  
楼主只是解释说明一个现象的背后产后的原因,至于为什么不安全用什么安全这个大家都是知道的,呵呵,支持
12 楼 kingkan 2011-06-16  
HashMap是非线程安全的。

试下用ConcurrentHashMap吧。
11 楼 marshaldong 2011-06-16  
freish 写道
handby123 写道
看到这我突然想弱弱地问一句:很看到几次HASHMAP通过KEY查找值得时间复杂度为O(1)
然我疑惑的是 get()方法中不是也先要遍历table数组么 难道这不算时间复杂度?

是不用遍历table数组的,数组的下标是通过indexFor迅速定位的,但是table中的元素是一个链表,如果hash的加载因子太大,就有可能出现很多元素hash得到的table索引是一样的,这就需要遍历这个链表了

对,这时遍历是因为有了”键冲突“。
10 楼 dingzhaoxu 2011-06-16  
yunchow 写道
K,HashMap本来就不是线程安全的,多此一举

9 楼 yunchow 2011-06-16  
K,HashMap本来就不是线程安全的,多此一举
8 楼 freish 2011-06-16  
handby123 写道
看到这我突然想弱弱地问一句:很看到几次HASHMAP通过KEY查找值得时间复杂度为O(1)
然我疑惑的是 get()方法中不是也先要遍历table数组么 难道这不算时间复杂度?

是不用遍历table数组的,数组的下标是通过indexFor迅速定位的,但是table中的元素是一个链表,如果hash的加载因子太大,就有可能出现很多元素hash得到的table索引是一样的,这就需要遍历这个链表了
7 楼 handby123 2011-06-16  
看到这我突然想弱弱地问一句:很看到几次HASHMAP通过KEY查找值得时间复杂度为O(1)
然我疑惑的是 get()方法中不是也先要遍历table数组么 难道这不算时间复杂度?
6 楼 suhuanzheng7784877 2011-06-16  
ticmy 写道
我怎么不能评价

四哥~~~顶一个
5 楼 ticmy 2011-06-16  
我怎么不能评价
4 楼 freish 2011-06-16  
xieboxin 写道
学习了,不过楼主所贴的代码不能正确证明。我改了下,如下:


public static void main(String[] args) {
		final Map<String, String> map = new HashMap<String, String>(4, 0.5f);

		Thread thread = new Thread() {
			@Override
			public void run() {
				while (true) {
					System.out.println(map.get("name1"));
					try {
						Thread.sleep(1000);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			}
		};
		thread.setDaemon(true);
		thread.start();
		for (int i = 0; i < 3; i++) {
			map.put("name" + i, "value" + i);
			System.out.println("put");
		}
		try {
			Thread.sleep(1000000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}



我实际debug的,没问题啊
3 楼 xieboxin 2011-06-15  
学习了,不过楼主所贴的代码不能正确证明。我改了下,如下:


public static void main(String[] args) {
		final Map<String, String> map = new HashMap<String, String>(4, 0.5f);

		Thread thread = new Thread() {
			@Override
			public void run() {
				while (true) {
					System.out.println(map.get("name1"));
					try {
						Thread.sleep(1000);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			}
		};
		thread.setDaemon(true);
		thread.start();
		for (int i = 0; i < 3; i++) {
			map.put("name" + i, "value" + i);
			System.out.println("put");
		}
		try {
			Thread.sleep(1000000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
2 楼 大马甲 2011-06-15  
学 习 学 习
1 楼 duanhengtao03 2011-06-15  
先顶了,然后再看!

相关推荐

    Java系统源码+科研工作量管理系统

    Java系统源码+科研工作量管理系统 内容概要: 本资源包含了完整的Java前后端源码及说明文档,适用于想要快速搭建并部署Java Web应用程序的开发者、学习者。 技术栈: 后端:Java生态系统,包含Spring Boot、Shiro、MyBatis等,数据库使用Mysql 前端:Vue、Bootstrap、Jquery等 适用场景示例: 1、毕业生希望快速启动一个新的Java Web应用程序。 2、团队寻找一个稳定的模板来加速产品开发周期。 3、教育机构或个人学习者用于教学目的或自学练习。 4、创业公司需要一个可以立即投入使用的MVP(最小可行产品)。

    毕业设计-智能优化之粒子群模型Python代码.rar

    1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、本项目仅用作交流学习参考,请切勿用于商业用途。

    Matlab实现SMA-KELM黏菌优化算法优化核极限学习机分类预测(含完整的程序,GUI设计和代码详解)

    内容概要:本文介绍了如何在MATLAB中实现结合黏菌优化算法(SMA)和核极限学习机(KELM)的分类预测模型。SMA优化KELM的超参数,提高模型的训练效率和预测精度,特别适用于处理复杂、高维数据集。文档详细阐述了项目背景、目标、挑战、模型架构、代码实现、结果展示、GUI设计、部署与应用等多个方面。 适合人群:具备一定MATLAB编程基础,对机器学习特别是优化算法和核方法感兴趣的科研人员和工程师。 使用场景及目标:①金融预测:股票价格、外汇市场等时间序列预测;②医疗诊断:疾病预测与辅助诊断;③工业故障检测:设备故障预警;④气象预测:天气变化预测;⑤市场营销:客户行为分析与预测。通过结合SMA和KELM,提升模型在高维数据上的分类和预测性能。 其他说明:文档不仅提供了详细的理论和方法介绍,还包含了完整的程序代码和GUI设计,有助于读者快速上手并应用到实际问题中。此外,文档还讨论了模型的部署、安全性和未来改进方向。

    Java jdbc for sqlserver2000 驱动包: msbase.jar;mssqlserver.jar;msutil.jar

    解压到项目下的LIB目录,在IDEA上右键,选添加为库即可。 连接代码如下: import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class SQL { public static void main(String[] args) { String driverName = "com.microsoft.jdbc.sqlserver.SQLServerDriver"; String connectionUrl = "jdbc:microsoft:sqlserver://localhost:1433;DatabaseName=数据库名"; String username = "sa"; String password = "口令"; try { Class.forNam……

    毕业设计-神经网络图像分类代码(可直接运行).rar

    1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、本项目仅用作交流学习参考,请切勿用于商业用途。

    水母检测4-YOLO(v5至v9)、COCO、CreateML、Paligemma、TFRecord、VOC数据集合集.rar

    水母检测4-YOLO(v5至v9)、COCO、CreateML、Paligemma、TFRecord、VOC数据集合集.rar水族馆-V2释放 ============================= *与您的团队在计算机视觉项目上合作 *收集和组织图像 *了解非结构化图像数据 *注释,创建数据集 *导出,训练和部署计算机视觉模型 *使用主动学习随着时间的推移改善数据集 它包括638张图像。 水族馆以可可格式注释。 将以下预处理应用于每个图像: 没有应用图像增强技术。

    电缆损坏检测8-YOLO(v5至v9)、COCO、CreateML、Darknet、Paligemma、TFRecord、VOC数据集合集.rar

    电缆损坏检测8-YOLO(v5至v9)、COCO、CreateML、Darknet、Paligemma、TFRecord、VOC数据集合集.rar电缆损坏-V2释放 ============================= *与您的团队在计算机视觉项目上合作 *收集和组织图像 *了解非结构化图像数据 *注释,创建数据集 *导出,训练和部署计算机视觉模型 *使用主动学习随着时间的推移改善数据集 它包括1318张图像。 电缆破坏以可可格式注释。 将以下预处理应用于每个图像: 没有应用图像增强技术。

    基于java的讯友网络相册.zip

    项目包含前后台完整源码。 项目都经过严格调试,确保可以运行! 具体项目介绍可查看博主文章或私聊获取 助力学习实践,提升编程技能,快来获取这份宝贵的资源吧!

    2024税务稽查典型案例分析(PPT格式,可编辑)

    2024年以来,税务稽查主要针对虚开骗税、骗取税收优惠、隐匿收入、虚增成本等行为,开展重点行业重点领域重点行为税收监管。本课程从案例出发,梳理稽查重点关注的问题行为,分析常见涉税疑点,供财务人员实务参考。

    content_1734090857469.docx

    content_1734090857469.docx

    阵列信号处理-MUSIC算法-幅相误差校正-协方差矩阵校正法-信噪比变化

    阵列信号处理,MUSIC算法中,使用基于协方差矩阵的幅相误差校正法实现幅相误差校正

    linux的概要介绍与分析

    以下是一个关于Linux系统管理与自动化脚本项目的资源描述及源码概要: 资源描述 本项目专注于Linux系统管理与自动化脚本开发,旨在通过一系列脚本提升系统运维效率。在资源准备阶段,我们深入研究了Linux系统架构、Shell脚本编程、以及常用系统管理命令。参考了《Linux命令行与Shell脚本编程大全》等经典书籍,以及Linux官方文档和在线社区,如Stack Overflow和Linux Academy,这些资源为我们提供了丰富的知识和实战案例。 项目实施过程中,我们利用Bash Shell作为主要脚本语言,结合sed、awk、grep等文本处理工具,以及cron作业调度器,实现了系统监控、日志分析、自动备份、用户管理等一系列自动化任务。同时,通过SSH和rsync等工具,实现了远程服务器管理和文件同步,极大地提高了运维的灵活性和效率。 项目源码概要 项目源码包含多个Shell脚本文件,每个脚本负责不同的自动化任务: system_monitor.sh:监控系统资源使用情况,如CPU、内存、磁盘空间等,并生成报告。 log_analyzer.sh:分析系统日志文件,提取关

    黑鲨4S完好机备份基带qcn 黑鲨4S基带qcn

    资源说明; 完好机备份的基带qcn文件 下载后解压 可以解决常规更新降级刷第三方导致的基带丢失。 会使用有需要的友友下载,不会使用的请不要下载 需要开端口才可以写入,不会开端口的请不要下载 希望我的资源可以为你带来帮助 谢谢 参考: https://blog.csdn.net/u011283906/article/details/124720894?spm=1001.2014.3001.5502

    javaweb学生信息管理系统-lw.zip

    项目包含前后台完整源码。 项目都经过严格调试,确保可以运行! 具体项目介绍可查看博主文章或私聊获取 助力学习实践,提升编程技能,快来获取这份宝贵的资源吧!

    情侣恋爱主题源码LikeGirl v5.2.0最终版

    情侣恋爱主题源码LikeGirlv5.2.0最终版,经过多次更新和优化,情侣小站现已正式定版为v5.2.0。从今日起,此版本将成为项目的最终版本。 维护终止:自2024年11月7日起,情侣小站将不再接受新的功能更新或bug 修复。 用户责任:如在使用过程中遇到任何问题,请自行修复或选择放弃使用。

    基于java进销存管理系统.zip

    项目包含前后台完整源码。 项目都经过严格调试,确保可以运行! 具体项目介绍可查看博主文章或私聊获取 助力学习实践,提升编程技能,快来获取这份宝贵的资源吧!

    园区监控方案【范本模板】.pdf

    园区监控方案【范本模板】.pdf

    基于ssm的房源管理系统源代码(java+vue+mysql+说明文档+LW).zip

    基于ssm的房源管理系统源代码(java+vue+mysql+说明文档+LW).zip

    商务大楼能源计量系统施工方法.docx

    商务大楼能源计量系统施工方法.docx

Global site tag (gtag.js) - Google Analytics