java.util.HashMap在多线程环境中可能出现的问题

众所周知,HashMap不是线程安全的,但是一不小心就可能缺乏同步地用到了多线程环境里去了,那么在没有同步的时候,HashMap可能出现哪些问题呢?

一、put非null元素后get出来的却是null,具体分析如下:
get方法:

public V get(Object key) {
     if (key == null )
         return getForNullKey();
     int hash = hash(key.hashCode());
 
     // indexFor方法取得key在table数组中的索引,table数组中的元素是一个链表结构,
     // 遍历链表,取得对应key的value
     for (Entry 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 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 e = table[bucketIndex];
     table[bucketIndex] = new Entry(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 e = src[j];
         if (e != null ) {
             src[j] = null ;
             do {
                 Entry 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 map = new HashMap( 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            map.put( "name" + i, "value" + i);
         }
     }
}

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

其它可能由未同步HashMap导致的问题:

1、多线程put后可能导致get死循环(主要问题在于put的时候transfer方法循环将旧数组中的链表移动到新数组)

2、多线程put的时候可能导致元素丢失(主要问题出在addEntry方法的new Entry(hash, key, value, e),如果两个线程都同时取得了e,则他们下一个元素都是e,然后赋值给table元素的时候有一个成功有一个丢失)

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

你可能感兴趣的:(HashMap)