`
freish
  • 浏览: 83871 次
  • 性别: 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  
先顶了,然后再看!

相关推荐

    HashMap源码实现红黑树添加元素和删除元素

    HashMap 中元素的存储规则是按照 key 的 hash 值找到下标,然后判断该位置是否有元素,如果有就将尾节点的 Node 的 next 指向新的元素;如果没有就直接插入。 红黑树的插入操作: 在红黑树中,插入操作是通过 ...

    HashMap总结

    1. 使用迭代器遍历:使用 iterator() 方法取得 HashMap 的迭代器,然后使用 hasNext() 和 next() 方法遍历 HashMap 中的元素。 2. 使用 foreach 遍历:使用 foreach 语句遍历 HashMap 中的元素。 HashMap 的常用...

    hashmap面试题_hashmap_

    3. 如何避免HashMap中的哈希碰撞? 答:通过良好的键的hashCode()实现减少哈希冲突,以及使用链表/红黑树处理哈希冲突。 4. HashMap与Hashtable的区别? 答:HashMap非线程安全,而Hashtable是线程安全的;HashMap...

    hashMap利用iterator迭代器迭代元素方法

    当我们需要遍历`HashMap`中的所有元素时,通常会使用`Iterator`接口,它是Java集合框架的一部分,提供了对集合的迭代访问。 `Iterator`接口定义了三个基本方法:`hasNext()`、`next()`和`remove()`。`hasNext()`...

    HashMap 分析

    通常,负载因子的默认值为0.75,意味着当HashMap中元素的数量超过其容量的75%时,就会进行扩容操作。 在给出的文档片段中,我们可以看到两种情况:一种是负载因子为0.75,另一种是负载因子为0.3。这两种情况下,都...

    HashMap介绍和使用

    当向HashMap中添加元素时,系统会根据键的哈希值计算出其在数组中的位置,并将元素存放在该位置。如果该位置已有元素,则将新元素以链表形式连接到已有元素之后。 #### 二、哈希算法 为了提高查找效率,HashMap...

    Java中HashMap的工作机制

    如果在冲突链中遍历完都没有找到相等的key,则返回null,表示键不存在于HashMap中。 值得注意的是,由于HashMap的Entry数组是动态扩容的,数组的长度会根据实际存储的键值对数量进行增长,以保持较低的哈希冲突率。...

    hashmap实现原理

    这意味着当HashMap中存储的元素数量达到16 * 0.75,即12个元素时,HashMap会自动扩容,将容量翻倍。扩容过程包括创建一个新的、更大的数组,并通过rehash操作将旧数组中的元素迁移到新数组中,以保持键值对的正确...

    HashMap之resize()方法源码解读.docx

    HashMap的resize()方法是HashMap中最核心的方法之一,该方法负责扩容HashMap的容量,以便存储更多的键值对。下面我们将对HashMap的resize()方法进行源码解读,了解其扩容机制和原理。 一、resize()方法概述 resize...

    HashMap如何添加元素详解

    map接口是一个双边队列,拥有key,value两个属性,其中key在存储的集合中不允许重复,value可以重复。 HashMap特点 存储结构在jdk1.7当中是数组加链表的结构,在jdk1.8当中改为了数组加链表加红黑树的结构。 HashMap...

    Java中HashMap详解(通俗易懂).doc

    当向HashSet添加元素时,实际上是在HashMap中添加键,而值是默认的null。由于HashSet没有值的概念,所以它不提供键值对的关联操作,仅提供基本的添加、删除和查询操作。HashSet的元素存储同样依赖于哈希函数和链地址...

    java中HashMap详解.pdf

    在添加元素时,如果HashMap中的元素数量超过了一个临界值(阈值,threshold),HashMap的容量会加倍,以减少进一步操作中的冲突,并提供更大的空间来存储更多的元素。这个过程涉及到数组的扩容(resize),新的数组...

    java中HashMap详解

    当HashMap中的元素数量达到容量(初始容量或扩容后的容量)与负载因子的乘积时,会进行扩容。扩容时,HashMap会创建一个新的、容量更大的数组,并将旧数组中的元素重新哈希到新数组中。 7. **删除元素**: 删除...

    HashMap的数据结构

    7. **键的唯一性**:HashMap中的键是唯一的,这意味着不能有两个相同的键存在。如果尝试插入一个已经存在的键,新的值将会替换旧的值。 8. **迭代器**:HashMap提供了迭代器`keySet()`、`values()`和`entrySet()`,...

    Java HashMap 如何正确遍历并删除元素的方法小结

    这段代码将抛出 `java.util.ConcurrentModificationException` 异常,因为在遍历 HashMap 的元素过程中删除了当前所在元素,下一个待访问的元素的指针也由此丢失了。 2. 正确的删除方法 正确的删除方法是使用迭代...

    Java HashMap类详解

    本资源详细介绍了 Java 中的 HashMap 类,包括其实现机制、Hash 存储机制、集合存储机制等方面的知识点。 1. HashMap 和 HashSet 的关系 HashMap 和 HashSet 是 Java Collection Framework 的两个重要成员,虽然...

    Hashmap详解

    put 方法是 HashMap 中最重要的方法之一,它负责将键值对添加到 HashMap 中。put 方法的实现可以分为以下几个步骤: 1. 首先,根据键的 hashCode 值计算 Hash 码,Hash 码是用于确定键值对在数组中的索引。 2. 然后...

    基于JavaScript的HashMap实现

    6. **容量控制**:当HashMap中的元素数量超过其容量与负载因子的乘积时,需要进行扩容操作。 7. **扩容策略**:扩容通常会创建一个新的更大的哈希表,并将旧表中的所有元素重新插入新表。 8. **遍历**:提供`...

    易语言HashMap类

    6. **取所有键**和**取所有值**:这两个方法分别返回HashMap中所有的键和值,以数组或列表形式提供,便于遍历和处理。 7. **枚举所有键**:通过枚举器,开发者可以按顺序遍历HashMap中的所有键,这对于遍历整个哈希...

    java HashMap原理分析

    在HashMap中,哈希函数用于将Key转换为一个哈希码,然后根据哈希码将Key-Value对存储在数组中的特定位置上。哈希函数的应用非常广泛,例如在加密、数据压缩和数据库查询等领域。 2. HashMap的存储原理和查询效率 ...

Global site tag (gtag.js) - Google Analytics