web-dev-qa-db-fra.com

ConcourshashMap: Évitez la création d'objet supplémentaire avec "putifabsent"?

Je regagne plusieurs valeurs pour les clés dans un environnement multi-fileté. Les clés ne sont pas connues à l'avance. Je pensais faire quelque chose comme ça:

class Aggregator {
    protected ConcurrentHashMap<String, List<String>> entries =
                            new ConcurrentHashMap<String, List<String>>();
    public Aggregator() {}

    public void record(String key, String value) {
        List<String> newList =
                    Collections.synchronizedList(new ArrayList<String>());
        List<String> existingList = entries.putIfAbsent(key, newList);
        List<String> values = existingList == null ? newList : existingList;
        values.add(value);
    }
}

Le problème que je vois, c'est que chaque fois que cette méthode fonctionne, je dois créer une nouvelle instance d'un ArrayList, que je jette ensuite (dans la plupart des cas). Cela semble être un abus injustifié du collecteur des ordures. Existe-t-il un moyen meilleur et fil-propre d'initialiser ce type de structure sans avoir à synchronize la méthode record? Je suis un peu surpris par la décision d'avoir la méthode putIfAbsent non renvoyer l'élément nouvellement créé et par l'absence de moyen de différer l'instanciation à moins que cela ne soit appelé (pour ainsi dire).

38

Java 8 a introduit une API pour répondre à ce problème exact, faisant une solution 1 ligne:

public void record(String key, String value) {
    entries.computeIfAbsent(key, k -> Collections.synchronizedList(new ArrayList<String>())).add(value);
}

Pour Java 7:

public void record(String key, String value) {
    List<String> values = entries.get(key);
    if (values == null) {
        entries.putIfAbsent(key, Collections.synchronizedList(new ArrayList<String>()));
        // At this point, there will definitely be a list for the key.
        // We don't know or care which thread's new object is in there, so:
        values = entries.get(key);
    }
    values.add(value);
}

Ceci est le modèle de code standard lors de la collecte d'un ConcurrentHashMap.

Le procédé spécial putIfAbsent(K, V)) Mettez votre objet de valeur dans ou si un autre thread a été précédé de vous, il ignorera votre objet de valeur. De toute façon, après l'appel à putIfAbsent(K, V)), get(key) est garantie d'être cohérente entre les threads et donc le code ci-dessus est threadsafe.

Le seul surcharge gaspillé est si un autre thread ajoute une nouvelle entrée en même temps pour la même clé: vous peut finir par jeter la valeur nouvellement créée. , mais cela ne se produit que s'il n'y a pas déjà d'entrée et il y a une course que votre fil perd, ce qui serait généralement rare.

41
Bohemian

À partir de Java-8, vous pouvez créer des cartes multiples en utilisant le motif suivant:

public void record(String key, String value) { entries.computeIfAbsent(key, k -> Collections.synchronizedList(new ArrayList<String>())) .add(value); }

La documentation ConcurrenthashMap (pas le contrat général) spécifie que la arrachelist ne sera créée qu'une seule fois pour chaque touche, lors du léger coût initial de retarder les mises à jour, tandis que le tableau est créé pour une nouvelle clé:

http://docs.oracle.com/javase/8/docs/api/java/util/concurrent/concurrenthashashmap.html#computeifabsent-k-java.util.function.function-

15
Peter

En fin de compte, j'ai mis en place une légère modification de la réponse de @ Bohemian. Sa solution proposée écrase la variable values avec l'appel putIfAbsent, ce qui crée le même problème que j'avais auparavant. Le code qui semble fonctionner ressemble à ceci:

    public void record(String key, String value) {
        List<String> values = entries.get(key);
        if (values == null) {
            values = Collections.synchronizedList(new ArrayList<String>());
            List<String> values2 = entries.putIfAbsent(key, values);
            if (values2 != null)
                values = values2;
        }
        values.add(value);
    }

Ce n'est pas aussi élégant que je voudrais, mais c'est mieux que l'original qui crée une nouvelle instance ArrayList à chaque appel.

11

Créé deux versions basées sur la réponse de Gene

public  static <K,V> void putIfAbsetMultiValue(ConcurrentHashMap<K,List<V>> entries, K key, V value) {
    List<V> values = entries.get(key);
    if (values == null) {
        values = Collections.synchronizedList(new ArrayList<V>());
        List<V> values2 = entries.putIfAbsent(key, values);
        if (values2 != null)
            values = values2;
    }
    values.add(value);
}

