HashMap线程不安全在哪里

线程不安全

如果多个线程同时访问这个Map,而且至少一个线程对Map进行结构性的修改(增加,删除操作,update不算),那么它必须在外部进行同步。

比如一个线程对HashMap进行扩容,另外一个线程读取HashMap的值,扩容可能导致数组table的length变了。
比如,key原来对应2的位置,扩容后变成了18位置,相当于数组长度从16变成了32。
而一个线程在扩容之前取得了数组length是16,而在table[index]的时候刚好扩容完成,还是从2的位置读取元素,那肯定读出来的是null,而实际上HashMap中是有值的。
这样的数据错误,是由于线程不安全造成的。
而在两个线程同时写HashMap时候的数据不一致,这个只能从业务层做保证了,肯定会不一致,就算是ConcurrentHashMap也会产生不一致。

为什么ConcurrentHashMap可以做到在扩容的时候读取正确的值呢?
这里写图片描述

ConcurrentHashMap中在扩容的时候,会复制原来数组的元素table到nextTable,复制过程会对原有的链表头结点加sychronize。

在每个节点链表复制完成之后,把原有节点变成forwardingNode,指向nextTable。

这样,在复制过程中,get函数依然能获取正确的值:
1、该数组节点没有被复制完,那么则会查找Node类型的链表头结点
2、该数组节点已经被复制完,那么则会查找nextTable中的元素

如果该数组节点没有被复制完,且没有sychronized,那么说明还没有复制到这个节点,(因为节点复制是从数组下标为0开始的,逐个递增)那么可以在该节点的链表进行插入,插入需要加sychronized。

如果该数组节点没有复制完,且有sychronized,那么插入操作只能等待,直到获得锁。等该节点变成forwardingNode,就能获得锁,进行插入。这时插入操作会插入到nextTable中了。

如果该数组节点已经被复制完,那么是forwardingNode类型,需要插入到nextTable中了。

示例代码

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(0 L, "www.imxylz.cn");
        //map.put(1L, "www.imxylz.cn");
        for (int j = 0; j < count; j++) {
            newFixedThreadPool.submit(new Runnable() {
                public void run() {
                    map.put(System.nanoTime() + new Random().nextLong(), "www.imxylz.cn");
                    String obj = map.get(0 L);
                    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(500 L);
        }
    }
}

结果一定会输出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

http://coolshell.cn/articles/9606.html

展开阅读全文

没有更多推荐了,返回首页