ConcurrentHashMap: избегайте создания дополнительных объектов с помощью "putIfAbsent"?

Я суммировать несколько значений для ключей в многопоточной среде. Ключи заранее не известны. Я думал, что сделаю что-то вроде этого:

class Aggregator {
    protected ConcurrentHashMap<String, List<String>> entries =
                            new ConcurrentHashMap<String, List<String>>();
    public Aggregator() {}

    public void record(String key, String value) {
        List<String> newList =
                    Collections.synchronizedList(new ArrayList<String>());
        List<String> existingList = entries.putIfAbsent(key, newList);
        List<String> values = existingList == null ? newList : existingList;
        values.add(value);
    }
}

проблему я вижу в том, что каждый раз, когда этот метод работает, мне нужно создать новый экземпляр ArrayList, который я затем выбрасываю (в большинстве случаев). Это выглядит как неоправданное злоупотребление мусорщиком. Есть ли лучший, потокобезопасный способ инициализации такого рода структуры без необходимости synchronize the record способ? Я несколько удивлен решением для putIfAbsent метод не возвращает вновь созданный элемент, и из-за отсутствия способа отложить создание экземпляра, если он не вызван (так сказать).

7 ответов


Java 8 представила API для удовлетворения этой точной проблемы, сделав 1-строчное решение:

public void record(String key, String value) {
    entries.computeIfAbsent(key, k -> Collections.synchronizedList(new ArrayList<String>())).add(value);
}

Для Java 7:

public void record(String key, String value) {
    List<String> values = entries.get(key);
    if (values == null) {
        entries.putIfAbsent(key, Collections.synchronizedList(new ArrayList<String>()));
        // At this point, there will definitely be a list for the key.
        // We don't know or care which thread's new object is in there, so:
        values = entries.get(key);
    }
    values.add(value);
}

это стандартный шаблон кода при вставке ConcurrentHashMap.

специальный метод putIfAbsent(K, V)) либо поставит ваш объект value, либо, если другой поток получил перед вами, то он будет игнорировать ваш объект value. В любом случае, после вызова putIfAbsent(K, V)), get(key) гарантировано, что будет последователен между потоками и поэтому приведенный выше код является threadsafe.

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


начиная с Java-8 Вы можете создавать Мультикарты, используя следующий шаблон:

public void record(String key, String value) { entries.computeIfAbsent(key, k -> Collections.synchronizedList(new ArrayList<String>())) .add(value); }

документация ConcurrentHashMap (не генеральный контракт) указывает, что ArrayList будет создан только один раз для каждого ключа, при небольшой начальной стоимости задержки обновлений, пока ArrayList создается для нового ключ:

http://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ConcurrentHashMap.html#computeIfAbsent-K-java.util.function.Function-


в конце концов, я реализовал небольшую модификацию ответа @Bohemian. Его предлагаемое решение перезаписывает values переменной с putIfAbsent вызов, который создает ту же проблему, что и раньше. Код, который, кажется, работает, выглядит так:

    public void record(String key, String value) {
        List<String> values = entries.get(key);
        if (values == null) {
            values = Collections.synchronizedList(new ArrayList<String>());
            List<String> values2 = entries.putIfAbsent(key, values);
            if (values2 != null)
                values = values2;
        }
        values.add(value);
    }

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


создал два варианта ответа Гена

public  static <K,V> void putIfAbsetMultiValue(ConcurrentHashMap<K,List<V>> entries, K key, V value) {
    List<V> values = entries.get(key);
    if (values == null) {
        values = Collections.synchronizedList(new ArrayList<V>());
        List<V> values2 = entries.putIfAbsent(key, values);
        if (values2 != null)
            values = values2;
    }
    values.add(value);
}

public  static <K,V> void putIfAbsetMultiValueSet(ConcurrentMap<K,Set<V>> entries, K key, V value) {
    Set<V> values = entries.get(key);
    if (values == null) {
        values = Collections.synchronizedSet(new HashSet<V>());
        Set<V> values2 = entries.putIfAbsent(key, values);
        if (values2 != null)
            values = values2;
    }
    values.add(value);
}

Он работает


это проблема, на которую я также искал ответ. Метод putIfAbsent фактически не решает проблему создания дополнительного объекта, он просто гарантирует, что один из этих объектов не заменит другой. Но условия гонки между потоками могут привести к созданию нескольких экземпляров объектов. Я мог бы найти 3 решения для этой проблемы (и я бы следовал этому порядку предпочтений):

1-Если вы находитесь на Java 8, лучший способ достичь этого, вероятно, новый computeIfAbsent метод ConcurrentMap. Вам просто нужно дать ему вычислительную функцию, которая будет выполняться синхронно (по крайней мере, для ConcurrentHashMap реализации). Пример:

private final ConcurrentMap<String, List<String>> entries =
        new ConcurrentHashMap<String, List<String>>();

public void method1(String key, String value) {
    entries.computeIfAbsent(key, s -> new ArrayList<String>())
            .add(value);
}

это из javadoc ConcurrentHashMap.computeIfAbsent:

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

2-Если вы не можете использовать Java 8, вы можете использовать Guava ' s LoadingCache, который является потокобезопасным. Вы определяете функцию загрузки для нее (так же, как


отходы памяти (также GC и т. д.) эта проблема создания пустого списка массивов обрабатывается Java 1.7.40. Не беспокойтесь о создании пустого arraylist. Ссылка: http://javarevisited.blogspot.com.tr/2014/07/java-optimization-empty-arraylist-and-Hashmap-cost-less-memory-jdk-17040-update.html


С putIfAbsent имеет самое быстрое время выполнения, он составляет от 2 до 50 раз быстрее, чем подход "лямбда" в evironments с высокой конкуренцией. лямбда не является причиной этого "powerloss", проблема заключается в обязательной синхронизации внутри computeIfAbsent до оптимизации Java-9.

тест:

import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;

public class ConcurrentHashMapTest {
    private final static int numberOfRuns = 1000000;
    private final static int numberOfThreads = Runtime.getRuntime().availableProcessors();
    private final static int keysSize = 10;
    private final static String[] strings = new String[keysSize];
    static {
        for (int n = 0; n < keysSize; n++) {
            strings[n] = "" + (char) ('A' + n);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        for (int n = 0; n < 20; n++) {
            testPutIfAbsent();
            testComputeIfAbsentLamda();
        }
    }

    private static void testPutIfAbsent() throws InterruptedException {
        final AtomicLong totalTime = new AtomicLong();
        final ConcurrentHashMap<String, AtomicInteger> map = new ConcurrentHashMap<String, AtomicInteger>();
        final Random random = new Random();
        ExecutorService executorService = Executors.newFixedThreadPool(numberOfThreads);

        for (int i = 0; i < numberOfThreads; i++) {
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    long start, end;
                    for (int n = 0; n < numberOfRuns; n++) {
                        String s = strings[random.nextInt(strings.length)];
                        start = System.nanoTime();

                        AtomicInteger count = map.get(s);
                        if (count == null) {
                            count = new AtomicInteger(0);
                            AtomicInteger prevCount = map.putIfAbsent(s, count);
                            if (prevCount != null) {
                                count = prevCount;
                            }
                        }
                        count.incrementAndGet();
                        end = System.nanoTime();
                        totalTime.addAndGet(end - start);
                    }
                }
            });
        }
        executorService.shutdown();
        executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.DAYS);
        System.out.println("Test " + Thread.currentThread().getStackTrace()[1].getMethodName()
                + " average time per run: " + (double) totalTime.get() / numberOfThreads / numberOfRuns + " ns");
    }

    private static void testComputeIfAbsentLamda() throws InterruptedException {
        final AtomicLong totalTime = new AtomicLong();
        final ConcurrentHashMap<String, AtomicInteger> map = new ConcurrentHashMap<String, AtomicInteger>();
        final Random random = new Random();
        ExecutorService executorService = Executors.newFixedThreadPool(numberOfThreads);
        for (int i = 0; i < numberOfThreads; i++) {
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    long start, end;
                    for (int n = 0; n < numberOfRuns; n++) {
                        String s = strings[random.nextInt(strings.length)];
                        start = System.nanoTime();

                        AtomicInteger count = map.computeIfAbsent(s, (k) -> new AtomicInteger(0));
                        count.incrementAndGet();

                        end = System.nanoTime();
                        totalTime.addAndGet(end - start);
                    }
                }
            });
        }
        executorService.shutdown();
        executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.DAYS);
        System.out.println("Test " + Thread.currentThread().getStackTrace()[1].getMethodName()
                + " average time per run: " + (double) totalTime.get() / numberOfThreads / numberOfRuns + " ns");
    }

}

результаты:

Test testPutIfAbsent average time per run: 115.756501 ns
Test testComputeIfAbsentLamda average time per run: 276.9667055 ns
Test testPutIfAbsent average time per run: 134.2332435 ns
Test testComputeIfAbsentLamda average time per run: 223.222063625 ns
Test testPutIfAbsent average time per run: 119.968893625 ns
Test testComputeIfAbsentLamda average time per run: 216.707419875 ns
Test testPutIfAbsent average time per run: 116.173902375 ns
Test testComputeIfAbsentLamda average time per run: 215.632467375 ns
Test testPutIfAbsent average time per run: 112.21422775 ns
Test testComputeIfAbsentLamda average time per run: 210.29563725 ns
Test testPutIfAbsent average time per run: 120.50643475 ns
Test testComputeIfAbsentLamda average time per run: 200.79536475 ns