У меня есть карта, которая должна связывать строки с идентификатором. Между идентификаторами не должно быть пробелов, и они должны быть уникальными целыми числами от 0 до N.

В запросе всегда есть две строки, одна из которых, обе или никакая, возможно, уже были проиндексированы. Карта строится параллельно из пула ForkJoin, и в идеале я хотел бы избежать явных синхронизированных блоков. Я ищу оптимальный способ максимизировать пропускную способность с блокировкой или без нее.

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

public class Foo {
    private final Map<String, Integer> idGenerator = new ConcurrentHashMap<>();

    // invoked from multiple threads
    public void update(String key1, String key2) {
      idGenerator.dosomething(key, ?) // should save the key and unique id
      idGenerator.dosomething(key2, ?) // should save the key2 and its unique id
      Bar bar = new Bar(idGenerator.get(key), idGenerator.get(key2));
      // ... do something with bar
   }
}

Я думаю, что метод size() в сочетании с merge() может решить проблему, но я не могу убедить себя в этом. Может ли кто-нибудь предложить подход к этой проблеме?

< Сильный > ИЗМЕНИТЬ

Что касается флага дублирования, это не может быть решено с помощью AtomicInteger.incrementAndGet(), как предлагается в связанном ответе. Если бы я сделал это вслепую для каждой строки, в последовательностях были бы пробелы . Необходима составная операция, которая проверяет, существует ли ключ, и только после этого генерирует идентификатор. Я искал способ реализовать такую составную операцию через API Map.

Второй предоставленный ответ противоречит требованиям, которые я конкретно изложил в вопросе.

2
John 22 Сен 2018 в 11:08

2 ответа

Лучший ответ

Невозможно сделать это так, как вы этого хотите - ConcurrentHashMap сам по себе не блокируется. Однако вы можете сделать это атомарно, не выполняя явного управления блокировками, используя функция java.util.Map.computeIfAbsent.

Вот пример кода в стиле того, что вы предоставили, который должен вас подтолкнуть.

ConcurrentHashMap<String, Integer> keyMap = new ConcurrentHashMap<>();
AtomicInteger sequence = new AtomicInteger();

public void update(String key1, String key2) {
    Integer id1 = keyMap.computeIfAbsent(key1, s -> sequence.getAndIncrement());
    Integer id2 = keyMap.computeIfAbsent(key2, s -> sequence.getAndIncrement());

    Bar bar = new Bar(id1, id2);
    // ... do something with bar
}
4
lscoughlin 22 Сен 2018 в 09:04

Я не уверен, что вы сможете делать именно то, что хотите. Тем не менее, вы можете выполнить пакетные обновления или выполнить проверку отдельно от перечисления / добавления.

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

задержка не так уж и важна. Это приложение должно обрабатывать большой объем данных и в конечном итоге производить вывод. В большинстве случаев на карте должно быть поисковое совпадение.

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

Одного потока записи может быть достаточно.

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

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

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


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

Перечисляющий поток читает со всех из них.

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


своего рода структура данных чтение-копирование-обновление (RCU) может быть хорошо, если это есть в Java . Это позволит читателям продолжать отфильтровывать дубликаты на полной скорости, в то время как поток перечисления создает новую таблицу с выполненной партией вставок с нулевой конкуренцией при построении новой таблицы.


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

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

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

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

3
Peter Cordes 22 Сен 2018 в 10:00