public  static <K,V> void putIfAbsetMultiValueSet(ConcurrentMap<K,Set<V>> entries, K key, V value) {
    Set<V> values = entries.get(key);
    if (values == null) {
        values = Collections.synchronizedSet(new HashSet<V>());
        Set<V> values2 = entries.putIfAbsent(key, values);
        if (values2 != null)
            values = values2;
    }
    values.add(value);
}

Ça marche bien

3
fracca

C'est un problème que j'ai aussi recherché une réponse. La méthode putIfAbsent ne résout pas réellement le problème de création d'objet supplémentaire, il veille à ce que l'un de ces objets ne remplace pas une autre. Mais les conditions de course entre les threads peuvent provoquer une instanciation de plusieurs objets. Je pourrais trouver 3 solutions pour ce problème (et je suivrais cet ordre de préférence):

1- Si vous êtes sur Java 8, la meilleure façon d'y parvenir est probablement la nouvelle méthode computeIfAbsentConcurrentMap. Vous avez juste besoin de lui donner un Fonction de calcul qui sera exécutée de manière synchrone (au moins pour la mise en œuvre ConcurrentHashMap). Exemple:

private final ConcurrentMap<String, List<String>> entries =
        new ConcurrentHashMap<String, List<String>>();

public void method1(String key, String value) {
    entries.computeIfAbsent(key, s -> new ArrayList<String>())
            .add(value);
}

Cela vient de la Javadoc de ConcurrentHashMap.computeIfAbsent:

Si la clé spécifiée n'est pas déjà associée à une valeur, tente de calculer sa valeur à l'aide de la fonction de mappage donné et l'entre dans cette carte à moins que NULL. L'invocation de la méthode complète est effectuée atomiquement, la fonction est donc appliquée au plus une fois par clé. Certaines tentatives de mise à jour des opérations de mise à jour sur cette carte par d'autres threads peuvent être bloquées pendant que le calcul est en cours. Le calcul doit donc être court et simple, et ne doit pas tenter de mettre à jour d'autres mappages de cette carte.

2- Si vous ne pouvez pas utiliser Java 8, vous pouvez utiliser Guava 's _' s LoadingCache, qui est en sécurité. Vous définissez une fonction de charge (juste comme La fonction compute ci-dessus) et vous pouvez être sûr que ce sera appelé synchroneusement. Exemple:

private final LoadingCache<String, List<String>> entries = CacheBuilder.newBuilder()
        .build(new CacheLoader<String, List<String>>() {
            @Override
            public List<String> load(String s) throws Exception {
                return new ArrayList<String>();
            }
        });

public void method2(String key, String value) {
    entries.getUnchecked(key).add(value);
}

3- Si vous ne pouvez pas utiliser de goyava non plus, vous pouvez toujours synchroniser manuellement et effectuer un verrouillage à double vérification. Exemple:

private final ConcurrentMap<String, List<String>> entries =
        new ConcurrentHashMap<String, List<String>>();

public void method3(String key, String value) {
    List<String> existing = entries.get(key);
    if (existing != null) {
        existing.add(value);
    } else {
        synchronized (entries) {
            List<String> existingSynchronized = entries.get(key);
            if (existingSynchronized != null) {
                existingSynchronized.add(value);
            } else {
                List<String> newList = new ArrayList<>();
                newList.add(value);
                entries.put(key, newList);
            }
        }
    }
}

J'ai fait un exemple de mise en œuvre de toutes ces 3 méthodes et de plus, la méthode non synchronisée, qui provoque une création d'objet supplémentaire: http://pastebin.com/qz4dujtr

3
Utku Özdemir

L'approche avec putIfAbsent a le délai d'exécution le plus rapide, il est de 2 à 50 fois plus rapide que l'approche "Lambda" dans les evironnements à haute conflit. La Lambda n'est pas la raison de ce "PowerLoss", la question est la synchronisation obligatoire à l'intérieur de ComputeIfabsent avant les optimisations Java-9.

la référence:

