Использование ConcurrentHashMap, когда требуется синхронизация?

У меня есть ConcurrentHashMap, где я делаю следующее:

sequences = new ConcurrentHashMap<Class<?>, AtomicLong>();

if(!sequences.containsKey(table)) {
    synchronized (sequences) {
        if(!sequences.containsKey(table))
            initializeHashMapKeyValue(table);
    }
}

Мой вопрос - нет необходимости делать дополнительные

if(!sequences.containsKey(table))

Проверьте внутри синхронизированного блока, чтобы другие потоки не инициализировали одно и то же значение hashmap?

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

Ответ 1

Операции

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

Ответ 2

Вы должны использовать putIfAbsent методы ConcurrentMap.

ConcurrentMap<String, AtomicLong> map = new ConcurrentHashMap<String, AtomicLong> ();

public long addTo(String key, long value) {
  // The final value it became.
  long result = value;
  // Make a new one to put in the map.
  AtomicLong newValue = new AtomicLong(value);
  // Insert my new one or get me the old one.
  AtomicLong oldValue = map.putIfAbsent(key, newValue);
  // Was it already there? Note the deliberate use of '!='.
  if ( oldValue != newValue ) {
    // Update it.
    result = oldValue.addAndGet(value);
  }
  return result;
}

Для функциональных пуристов среди нас вышеуказанное может быть упрощено (или, возможно, комплексифицировано) до:

public long addTo(String key, long value) {
    return map.putIfAbsent(key, new AtomicLong()).addAndGet(value);
}

И в Java 8 мы можем избежать ненужного создания AtomicLong:

public long addTo8(String key, long value) {
    return map.computeIfAbsent(key, k -> new AtomicLong()).addAndGet(value);
}

Ответ 3

Вы не можете получить эксклюзивный замок с ConcurrentHashMap. В таком случае вам лучше использовать Synchronized HashMap.

Внутри ConcurrentHashMap уже существует атомный метод, если объект еще не существует; putIfAbsent

Ответ 4

Я вижу, что вы там делали;-) Вопрос: вы сами видите?

Прежде всего, вы использовали что-то под названием "Double checked lock pattern". Если у вас есть быстрый путь (первый содержит), который не нуждается в синхронизации, если он выполняется, и медленный путь, который необходимо синхронизировать, потому что вы выполняете сложную операцию. Ваша операция состоит в проверке, находится ли что-то внутри карты, а затем помещено там что-то/инициализируется. Поэтому не имеет значения, что ConcurrentHashMap является потокобезопасным для одиночной операции, потому что вы выполняете две простые операции, которые нужно рассматривать как единицу, поэтому да, этот синхронизированный блок правильный, и на самом деле он может быть синхронизирован любым другим, например, this.

Ответ 5

В Java 8 вы можете заменить блокировку с двойным флажком на .computeIfAbsent:

sequences.computeIfAbsent(table, k -> initializeHashMapKeyValue(k));

Ответ 6

Создайте файл с именем dictionary.txt со следующим содержимым:

a
as
an
b
bat
ball

Здесь мы имеем: Количество слов, начинающихся с "a": 3

Количество слов, начинающихся с "b": 3

Общее количество слов: 6

Теперь выполните следующую программу как: java WordCount test_dictionary.txt 10

public class WordCount {
String fileName;

public WordCount(String fileName) {
    this.fileName = fileName;
}

public void process() throws Exception {
    long start = Instant.now().toEpochMilli();

    LongAdder totalWords = new LongAdder();
    //Map<Character, LongAdder> wordCounts = Collections.synchronizedMap(new HashMap<Character, LongAdder>());
    ConcurrentHashMap<Character, LongAdder> wordCounts = new ConcurrentHashMap<Character, LongAdder>();

    Files.readAllLines(Paths.get(fileName))
        .parallelStream()
        .map(line -> line.split("\\s+"))
        .flatMap(Arrays::stream)
        .parallel()
        .map(String::toLowerCase)
        .forEach(word -> {
            totalWords.increment();
            char c = word.charAt(0);
            if (!wordCounts.containsKey(c)) {
                wordCounts.put(c, new LongAdder());
            }
            wordCounts.get(c).increment();
        });
    System.out.println(wordCounts);
    System.out.println("Total word count: " + totalWords);

    long end = Instant.now().toEpochMilli();
    System.out.println(String.format("Completed in %d milliseconds", (end - start)));
}

public static void main(String[] args) throws Exception {
    for (int r = 0; r < Integer.parseInt(args[1]); r++) {
        new WordCount(args[0]).process();
    }
}

}

Вы увидите, что подсчеты меняются, как показано ниже:

{a = 2, b = 3}

Общее количество слов: 6

Завершено за 77 миллисекунд

{a = 3, b = 3}

Общее количество слов: 6

Теперь закомментируйте ConcurrentHashMap в строке 13, раскомментируйте строку над ней и запустите программу еще раз.

Вы увидите детерминированные подсчеты.