2016-10-17 2 views
1

Играя с ConcurrentHashMap, я обнаружил, что computeIfAbsent в два раза медленнее, чем putIfAbsent. Вот простой тест:Почему ConcurrentHashMap :: putIfAbsent быстрее, чем ConcurrentHashMap :: computeIfAbsent?

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()) время выполнения 10068ms, для testCase2 (который выполняет тот же материал, но без упаковки его в computeIfAbsent)() казни время составляет 5009 мс (конечно, это немного меняется, но основная тенденция такова). Наиболее интересным является testCase3 - он почти такой же, как testCase1 (за исключением того, что putIfAbsent() используется вместо computeIfAbsent()), но его выполнение выполняется в два раза быстрее (5010ms для testCase3 против 10068ms для testCase1).

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

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

+1

Вы ничего не измеряете. Сделайте правильную отметку с помощью микросхемы с помощью JMH. –

+0

Перед тестированием необходимо подкрепить код. Запускайте каждый тест несколько раз в течение как минимум 10 секунд и игнорируйте эти результаты (только считайте те, которые появляются после) –

+0

'putIfAbsent()' уже имеет объект. 'computeIfAbsent()' должен выполнить метод для определения объекта. Все это документировано. – EJP

ответ

1

Вы Попадаются документированная особенность:

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

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

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

Смежные вопросы