web-dev-qa-db-fra.com

Java bloc synchronisé contre Collections.synchronizedMap

Le code suivant est-il configuré pour synchroniser correctement les appels sur 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);
      }
    }
  }
}

D'après ma compréhension, j'ai besoin du bloc synchronisé dans addToMap() pour empêcher un autre thread d'appeler remove() ou containsKey() avant de passer à l'appel de put() mais je n’ai pas besoin d’un bloc synchronisé dans doWork() car un autre thread ne peut pas entrer le bloc synchronisé dans addToMap() avant remove() retourne car j’ai créé la carte à l’origine avec Collections.synchronizedMap(). Est-ce exact? Y a-t-il une meilleure manière de faire cela?

81
Ryan Ahearn

Collections.synchronizedMap() garantit que chaque opération atomique que vous souhaitez exécuter sur la carte sera synchronisée.

L'exécution de deux opérations (ou plus) sur la carte doit cependant être synchronisée dans un bloc. Alors oui, vous synchronisez correctement.

87
Yuval Adam

Si vous utilisez JDK 6, vous pouvez vérifier ConcurrentHashMap

Notez la méthode putIfAbsent dans cette classe.

14
TofuBeer

Il y a le potentiel pour un bogue subtil dans votre code.

[PDATE: Puisqu'il utilise map.remove (), cette description n'est pas totalement valide. J'ai raté ce fait la première fois. :( Merci à l'auteur de la question pour l'avoir signalé. Je laisse le reste tel quel, mais j'ai modifié l'instruction principale pour indiquer qu'il y a potentiellement a punaise.]

Dans doWork (), vous obtenez la valeur de liste de la carte de manière sécurisée pour les threads. Par la suite, cependant, vous accédez à cette liste de manière peu sûre. Par exemple, un thread peut utiliser la liste dans doWork () alors qu'un autre thread appelle synchronizedMap.get (clé) .add (valeur) dans addToMap ( ). Ces deux accès ne sont pas synchronisés. En règle générale, les garanties de thread-safe d'une collection ne s'étendent pas aux clés ou aux valeurs qu'elles stockent.

Vous pouvez résoudre ce problème en insérant une liste synchronisée dans la carte, comme

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

Vous pouvez également synchroniser sur la carte tout en accédant à la liste dans doWork ():

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

La dernière option limitera un peu la concurrence, mais est un peu plus claire à l’OMI.

En outre, une note rapide sur ConcurrentHashMap. C'est une classe vraiment utile, mais ce n'est pas toujours un remplacement approprié pour HashMaps synchronisé. Citant ses Javadocs,

Cette classe est totalement interopérable avec Hashtable dans les programmes qui reposent sur la sécurité de son thread mais pas sur ses détails de synchronisation .

En d'autres termes, putIfAbsent () convient parfaitement aux insertions atomiques, mais ne garantit pas que d'autres parties de la carte ne changeront pas pendant cet appel. il ne garantit que l'atomicité. Dans votre exemple de programme, vous utilisez les détails de synchronisation de HashMap (synchronisé) pour des tâches autres que put () s.

Dernière chose. :) Cette excellente citation de Java Concurrency in Practice m'aide toujours dans la conception d'un programme de débogage de programmes multithreads.

Pour chaque variable d'état modifiable à laquelle plusieurs threads peuvent accéder, tous les accès à cette variable doivent être effectués avec le même verrou maintenu.

13
JLR

Oui, vous synchronisez correctement. Je vais expliquer cela plus en détail. Vous devez synchroniser deux ou plusieurs appels de méthode sur l'objet synchronizedMap uniquement si vous devez vous appuyer sur les résultats des appels de méthode précédents lors de l'appel de méthode suivant dans la séquence d'appels de méthode sur l'objet synchronizedMap. Jetons un coup d’œil à ce code:

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);
    }
}

Dans ce code

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

et

synchronizedMap.put(key, valuesList);

les appels de méthode sont basés sur le résultat de la précédente

synchronizedMap.containsKey(key)

appel de méthode.

Si la séquence d'appels de méthode n'a pas été synchronisée, le résultat peut être erroné. Par exemple, thread 1 Exécute la méthode addToMap() et thread 2 Exécute la méthode doWork() La séquence d'appels de méthode sur synchronizedMap objet peut être comme suit: Thread 1 a exécuté la méthode

synchronizedMap.containsKey(key)

et le résultat est "true". Une fois que ce système d’exploitation a basculé le contrôle d’exécution sur thread 2 Et qu’il a exécuté

synchronizedMap.remove(key)

Ensuite, le contrôle d’exécution est revenu sur thread 1 Et il s’est exécuté par exemple.

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

croire que l'objet synchronizedMap contient le key et que NullPointerException sera levé parce que synchronizedMap.get(key) retournera null. Si la séquence d'appels de méthode sur l'objet synchronizedMap ne dépend pas des résultats les uns des autres, vous n'avez pas besoin de synchroniser la séquence. Par exemple, vous n'avez pas besoin de synchroniser cette séquence:

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

Ici

synchronizedMap.put(key2, valuesList2);

appel de méthode ne repose pas sur les résultats de la précédente

synchronizedMap.put(key1, valuesList1);

appel de méthode (peu importe si un thread est intervenu entre les deux appels de méthode et a par exemple supprimé le key1).

10
Sergey

Cela me semble correct. Si je devais changer quoi que ce soit, je cesserais d'utiliser la méthode Collections.synchronizedMap () et de tout synchroniser de la même manière, juste pour que ce soit plus clair.

Aussi, je remplacerais

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

avec

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

Départ Google Collections 'Multimap, par exemple. page 28 de cette présentation .

Si vous ne pouvez pas utiliser cette bibliothèque pour une raison quelconque, envisagez d'utiliser ConcurrentHashMap au lieu de SynchronizedHashMap; il a une méthode astucieuse putIfAbsent(K,V) avec laquelle vous pouvez ajouter de manière atomique la liste des éléments si elle n’est pas déjà présente. Pensez également à utiliser CopyOnWriteArrayList pour les valeurs de la carte si vos habitudes d'utilisation le justifient.

2
Barend

La façon dont vous avez synchronisé est correcte. Mais il ya un hic

  1. Le wrapper synchronisé fourni par la structure Collection garantit que les appels de méthode I.e add/get/includes s'exécutent de manière mutuellement exclusive.

Cependant, dans le monde réel, vous interrogerez généralement la carte avant de saisir la valeur. Par conséquent, vous devez effectuer deux opérations et par conséquent un bloc synchronisé est nécessaire. Donc, la façon dont vous l'avez utilisé est correcte. Toutefois.

  1. Vous auriez pu utiliser une implémentation simultanée de Map disponible dans le cadre de la collection. L'avantage 'ConcurrentHashMap' est

une. Il possède une API 'putIfAbsent' qui ferait la même chose mais de manière plus efficace.

b. Son efficacité: dThe CocurrentMap ne verrouille que les clés, ce qui ne bloque pas le monde entier de la carte. Où avez-vous bloqué des clés et des valeurs?.

c. Vous auriez peut-être transmis la référence de votre objet cartographique ailleurs dans votre base de code où vous/un autre développeur de votre tean risque de ne pas l'utiliser correctement. C'est-à-dire qu'il peut simplement ajouter () ou obtenir () sans verrouiller l'objet de la carte. Par conséquent, son appel ne s'exécutera pas mutuellement de manière exclusive sur votre bloc de synchronisation. Mais l'utilisation d'une implémentation simultanée vous assure que vous ne pourrez jamais l'utiliser/l'implémenter de manière incorrecte.

2
Jai Pandit