当你明明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出现了违反直觉的现象,那么可能就是并发导致的了
分享到:
相关推荐
HashMap源码实现红黑树添加元素和删除元素
动态扩容:当HashMap中的元素数量超过了容量(默认为16)与负载因子(默认为0.75)的乘积时,HashMap会自动扩容,即创建一个新的数组,并将原来的元素重新映射到新的数组中。 高性能:由于使用了哈希表,HashMap在查找...
map接口是一个双边队列,拥有key,value两个属性,其中key在存储的集合中不允许重复,value可以重复。 HashMap特点 存储结构在jdk1.7当中是数组加链表的结构,在jdk1.8当中改为了数组加链表加红黑树的结构。 HashMap...
这个是利用iterator迭代器来对hashMap进行迭代元素,数据java基础的内容。有兴趣的朋友可以看看。
hashmap中hash函数的构造问题,提供了各种构造方法。以及比较函数的构造 挺适合入门学习的
HashMap源码流程图 一图解析HashMap源码流程 // 默认的HashMap中数组的长度 16 static final int DEFAULT_INITIAL_CAPACITY = 1 ; // aka 16 // HashMap中的数组的最大容量 ...// HashMap中的元素个数
Java、hashMap
hashmap实例 hashmap实例hashmap实例hashmap实例
HashMap介绍和使用
hashmap相关的面试题
java中HashMap详解.pdf
HashMap存放.doc
本文档主要讲述的是java中HashMap详解;HashMap和HashSet是Java Collection Framework的两个重要成员,其中HashMap是Map接口的常用实现类,HashSet是Set接口的常用实现类。虽然HashMap和HashSet实现的接口规范不同,...
HashMap数据结构,HashMap的构造方法,HashMap的put,HashMap的get
HashMap中红黑树TreeNode的split方法源码解读,对split方法源码的上下文/变量定义,及所调用的关键方法都给出了详细解释说明,欢迎指正
Hashmap详解
HashMap是一个散列桶(数组和链表),它存储的内容是键值对(key-value)映射HashMap采用了数组和链表的数据结构,能在查询和修改方便继承了数组的线性查找和链表的寻址修改HashMap是非synchronized,所以HashMap很快...
模拟java中的HashMap类js类对象,可以与js的Array类对象配合使用
hashMap排序,hashmap使用还是比较频繁。这时自己写的一个实现hashmap排序的例子
Javascript实现和操作HashMap,压缩包里面有hashmap定义和操作的例子