HashMap中的元素玩起了躲猫猫

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

 

Java代码 复制代码  收藏代码
  1. public V get(Object key) {   
  2.         if (key == null)   
  3.             return getForNullKey();   
  4.         int hash = hash(key.hashCode());   
  5.   
  6.         // indexFor方法取得key在table数组中的索引,table数组中的元素是一个链表结构,遍历链表,取得对应key的value   
  7.         for (Entry<K, V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {   
  8.             Object k;   
  9.             if (e.hash == hash && ((k = e.key) == key || key.equals(k)))   
  10.                 return e.value;   
  11.         }   
  12.         return null;   
  13.     }  
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方法:

 

Java代码 复制代码  收藏代码
  1. public V put(K key, V value) {   
  2.         if (key == null)   
  3.             return putForNullKey(value);   
  4.         int hash = hash(key.hashCode());   
  5.         int i = indexFor(hash, table.length);   
  6.         for (Entry<K, V> e = table[i]; e != null; e = e.next) {   
  7.             Object k;   
  8.             if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {   
  9.                 V oldValue = e.value;   
  10.                 e.value = value;   
  11.                 e.recordAccess(this);   
  12.                 return oldValue;   
  13.             }   
  14.         }   
  15.   
  16.         modCount++;   
  17.         // 若之前没有put进该key,则调用该方法   
  18.         addEntry(hash, key, value, i);   
  19.         return null;   
  20.     }  
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里面的实现:

 

Java代码 复制代码  收藏代码
  1. void addEntry(int hash, K key, V value, int bucketIndex) {   
  2.         Entry<K, V> e = table[bucketIndex];   
  3.         table[bucketIndex] = new Entry<K, V>(hash, key, value, e);   
  4.         if (size++ >= threshold)   
  5.             resize(2 * table.length);   
  6.     }  
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方法:

 

 

Java代码 复制代码  收藏代码
  1. void resize(int newCapacity) {   
  2.         Entry[] oldTable = table;   
  3.         int oldCapacity = oldTable.length;   
  4.         if (oldCapacity == MAXIMUM_CAPACITY) {   
  5.             threshold = Integer.MAX_VALUE;   
  6.             return;   
  7.         }   
  8.   
  9.         Entry[] newTable = new Entry[newCapacity];   
  10.         transfer(newTable);   
  11.         table = newTable;   
  12.         threshold = (int) (newCapacity * loadFactor);   
  13.     }  
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方法:

 

Java代码 复制代码  收藏代码
  1. void transfer(Entry[] newTable) {   
  2.         Entry[] src = table;   
  3.         int newCapacity = newTable.length;   
  4.         for (int j = 0; j < src.length; j++) {   
  5.             Entry<K, V> e = src[j];   
  6.             if (e != null) {   
  7.                 src[j] = null;   
  8.                 do {   
  9.                     Entry<K, V> next = e.next;   
  10.                     int i = indexFor(e.hash, newCapacity);   
  11.                     e.next = newTable[i];   
  12.                     newTable[i] = e;   
  13.                     e = next;   
  14.                 } while (e != null);   
  15.             }   
  16.         }   
  17.     }  
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了,也就是这一句:

 

Java代码 复制代码  收藏代码
  1. if (e != null) {   
  2.         src[j] = null;  
if (e != null) {
		src[j] = null;

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

 

 

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

 

Java代码 复制代码  收藏代码
  1. import java.util.HashMap;   
  2. import java.util.Map;   
  3. public class TestHashMap {   
  4.     public static void main(String[] args) {   
  5.         final Map<String, String> map = new HashMap<String, String>(40.5f);   
  6.            
  7.         new Thread(){   
  8.             public void run() {   
  9.                 while(true) {    
  10.                     System.out.println(map.get("name1"));   
  11.                     try {   
  12.                         Thread.sleep(1000);   
  13.                     } catch (InterruptedException e) {   
  14.                         e.printStackTrace();   
  15.                     }   
  16.                 }   
  17.             }   
  18.         }.start();   
  19.         for(int i=0; i<3; i++) {   
  20.             map.put("name" + i, "value" + i);   
  21.         }   
  22.     }   
  23. }  
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出现了违反直觉的现象,那么可能就是并发导致的了

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值