import Java.util.Random;
import Java.util.concurrent.ConcurrentHashMap;
import Java.util.concurrent.ExecutorService;
import Java.util.concurrent.Executors;
import Java.util.concurrent.TimeUnit;
import Java.util.concurrent.atomic.AtomicInteger;
import Java.util.concurrent.atomic.AtomicLong;

public class ConcurrentHashMapTest {
    private final static int numberOfRuns = 1000000;
    private final static int numberOfThreads = Runtime.getRuntime().availableProcessors();
    private final static int keysSize = 10;
    private final static String[] strings = new String[keysSize];
    static {
        for (int n = 0; n < keysSize; n++) {
            strings[n] = "" + (char) ('A' + n);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        for (int n = 0; n < 20; n++) {
            testPutIfAbsent();
            testComputeIfAbsentLamda();
        }
    }

    private static void testPutIfAbsent() throws InterruptedException {
        final AtomicLong totalTime = new AtomicLong();
        final ConcurrentHashMap<String, AtomicInteger> map = new ConcurrentHashMap<String, AtomicInteger>();
        final Random random = new Random();
        ExecutorService executorService = Executors.newFixedThreadPool(numberOfThreads);

        for (int i = 0; i < numberOfThreads; i++) {
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    long start, end;
                    for (int n = 0; n < numberOfRuns; n++) {
                        String s = strings[random.nextInt(strings.length)];
                        start = System.nanoTime();

                        AtomicInteger count = map.get(s);
                        if (count == null) {
                            count = new AtomicInteger(0);
                            AtomicInteger prevCount = map.putIfAbsent(s, count);
                            if (prevCount != null) {
                                count = prevCount;
                            }
                        }
                        count.incrementAndGet();
                        end = System.nanoTime();
                        totalTime.addAndGet(end - start);
                    }
                }
            });
        }
        executorService.shutdown();
        executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.DAYS);
        System.out.println("Test " + Thread.currentThread().getStackTrace()[1].getMethodName()
                + " average time per run: " + (double) totalTime.get() / numberOfThreads / numberOfRuns + " ns");
    }

    private static void testComputeIfAbsentLamda() throws InterruptedException {
        final AtomicLong totalTime = new AtomicLong();
        final ConcurrentHashMap<String, AtomicInteger> map = new ConcurrentHashMap<String, AtomicInteger>();
        final Random random = new Random();
        ExecutorService executorService = Executors.newFixedThreadPool(numberOfThreads);
        for (int i = 0; i < numberOfThreads; i++) {
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    long start, end;
                    for (int n = 0; n < numberOfRuns; n++) {
                        String s = strings[random.nextInt(strings.length)];
                        start = System.nanoTime();

                        AtomicInteger count = map.computeIfAbsent(s, (k) -> new AtomicInteger(0));
                        count.incrementAndGet();

                        end = System.nanoTime();
                        totalTime.addAndGet(end - start);
                    }
                }
            });
        }
        executorService.shutdown();
        executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.DAYS);
        System.out.println("Test " + Thread.currentThread().getStackTrace()[1].getMethodName()
                + " average time per run: " + (double) totalTime.get() / numberOfThreads / numberOfRuns + " ns");
    }

}

Les résultats:

Test testPutIfAbsent average time per run: 115.756501 ns
Test testComputeIfAbsentLamda average time per run: 276.9667055 ns
Test testPutIfAbsent average time per run: 134.2332435 ns
Test testComputeIfAbsentLamda average time per run: 223.222063625 ns
Test testPutIfAbsent average time per run: 119.968893625 ns
Test testComputeIfAbsentLamda average time per run: 216.707419875 ns
Test testPutIfAbsent average time per run: 116.173902375 ns
Test testComputeIfAbsentLamda average time per run: 215.632467375 ns
Test testPutIfAbsent average time per run: 112.21422775 ns
Test testComputeIfAbsentLamda average time per run: 210.29563725 ns
Test testPutIfAbsent average time per run: 120.50643475 ns
Test testComputeIfAbsentLamda average time per run: 200.79536475 ns
1
Krzysztof Cichocki

Gaspillage de mémoire (également GC, etc.) que le problème de création de liste de réseau vide est traité avec Java 1.7.40. Ne vous inquiétez pas de créer une arrayliste vide. Référence: http://javarevisited.blogspot.com.tr/2014/07/java-optimization-empty-arraylist-and-hashmap-cost-less-Memory-jdk-17040-update.html =

1
Erdinç Taşkın