map 的问题.

一个线程在删除,一个线程在增加,map在扩容的时候,复制一份,复制的这份里面可能包含这个公司.导致remove的时候没有移除掉.

那么改为 currentHashMap 就可以了.

 

 

使用Java的同学应该都是知道HashMap是线程不安全的,不能够并发的去put和get,如果有并发操作,会抛出ConcurrentModificationException这个异常。不过可能很多同学没有注意,这个异常并不是一定会抛出的。而并发的去对HashMap对象进行put和get的结果,是可能造成死循环。
     年前在线上,也遇到了这么一次。当时是有同学找过来,说有台机器的load很高,已经启动了流量控制,也没有请求进到容器中。后来dump了thread,也通过top -H看到占用CPU比较高的线程,发现都是在执行HashMap.get,想到可能就是上面的原因,后来看了源码,发现确实是这么一个问题。
 具体对于HashMap在并发操作时陷入死循环的分析,可以参看这篇博客

http://pt.alibaba-inc.com/wp/dev_related_969/hashmap-result-in-improper-use-cpu-100-of-the-problem-investigated.html

 

死循环理解:

map扩容的时候,比如两个节点 E1,E2   本来是E2的next 是E1.

在两个线程情况下,第一个线程跑完了扩容,导致key冲排序E1指向E2.此时第二个线程 刚将E2的next指向E1拿到 (数据已经改变)

当第二个线程运行到E2的时候,还是使用老的排序,认为E2指向E1 导致死循环.

 

 ---------------------并发环境下,HashMap不能准确获取值--------------------------

大家都知道HashMap不是线程安全的,但是大家的理解可能都不是十分准确。很显然读写同一个key会导致不一致大家都能理解,但是如果读写一个不变的对象会有问题么?看看下面的代码就明白了。

1 import java.util.HashMap; 
   
 2 import java.util.Map; 
   
 3 import java.util.Random; 
   
 4 import java.util.concurrent.ExecutorService; 
   
 5 import java.util.concurrent.Executors; 
   
 6 import java.util.concurrent.TimeUnit; 
   
 7 import java.util.concurrent.atomic.AtomicInteger; 
   
 8  
   
 9 public class HashMapTest2 { 
   
10     static void doit() throws Exception{ 
   
11         final int count = 200; 
   
12         final AtomicInteger checkNum = new AtomicInteger(0); 
   
13         ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(100); 
   
14         // 
   
15         final Map<Long, String> map = new HashMap<Long, String>(); 
   
16         map.put(0L, "www.imxylz.cn"); 
   
17         //map.put(1L, "www.imxylz.cn"); 
   
18         for (int j = 0; j < count; j++) { 
   
19             newFixedThreadPool.submit(new Runnable() { 
   
20                 public void run() { 
   
21                     map.put(System.nanoTime()+new Random().nextLong(), "www.imxylz.cn"); 
   
22                     String obj = map.get(0L); 
   
23                     if (obj == null) { 
   
24                         checkNum.incrementAndGet(); 
   
25                     } 
   
26                 } 
   
27             }); 
   
28         } 
   
29         newFixedThreadPool.awaitTermination(1, TimeUnit.SECONDS); 
   
30         newFixedThreadPool.shutdown(); 
   
31          
   
32         System.out.println(checkNum.get()); 
   
33     } 
   
34      
   
35     public static void main(String[] args) throws Exception{ 
   
36         for(int i=0;i<10;i++) { 
   
37             doit(); 
   
38             Thread.sleep(500L); 
   
39         } 
   
40     } 
   
41 } 
   
42

结果一定会输出0么?结果却不一定。比如某一次的结果是:

0 
   
3 
   
0 
   
0 
   
0 
   
0 
   
9 
   
0 
   
9 
   
0

 

查看了源码,其实出现这个问题是因为HashMap在扩容是导致了重新进行hash计算。

在HashMap中,有下面的源码:

 


1     public V get(Object key) { 
  
 2         if (key == null) 
  
 3             return getForNullKey(); 
  
 4         int hash = hash(key.hashCode()); 
  
 5         for (Entry<K,V> e = table[indexFor(hash, table.length)]; 
  
 6              e != null; 
  
 7              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     }



 

在indexOf中就会导致计算有偏移。

1 static int indexFor(int h, int length) { 
   
2         return h & (length-1); 
   
3     }

很显然在Map的容量(table.length,数组的大小)有变化时就会导致此处计算偏移变化。这样每次读的时候就不一定能获取到目标索引了。为了证明此猜想,我们改造下,变成以下的代码。

final Map<String, String> map = new HashMap<String, String>(10000);
0 
   
0 
   
0 
   
0 
   
0 
   
0 
   
0 
   
0 
   
0 
   
0

当然了如果只是读,没有写肯定没有并发的问题了。改换Hashtable或者ConcurrentHashMap肯定也是没有问题了。