jdk

JDK1.8的ConcurrentHashMap提供的compu

2021-01-30  本文已影响0人  小胖学编程

若key对应的value已经存在,多个线程并发获取该key对应value时。会存在性能问题。

bug详见:https://bugs.openjdk.java.net/browse/JDK-8161372

1. 源码分析

源码分析:
ConcurrentHashMap提供的computeIfAbsent源码分析

    public V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) {
        if (key == null || mappingFunction == null)
            throw new NullPointerException();
        int h = spread(key.hashCode());
        V val = null;
        int binCount = 0;
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
           //hash没有初始化时
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            else if ((f = tabAt(tab, i = (n - 1) & h)) == null) {  //key未维护value时
                Node<K,V> r = new ReservationNode<K,V>();
                synchronized (r) {
                    if (casTabAt(tab, i, null, r)) {
                        binCount = 1;
                        Node<K,V> node = null;
                        try {
                            if ((val = mappingFunction.apply(key)) != null)
                                node = new Node<K,V>(h, key, val, null);
                        } finally {
                            setTabAt(tab, i, node);
                        }
                    }
                }
                if (binCount != 0)
                    break;
            }
            else if ((fh = f.hash) == MOVED)   //hash迁移
                tab = helpTransfer(tab, f);
            else {    //value已经存在时,但是获取value时,有sync关键字
                boolean added = false;
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        if (fh >= 0) {
                            binCount = 1;
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek; V ev;
                                if (e.hash == h &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    val = e.val;
                                    break;
                                }
                                Node<K,V> pred = e;
                                if ((e = e.next) == null) {
                                    if ((val = mappingFunction.apply(key)) != null) {
                                        added = true;
                                        pred.next = new Node<K,V>(h, key, val, null);
                                    }
                                    break;
                                }
                            }
                        }
                        else if (f instanceof TreeBin) {
                            binCount = 2;
                            TreeBin<K,V> t = (TreeBin<K,V>)f;
                            TreeNode<K,V> r, p;
                            if ((r = t.root) != null &&
                                (p = r.findTreeNode(h, key, null)) != null)
                                val = p.val;
                            else if ((val = mappingFunction.apply(key)) != null) {
                                added = true;
                                t.putTreeVal(h, key, val);
                            }
                        }
                    }
                }
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (!added)
                        return val;
                    break;
                }
            }
        }
        if (val != null)
            addCount(1L, binCount);
        return val;
    }

由上面的源码可知:当value存在时,每次获取value都有sync关键字。

2. 复现

测试代码

@Slf4j
public class Main {
    static final int MAP_SIZE = 20;
    static final int THREADS = 20;
    static final ConcurrentHashMap<Integer, Integer> map = new ConcurrentHashMap<>();

    static {
        for (int i = 0; i < MAP_SIZE; i++) map.put(i, i);
    }

    static class TestThread extends Thread {
        public void run() {
            int i = 0;
            int result = 0;
            while (result < Integer.MAX_VALUE) {
                i = (i + 1) % MAP_SIZE;
                result += map.computeIfAbsent(i, (key) -> key + key);
            }
        }
    }

    public static void main(String[] v) throws InterruptedException {
        ArrayList<Thread> threads = new ArrayList<>();
        for (int i = 0; i < THREADS; i++) {
            TestThread t = new TestThread();
            threads.add(t);
            t.start();
        }
        threads.get(0).join();
    }
}

使用jps命令查询pid,使用jstack pid命令查询线程堆栈信息。

"Thread-4" #15 prio=5 os_prio=31 tid=0x00007fb83e92e800 nid=0xa403 waiting for monitor entry [0x000070000900a000]
   java.lang.Thread.State: BLOCKED (on object monitor)
    at java.util.concurrent.ConcurrentHashMap.computeIfAbsent(ConcurrentHashMap.java:1674)
    - waiting to lock <0x000000076b4c5470> (a java.util.concurrent.ConcurrentHashMap$Node)
    at com.tellme.test.Main$TestThread.run(Main.java:29)

可以看到,当大并发获取value时,会在ConcurrentHashMap.java:1674行被阻塞。而这一行就是synchronized (f)代码。

3. 优化

优化方案:直接先通过get方法获取value,获取不到时,才使用computeIfAbsent方法去维护缓存。

    public static <K, V> V computeIfAbsent(Map<K, V> concurrentHashMap, K key, Function<? super K, ? extends V> mappingFunction) {
        V v = concurrentHashMap.get(key);
        if (v != null) {
            return v;
        }
        return concurrentHashMap.computeIfAbsent(key, mappingFunction);
    }
上一篇 下一篇

猜你喜欢

热点阅读