为什么ConcurrentHashMap的putIfAbsent比computeIfAbsent更快?

4

在使用ConcurrentHashMap时,我发现computeIfAbsent方法的速度是putIfAbsent方法的两倍。这里是一个简单的测试:

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;


public class Test {
    public static void main(String[] args) throws Exception {
        String[] keys = {"a1", "a2", "a3", "a4", "a5", "a6", "a7", "a8", "a9", "a0", "a01", "a02", "a03", "a04", "a05", "a06", "a07", "a08", "a09", "a00"};

        System.out.println("Test case 1");
        long time = System.currentTimeMillis();
        testCase1(keys);
        System.out.println("ExecutionTime: " + String.valueOf(System.currentTimeMillis() - time));

        System.out.println("Test case 2");
        time = System.currentTimeMillis();
        testCase2(keys);
        System.out.println("ExecutionTime: " + String.valueOf(System.currentTimeMillis() - time));

        System.out.println("Test case 3");
        time = System.currentTimeMillis();
        testCase3(keys);
        System.out.println("ExecutionTime: " + String.valueOf(System.currentTimeMillis() - time));
    }

    public static void testCase1(String[] keys) throws InterruptedException {
        ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();

        List<Thread> threads = new ArrayList<>();

        for (String key : keys) {
            Thread thread = new Thread(() -> map.computeIfAbsent(key, s -> {
                System.out.println(key);
                String result = new TestRun().compute();
                System.out.println("Computing finished for " + key);
                return result;
            }));
            thread.start();
            threads.add(thread);
        }

        for (Thread thread : threads) {
            thread.join();
        }
    }

    public static void testCase2(String[] keys) throws InterruptedException {
        List<Thread> threads = new ArrayList<>();

        for (String key : keys) {
            Thread thread = new Thread(() -> {
                System.out.println(key);
                new TestRun().compute();
                System.out.println("Computing finished for " + key);
            });
            thread.start();
            threads.add(thread);
        }

        for (Thread thread : threads) {
            thread.join();
        }
    }


    public static void testCase3(String[] keys) throws InterruptedException {
        ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();

        List<Thread> threads = new ArrayList<>();

        for (String key : keys) {
            Thread thread = new Thread(() -> {
                Callable<String> c = () -> {
                    System.out.println(key);
                    String result = new TestRun().compute();
                    System.out.println("Computing finished for " + key);
                    return result;
                };

                try {
                    map.putIfAbsent(key, c.call());
                } catch (Exception e) {
                    e.printStackTrace(System.out);
                }
            });
            thread.start();
            threads.add(thread);
        }

        for (Thread thread : threads) {
            thread.join();
        }
    }

}

class TestRun {
    public String compute() {
        try {
            Thread.currentThread().sleep(5000);
        } catch (Exception e) {
            e.printStackTrace(System.out);
        }
        return UUID.randomUUID().toString();
    }
}

在我的笔记本电脑上运行此测试,使用computeIfAbsent()的testCase1执行时间为10068ms,而在未将其封装到computeIfAbsent()中执行相同操作的testCase2执行时间为5009ms(当然会有些许变化,但主要趋势是如此)。最有趣的是testCase3——它与testCase1非常相似(除了使用putIfAbsent()代替computeIfAbsent()),但其执行速度快两倍(testCase3的执行时间为5010ms,而testCase1为10068ms)。
从源代码来看,computeIfAbsent()和putVal()(在putIfAbsent()中使用)基本相同。
有谁知道是什么导致线程执行时间不同吗?

2
你没有进行任何测量。请使用JMH进行适当的微基准测试。 - Svetlin Zarev
你需要在测试代码之前准备好它。多次运行每个测试至少10秒钟,并忽略那些结果(仅计算之后得到的结果)。 - Peter Lawrey
putIfAbsent() 已经有了对象。 computeIfAbsent() 必须执行一个方法来确定对象。这都有文档记录。 - user207421
为了匆忙的读者:测试代码使用 Thread.sleep(5000) 来很好地展示 ConcurrentHashMap 的一个特定行为。至少在这个问题上,没有必要进行适当的基准测试。 - Luke Usherwood
1个回答

4

你遇到了文档中的功能:

在进行计算时,其他线程对此映射尝试的更新操作可能会被阻塞,因此计算应简短且简单,并且不能尝试更新此映射的任何其他映射。

computeIfAbsent 检查键是否存在并锁定地图的某些部分。然后它调用函数对象并将结果放入地图中(如果返回值不为空)。只有在此地图部分解锁后,才能执行这个操作。

另一方面,test3总是调用 c.call(),计算结束后,才调用 putIfAbsent。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接