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?
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 HashSet
s contenues dans le ConcurrentMap
.)
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.
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.
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.
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);
}
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());
}