Требуются ли параллельные классы, предоставляемые JDK, для использования собственной встроенной блокировки своего экземпляра для синхронизации?

#java #concurrency

#java #параллелизм

Вопрос:

JDK предоставляет набор потокобезопасных классов, таких как ConcurrentHashMap, Concurrentlink queue и AtomicInteger.

Требуется ли синхронизировать эти классы this для реализации их потокобезопасного поведения?

При условии, что они это сделают, мы можем реализовать наши собственные синхронизированные операции над этими объектами и смешать их со встроенными?

Другими словами, безопасно ли это делать:

 ConcurrentMap<Integer, Account> accounts 
    = new ConcurrentHashMap<Integer, Account>();
  

 // Add an account atomically
synchronized(accounts) {
    if (!accounts.containsKey(id)) {
        Account account = new Account();
        accounts.put(id, account);
    }
}
  

И в другом потоке

 // Access the object expecting it to synchronize(this){…} internally
accounts.get(id);
  

Обратите внимание, что простой синхронизированный блок, приведенный выше, вероятно, можно было бы заменить на putIfAbsent(), но я вижу другие случаи, когда синхронизация с объектом могла бы быть полезной.

Комментарии:

1. ConcurrentMap.putIfAbsent() это то, что вы ищете.

2. @jtahlborn Да, это был не очень хороший пример. На самом деле я уже упоминал putIfAbsent() в вопросе. Небольшое отличие заключается в том, что вам не нужно каждый раз подготавливать добавляемое значение, помещая его инициализацию внутри блока if.

3. если вам нужно выполнить дорогостоящую инициализацию объекта, одним из способов является использование метода инициализации второго этапа на вашем объекте (и удешевление конструкции). затем, если объект успешно добавлен в map, вызовите метод инициализации второго этапа (или вызовите метод инициализации по требованию).

Ответ №1:

Требуется ли этим классам синхронизировать это для реализации их потокобезопасного поведения.

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

В случае приведенного выше метода put обратите внимание на javadoc:

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

Это означает, что параметры потокобезопасны и нет способа сделать то, что вы пытаетесь сделать выше (заблокировать всю таблицу). Кроме того, для операций, которые вы используете (put и get), ни один из них не потребует такой блокировки.

Мне особенно нравится эта цитата из javadoc из метода values():

Итератор представления является «слабо согласованным» итератором, который никогда не будет вызывать ConcurrentModificationException и гарантирует прохождение элементов в том виде, в каком они существовали при создании итератора, и может (но не гарантировано) отражать любые изменения, последующие за созданием.

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