HashMap的读写并发

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

import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

public class HashMapTest2 {
    static void doit() throws Exception{
        final int count = 200;
        final AtomicInteger checkNum = new AtomicInteger(0);
        ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(100);
        //
        final Map<Long, String> map = new HashMap<Long, String>();
        map.put(0L, “www.imxylz.info”);
        for (int j = 0; j < count; j++) {
            newFixedThreadPool.submit(new Runnable() {
                public void run() {
                    map.put(System.nanoTime()+new Random().nextLong(), “www.imxylz.info”);
                    String obj = map.get(0L);
                    if (obj == null) {
                        checkNum.incrementAndGet();
                    }
                }
            });
        }
        newFixedThreadPool.awaitTermination(1, TimeUnit.SECONDS);
        newFixedThreadPool.shutdown();

        System.out.println(checkNum.get());
    }

    public static void main(String[] args) throws Exception{
        for(int i=0;i<10;i++) {
            doit();
            Thread.sleep(500L);
        }
    }
}

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

0
3
0
0
0
0
9
0
9
0

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

在HashMap中,有下面的源码:

public V get(Object key) {
    if (key == null)
    return getForNullKey();
    int hash = hash(key.hashCode());
    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;
}

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

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

很显然在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肯定也是没有问题了。