Синхронізований блок Java проти Collections.synchronizedMap


85

Чи встановлений наступний код для коректної синхронізації дзвінків 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()оскільки інший потік не може ввести синхронізований блок addToMap()до remove()повернення, оскільки я спочатку створив карту з Collections.synchronizedMap(). Це правильно? Чи є кращий спосіб це зробити?

Відповіді:


90

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

Запуск двох (або більше) операцій на карті, однак, повинен бути синхронізований у блоці. Так що так - ви синхронізуєтесь правильно.


26
Думаю, було б непогано згадати, що це працює, тому що javadocs явно вказує, що синхронізованаMap синхронізується на самій карті, а не якийсь внутрішній замок. Якби це було так, синхронізований (synchronizedMap) був би неправильним.
екстранеон

2
@Yuval не могли б ви пояснити свою відповідь трохи глибше? Ви кажете, що sychronizedMap виконує операції атомарно, але чому тоді вам коли-небудь знадобиться ваш власний синхронізований блок, якщо syncMap робить усі ваші операції атомарними? Здається, ваш перший абзац не дозволяє турбуватися про другий.
almel

@almel див. мою відповідь
Сергій

2
чому потрібно мати синхронізований блок, оскільки карта вже використовує Collections.synchronizedMap()? Я не отримую другого балу.
Bimal Sharma

15

Якщо ви використовуєте JDK 6, тоді ви можете перевірити ConcurrentHashMap

Зверніть увагу на метод putIfAbsent у цьому класі.


9
Насправді це JDK 1,5
Богдан,

13

Існує потенціал для тонкої помилки в коді.

[ ОНОВЛЕННЯ: Оскільки він використовує map.remove (), цей опис не є повністю дійсним. Я пропустив цей факт вперше. :( Дякую автора на питання для вказуючи на це. Я їду інше як є, але змінив свинцеве заяву сказати , що є потенційно помилка.]

У doWork () ви отримуєте значення Списку з Карти безпечним для потоку способом. Однак згодом ви отримуєте доступ до цього списку з небезпечної точки зору. Наприклад, один потік може використовувати список у doWork (), тоді як інший потік викликає synchronizedMap.get (ключ) .add (значення) у 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
      }
    }
  }

Останній варіант трохи обмежить паралельність, але дещо зрозуміліший IMO.

Крім того, коротка примітка про ConcurrentHashMap. Це справді корисний клас, але не завжди є відповідною заміною синхронізованим HashMaps. Цитуючи його Javadocs,

Цей клас повністю сумісний з Hashtable у програмах, які покладаються на безпеку потоку, але не на деталі синхронізації .

Іншими словами, putIfAbsent () чудово підходить для атомних вставок, але не гарантує, що інші частини карти не зміняться під час цього дзвінка; це гарантує лише атомність. У вашій зразковій програмі ви покладаєтесь на деталі синхронізації (синхронізованої) HashMap для речей, відмінних від put () s.

Останнє. :) Ця чудова цитата з Java Concurrency на практиці завжди допомагає мені у розробці налагоджувальних багатопотокових програм.

Для кожної змінної змінної стану, до якої може отримати доступ більше одного потоку, всі звернення до цієї змінної повинні виконуватися з одним і тим же блокуванням.


Я бачу вашу думку щодо помилки, якщо я отримував доступ до списку за допомогою syncronizedMap.get (). Оскільки я використовую remove (), чи не слід наступне додавання з цим ключем створювати новий ArrayList і не заважати тому, який я використовую в doWork?
Ryan Ahearn

Правильно! Я повністю повів повз вашого видалення.
JLR

1
Для кожної змінної змінної стану, до якої може отримати доступ більше одного потоку, всі звернення до цієї змінної повинні виконуватися з тим самим блокуванням. ---- Я зазвичай додаю приватну власність, яка є лише новим Object (), і використовую це для моїх блоків синхронізації. Таким чином, я знаю все це через необроблений для цього контексту. синхронізовано (objectInVar) {}
AnthonyJClink

11

Так, ви синхронізуєтесь правильно. Поясню це детальніше. Ви повинні синхронізувати два або більше викликів методів на об’єкт 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об'єкт містить keyі NullPointerExceptionбуде кинуто, тому що synchronizedMap.get(key) повернеться null. Якщо послідовність викликів методу для synchronizedMapоб'єкта не залежить від результатів один одного, то вам не потрібно синхронізувати послідовність. Наприклад, вам не потрібно синхронізувати цю послідовність:

synchronizedMap.put(key1, valuesList1);
synchronizedMap.put(key2, valuesList2);

Ось

synchronizedMap.put(key2, valuesList2);

виклик методу не покладається на результати попереднього

synchronizedMap.put(key1, valuesList1);

виклику методу (йому байдуже, чи якийсь потік втручався між двома викликами методів і, наприклад, видалив key1).


4

Це здається мені правильним. Якби я хоч щось змінив, я б припинив використовувати Collections.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);

3
Що потрібно зробити. Мені незрозуміло, чому нам слід використовувати Collections.synchronizedXXX()API, коли нам все одно потрібно синхронізувати якийсь об’єкт (що в більшості випадків буде лише самою колекцією) у логіці нашого щоденного додатка
kellogs

3

Спосіб синхронізації правильний. Але є підводка

  1. Синхронізована обгортка, надана фреймворком Collection, гарантує, що виклики методу, тобто додавання / отримання / вміст, будуть виконуватися взаємовиключно.

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

  1. Ви могли використовувати одночасну реалізацію Map, доступну в рамках Frame. Перевага "ConcurrentHashMap" є

a. Він має API 'putIfAbsent', який робить те саме, але більш ефективно.

b. Його ефективність: dCocurrentMap просто блокує ключі, отже, не блокує весь світ карти. Де, як ви заблокували ключі, а також значення.

c. Ви могли передати посилання на об'єкт карти десь у вашій базі коду, де ви / інший розробник у вашому tean може закінчити використовувати його неправильно. Тобто він може просто все додати () або отримати (), не замикаючи об'єкт карти. Отже, його дзвінок не буде взаємовиключним для вашого блоку синхронізації. Але використання одночасної реалізації дає вам спокій, що її ніколи не можна використовувати / реалізовувати неправильно.


2

Перегляньте Google Collections ' Multimap, наприклад, сторінку 28 цієї презентації .

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.