Синхронизированный блок Java против коллекций.synchronizedMap


следующий код настроен для правильной синхронизации вызовов на synchronizedMap?

public class MyClass {
  private static Map<String, List<String>> synchronizedMap = Collections.synchronizedMap(new HashMap<String, List<String>>());

  public void doWork(String key) {
    List<String> values = null;
    while ((values = synchronizedMap.remove(key)) != null) {
      //do something with values
    }
  }

  public static void addToMap(String key, String value) {
    synchronized (synchronizedMap) {
      if (synchronizedMap.containsKey(key)) {
        synchronizedMap.get(key).add(value);
      }
      else {
        List<String> valuesList = new ArrayList<String>();
        valuesList.add(value);
        synchronizedMap.put(key, valuesList);
      }
    }
  }
}

насколько я понимаю, мне нужен синхронизированный блок в addToMap() чтобы предотвратить вызов другого потока remove() или containsKey() прежде чем я закончу вызов put() но мне не нужен синхронизированный блок в doWork() потому что другой поток не может войти в блок synchronized в addToMap() до remove() возвращает, потому что я создал карту изначально с Collections.synchronizedMap(). Заключаться в том правильно? Есть ли лучший способ сделать это?

7 81

7 ответов:

Collections.synchronizedMap() гарантирует, что каждая атомарная операция, которую вы хотите запустить на карте, будет синхронизирована.

выполнение двух (или более) операций на карте, однако, должно быть синхронизировано в блоке. Так что да-вы синхронизируетесь правильно.

Если вы используете JDK 6, то вы можете проверить ConcurrentHashMap

обратите внимание на метод putIfAbsent в этом классе.

есть потенциал для тонкой ошибки в коде.

[обновление: так как он использует карту.удалить () это описание не совсем корректно. Я пропустил этот факт в первый раз. : (Спасибо автору вопроса за указание на это. Я оставляю все как есть, но изменил ведущий оператор, чтобы сказать, что есть потенциально ошибка.]

на doWork () вы получаете значение списка с карты в a потокобезопасный способ. После этого, однако, вы получаете доступ к этому списку в небезопасном вопросе. Например, один поток может использовать список doWork () в то время как другой поток вызывает synchronizedMap.получить (ключ).добавить(значение) на addToMap(). Эти два доступа не синхронизированы. Эмпирическое правило заключается в том, что потокобезопасные гарантии коллекции не распространяются на ключи или значения, которые они хранят.

вы можете исправить это, вставив синхронизированный список в карту, как

List<String> valuesList = new ArrayList<String>();
valuesList.add(value);
synchronizedMap.put(key, Collections.synchronizedList(valuesList)); // sync'd list

в качестве альтернативы вы можете синхронизировать на карте при доступе к списку в doWork ():

  public void doWork(String key) {
    List<String> values = null;
    while ((values = synchronizedMap.remove(key)) != null) {
      synchronized (synchronizedMap) {
          //do something with values
      }
    }
  }

последний вариант немного ограничит параллелизм, но несколько яснее ИМО.

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

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

последнее дело. :) Это здорово цитата из параллелизм Java на практике всегда помогает мне в разработке отладке многопоточных программ.

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

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

synchronized (synchronizedMap) {
    if (synchronizedMap.containsKey(key)) {
        synchronizedMap.get(key).add(value);
    }
    else {
        List<String> valuesList = new ArrayList<String>();
        valuesList.add(value);
        synchronizedMap.put(key, valuesList);
    }
}

в этом коде

synchronizedMap.get(key).add(value);

и

synchronizedMap.put(key, valuesList);

вызовы методов полагаются на результат из предыдущих

synchronizedMap.containsKey(key)

вызов метода.

если последовательность вызовов метода не была синхронизирована, результат может быть неправильным. Например thread 1 выполняется методом addToMap() и thread 2 выполняется методом doWork() Последовательность вызовов метода на synchronizedMap объект может быть следующим: Thread 1 выполнил метод

synchronizedMap.containsKey(key)

и в результате "true". После этого операционная система переключила управление исполнением на thread 2 и он выполнил

synchronizedMap.remove(key)

после этого контроль выполнения был переключен обратно на thread 1 и он выполнил например

synchronizedMap.get(key).add(value);

верить

это выглядит правильно для меня. Если бы я что-то изменил, я бы перестал использовать коллекции.synchronizedMap () и синхронизировать все таким же образом, просто чтобы сделать его более ясным.

кроме того, я бы заменил

  if (synchronizedMap.containsKey(key)) {
    synchronizedMap.get(key).add(value);
  }
  else {
    List<String> valuesList = new ArrayList<String>();
    valuesList.add(value);
    synchronizedMap.put(key, valuesList);
  }

С

List<String> valuesList = synchronziedMap.get(key);
if (valuesList == null)
{
  valuesList = new ArrayList<String>();
  synchronziedMap.put(key, valuesList);
}
valuesList.add(value);

проверить Коллекции Google'Multimap, например страница 28 из презентации.

если вы не можете использовать эту библиотеку по какой-то причине, рассмотрите возможность использования ConcurrentHashMap вместо SynchronizedHashMap; Он имеет отличный putIfAbsent(K,V) метод, с помощью которого вы можете автоматически добавить элемент списка, если он уже не там. Кроме того, рассмотрите возможность использования CopyOnWriteArrayList для значений карты, если ваши шаблоны использования гарантируют это.

способ синхронизации является правильным. Но есть подвох

  1. синхронизированная оболочка, предоставляемая Collection framework, гарантирует, что вызовы метода т. е. add/get/contains будут выполняться взаимоисключающими.

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

  1. вы могли бы использовать параллельную реализацию Map, доступную в Collection framework. "ConcurrentHashMap" преимущество

a. он имеет API 'putIfAbsent', который будет делать то же самое, но более эффективным способом.

b. его эффективность: Dthe CocurrentMap просто блокирует ключи, следовательно, он не блокирует весь мир карты. Где, как вы заблокировали ключи и значения.

c. вы могли бы передать ссылку ваш объект карты где-то еще в вашей кодовой базе, где вы/другой разработчик в вашем tean может в конечном итоге использовать его неправильно. Т. е. он может просто все добавить () или получить () без блокировки объекта карты. Следовательно, его вызов не будет работать взаимоисключающим для вашего блока синхронизации. Но использование параллельной реализации дает вам спокойствие, что это никогда не может быть использован/реализован неправильно.