Почему ConcurrentHashMap :: putIfAbsent работает быстрее, чем ConcurrentHashMap :: computeIfAbsent?

2

Играя с 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();
    }
}

Выполняя этот тест на моем ноутбуке, testCase1 (который использует время выполнения computeIfAbsent()) составляет 10068 мс, для testCase2 (который выполняет тот же самый материал, но БЕЗ его упаковки в computeIfAbsent()), время выполнения составляет 5009 мс (конечно, оно немного меняется, но это основной тренд). Наиболее интересным является testCase3 - он почти такой же, как testCase1 (за исключением того, что putIfAbsent() используется вместо computeIfAbsent()), но его выполнение выполняется в два раза быстрее (5010ms для testCase3 против 10068ms для testCase1).

Рассматривая исходный код, он почти такой же как для computeIfAbsent(), так и для putVal() (который используется в putIfAbsent() под капотом).

Знает ли кто-нибудь, что вызывает различное время выполнения потоков?

  • 1
    Вы ничего не измеряете. Сделайте правильный микробенчмарк, используя JMH
  • 0
    Вы должны подготовить код перед его тестированием. Запускайте каждый тест несколько раз в течение не менее 10 секунд и игнорируйте эти результаты (считайте только те, которые следуют)
Показать ещё 1 комментарий
Теги:
performance

1 ответ

2
Лучший ответ

Вы сталкиваетесь с документированной функцией:

Некоторые попытки выполнить операции обновления на этой карте другими потоками могут быть заблокированы, пока выполняется вычисление, поэтому вычисление должно быть коротким и простым и не должно пытаться обновлять любые другие сопоставления этой карты.

computeIfAbsent проверяет наличие ключа и блокирует часть карты. Затем он вызывает функтор и помещает результат в карту (если возвращаемое значение не равно нулю). Только после этого эта часть карты разблокирована.

С другой стороны, test3 всегда вызывает c.call(), а после завершения вычисления он вызывает putIfAbsent.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню