web-dev-qa-db-fra.com

Si vous vérifiez si la carte contient Key avant d'utiliser putIfAbsent de ConcurrentMap

J'ai utilisé ConcurrentMap de Java pour une carte qui peut être utilisée à partir de plusieurs threads. Le putIfAbsent est une excellente méthode et est beaucoup plus facile à lire/écrire que d'utiliser des opérations de carte standard. J'ai du code qui ressemble à ceci:

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

// ...

map.putIfAbsent(name, new HashSet<X>());
map.get(name).add(Y);

En ce qui concerne la lisibilité, c'est très bien, mais cela nécessite de créer un nouveau HashSet à chaque fois, même s'il est déjà dans la carte. Je pourrais écrire ceci:

if (!map.containsKey(name)) {
    map.putIfAbsent(name, new HashSet<X>());
}
map.get(name).add(Y);

Avec ce changement, il perd un peu de lisibilité mais n'a pas besoin de créer le HashSet à chaque fois. Quel est le meilleur dans ce cas? J'ai tendance à me ranger du premier car il est plus lisible. Le second fonctionnerait mieux et pourrait être plus correct. Peut-être y a-t-il une meilleure façon de faire cela que l'un ou l'autre.

Quelle est la meilleure pratique pour utiliser un putIfAbsent de cette manière?

69
Chris Dail

La concurrence est difficile. Si vous allez vous embêter avec des cartes simultanées au lieu d'un verrouillage simple, vous pourriez aussi bien y aller. En effet, ne faites pas de recherches plus que nécessaire.

Set<X> set = map.get(name);
if (set == null) {
    final Set<X> value = new HashSet<X>();
    set = map.putIfAbsent(name, value);
    if (set == null) {
        set = value;
    }
}

(Avertissement de stackoverflow habituel: sur le dessus de ma tête. Non testé. Non compilé. Etc.)

pdate: 1.8 a ajouté computeIfAbsent méthode par défaut à ConcurrentMap (et Map ce qui est plutôt intéressant car cette implémentation serait incorrecte pour ConcurrentMap). (Et 1.7 a ajouté "l'opérateur diamant" <>.)

Set<X> set = map.computeIfAbsent(name, n -> new HashSet<>());

(Remarque, vous êtes responsable de la sécurité des threads de toutes les opérations des HashSets contenues dans le ConcurrentMap.)

105

La réponse de Tom est correcte en ce qui concerne l'utilisation de l'API pour ConcurrentMap. Une alternative qui évite d'utiliser putIfAbsent est d'utiliser la carte informatique de GoogleCollections/Guava MapMaker qui remplit automatiquement les valeurs avec une fonction fournie et gère toute la sécurité des threads pour vous. En fait, il ne crée qu'une seule valeur par clé et si la fonction de création est coûteuse, les autres threads demandant d'obtenir la même clé se bloqueront jusqu'à ce que la valeur soit disponible.

Edit de Guava 11, MapMaker est obsolète et est remplacé par le truc Cache/LocalCache/CacheBuilder. C'est un peu plus compliqué dans son utilisation mais fondamentalement isomorphe.

16
Jed Wesley-Smith

Vous pouvez utiliser MutableMap.getIfAbsentPut(K, Function0<? extends V>) à partir de Collections Eclipse (anciennement Collections GS ).

L'avantage par rapport à l'appel de get(), en effectuant une vérification nulle, puis en appelant putIfAbsent() est que nous ne calculons le hashCode de la clé qu'une seule fois et ne trouvons le bon endroit dans la table de hachage qu'une seule fois. Dans ConcurrentMaps comme org.Eclipse.collections.impl.map.mutable.ConcurrentHashMap, L'implémentation de getIfAbsentPut() est également thread-safe et atomique.

import org.Eclipse.collections.impl.map.mutable.ConcurrentHashMap;
...
ConcurrentHashMap<String, MyObject> map = new ConcurrentHashMap<>();
map.getIfAbsentPut("key", () -> someExpensiveComputation());

L'implémentation de org.Eclipse.collections.impl.map.mutable.ConcurrentHashMap Est vraiment non bloquante. Bien que tous les efforts soient faits pour ne pas appeler la fonction d'usine inutilement, il y a toujours une chance qu'elle soit appelée plus d'une fois pendant la contention.

Ce fait le distingue de Java 8's ConcurrentHashMap.computeIfAbsent(K, Function<? super K,? extends V>) . Le Javadoc pour cette méthode indique:

L'invocation de la méthode entière est effectuée de manière atomique, de sorte que la fonction est appliquée au plus une fois par clé. Certaines tentatives de mise à jour sur cette carte par d'autres threads peuvent être bloquées pendant le calcul, le calcul doit donc être court et simple ...

Remarque: je suis un committer pour les collections Eclipse.

5
Craig P. Motlin

En conservant une valeur pré-initialisée pour chaque thread, vous pouvez améliorer la réponse acceptée:

Set<X> initial = new HashSet<X>();
...
Set<X> set = map.putIfAbsent(name, initial);
if (set == null) {
    set = initial;
    initial = new HashSet<X>();
}
set.add(Y);

J'ai récemment utilisé cela avec des valeurs de carte AtomicInteger plutôt qu'avec Set.

3
karmakaze

Dans 5+ ans, je ne peux pas croire que personne n'ait mentionné ou publié une solution qui utilise ThreadLocal pour résoudre ce problème; et plusieurs des solutions sur cette page ne sont pas threadsafe et sont simplement bâclées.

L'utilisation de ThreadLocals pour ce problème spécifique n'est pas seulement considérée les meilleures pratiques pour la concurrence, mais pour minimiser la création de déchets/d'objets pendant conflit de thread. En outre, c'est un code incroyablement propre.

Par exemple:

private final ThreadLocal<HashSet<X>> 
  threadCache = new ThreadLocal<HashSet<X>>() {
      @Override
      protected
      HashSet<X> initialValue() {
          return new HashSet<X>();
      }
  };


private final ConcurrentMap<String, Set<X>> 
  map = new ConcurrentHashMap<String, Set<X>>();

Et la logique réelle ...

// minimize object creation during thread contention
final Set<X> cached = threadCache.get();

Set<X> data = map.putIfAbsent("foo", cached);
if (data == null) {
    // reset the cached value in the ThreadLocal
    listCache.set(new HashSet<X>());
    data = cached;
}

// make sure that the access to the set is thread safe
synchronized(data) {
    data.add(object);
}
2
Nathan

Mon approximation générique:

public class ConcurrentHashMapWithInit<K, V> extends ConcurrentHashMap<K, V> {
  private static final long serialVersionUID = 42L;

  public V initIfAbsent(final K key) {
    V value = get(key);
    if (value == null) {
      value = initialValue();
      final V x = putIfAbsent(key, value);
      value = (x != null) ? x : value;
    }
    return value;
  }

  protected V initialValue() {
    return null;
  }
}

Et comme exemple d'utilisation:

public static void main(final String[] args) throws Throwable {
  ConcurrentHashMapWithInit<String, HashSet<String>> map = 
        new ConcurrentHashMapWithInit<String, HashSet<String>>() {
    private static final long serialVersionUID = 42L;

    @Override
    protected HashSet<String> initialValue() {
      return new HashSet<String>();
    }
  };
  map.initIfAbsent("s1").add("chao");
  map.initIfAbsent("s2").add("bye");
  System.out.println(map.toString());
}
0
ggrandes