web-dev-qa-db-fra.com

Différents types d'ensembles thread-safe en Java

Il semble y avoir beaucoup d’implémentations et de façons différentes de générer des ensembles thread-safe en Java .

1) CopyOnWriteArraySet

2) Collections.synchronizedSet (Set set)

3) ConcurrentSkipListSet

4) Collections.newSetFromMap (nouveau ConcurrentHashMap ())

5) Autres ensembles générés de manière similaire à (4) 

Ces exemples proviennent de Modèle de concurrence: implémentations de jeux simultanés en Java 6

Quelqu'un pourrait-il simplement expliquer les différences, les avantages et les inconvénients de ces exemples et d’autres? J'ai de la difficulté à comprendre et à garder le fil droit des documents Java Std.

113
Ben

1) La CopyOnWriteArraySet est une implémentation assez simple - elle contient en gros une liste d’éléments dans un tableau, et lorsqu’elle change, elle copie le tableau. Les itérations et autres accès en cours d'exécution continuent avec l'ancien tableau, évitant ainsi la nécessité d'une synchronisation entre lecteurs et rédacteurs (bien que l'écriture elle-même doive être synchronisée). Les opérations normalement rapides (particulièrement contains()) sont assez lentes ici, car les tableaux seront recherchés en temps linéaire.

Utilisez-le uniquement pour de très petits ensembles qui seront souvent lus (modifiés) et modifiés rarement. (Swings listener-sets serait un exemple, mais ce ne sont pas vraiment des ensembles, et ne devraient de toute façon être utilisés qu'à partir de l'EDT.)

2) Collections.synchronizedSet encapsulera simplement un bloc synchronisé autour de chaque méthode du jeu d'origine. Vous ne devez pas accéder directement à l'ensemble d'origine. Cela signifie qu'aucune des deux méthodes de l'ensemble ne peut être exécutée simultanément (l'une bloquera jusqu'à la fin de l'autre) - ceci est sans danger pour les threads, mais vous n'aurez pas de concurrence si plusieurs threads utilisent réellement l'ensemble. Si vous utilisez l'itérateur, vous devez généralement toujours effectuer une synchronisation externe pour éviter les exceptions ConcurrentModificationExceptions lors de la modification de l'ensemble entre les appels d'itérateurs. Les performances seront similaires à celles du jeu d'origine (mais avec une surcharge de synchronisation et un blocage si elles sont utilisées simultanément).

Utilisez cette option si vous avez seulement une faible concurrence et que vous voulez vous assurer que toutes les modifications sont immédiatement visibles par les autres threads.

3) ConcurrentSkipListSet est l'implémentation SortedSet concurrente, avec la plupart des opérations de base dans O (log n). Il permet l'ajout/la suppression et la lecture/l'itération simultanés, une itération pouvant ou non indiquer les modifications depuis la création de l'itérateur. Les opérations en bloc sont simplement de multiples appels simples, et non de manière atomique - d'autres threads peuvent n'en observer que quelques-uns.

Évidemment, vous ne pouvez utiliser ceci que si vous avez un ordre total sur vos éléments . Cela semble être un candidat idéal pour les situations à haute concurrence, pour des ensembles pas trop grands (à cause du O (log n)).

4) Pour la ConcurrentHashMap (et l'ensemble qui en est dérivé): Ici, la plupart des options de base sont (en moyenne, si vous avez une bonne et rapide hashCode()) dans O(1) (mais peut dégénérer en O (n) ), comme pour HashMap/HashSet. Il y a un accès limité pour l'écriture (la table est partitionnée et l'accès en écriture sera synchronisé sur la partition requise), tandis que l'accès en lecture est entièrement concourant avec les threads en écriture (mais il est possible que les résultats des modifications ne soient pas encore visibles. écrit). L'itérateur peut voir ou ne pas voir les changements depuis sa création, et les opérations en bloc ne sont pas atomiques . Le redimensionnement est lent (comme pour HashMap/HashSet), essayez donc d'éviter cela en estimant la taille nécessaire à la création (et en utilisant environ 1/3 de plus, car il redimensionne quand 3/4 complet).

Utilisez-le lorsque vous avez de grands ensembles, une bonne (et rapide) fonction de hachage et que vous pouvez estimer la taille de l'ensemble et la simultanéité nécessaire avant de créer la carte.

5) Existe-t-il d’autres implémentations de cartes simultanées que l’on pourrait utiliser ici?

184
Paŭlo Ebermann

Il est possible de combiner les performances contains() de HashSet avec les propriétés liées à la concurrence de CopyOnWriteArraySet en utilisant le AtomicReference<Set> et en remplaçant le jeu complet à chaque modification.

Le schéma de mise en œuvre:

public abstract class CopyOnWriteSet<E> implements Set<E> {

    private final AtomicReference<Set<E>> ref;

    protected CopyOnWriteSet( Collection<? extends E> c ) {
        ref = new AtomicReference<Set<E>>( new HashSet<E>( c ) );
    }

    @Override
    public boolean contains( Object o ) {
        return ref.get().contains( o );
    }

    @Override
    public boolean add( E e ) {
        while ( true ) {
            Set<E> current = ref.get();
            if ( current.contains( e ) ) {
                return false;
            }
            Set<E> modified = new HashSet<E>( current );
            modified.add( e );
            if ( ref.compareAndSet( current, modified ) ) {
                return true;
            }
        }
    }

    @Override
    public boolean remove( Object o ) {
        while ( true ) {
            Set<E> current = ref.get();
            if ( !current.contains( o ) ) {
                return false;
            }
            Set<E> modified = new HashSet<E>( current );
            modified.remove( o );
            if ( ref.compareAndSet( current, modified ) ) {
                return true;
            }
        }
    }

}
19
Oleg Estekhin

Si les Javadocs ne vous aident pas, vous devriez probablement simplement trouver un livre ou un article à lire sur les structures de données. En bref:

  • CopyOnWriteArraySet crée une nouvelle copie du tableau sous-jacent chaque fois que vous modifiez la collection. Les écritures sont donc lentes et les itérateurs rapides et cohérents.
  • Collections.synchronizedSet () utilise des appels de méthode synchronisés à l'ancienne école pour créer un threadsafe Set. Ce serait une version peu performante.
  • ConcurrentSkipListSet offre des écritures performantes avec des opérations de traitement par lots incohérentes (addAll, removeAll, etc.) et des itérateurs.
  • Collections.newSetFromMap (new ConcurrentHashMap ()) a la sémantique de ConcurrentHashMap, qui, selon moi, n'est pas nécessairement optimisée pour les lectures ou les écritures mais, comme ConcurrentSkipListSet, comporte des opérations par lots incohérentes.
10
Ryan Stewart

Ensemble simultané de références faibles

Une autre particularité est un ensemble de références faibles

Un tel ensemble est pratique pour suivre les abonnés dans un scénario pub-sub . Lorsqu'un abonné sort du champ d'application ailleurs et qu'il se prépare par conséquent à devenir un candidat pour le ramassage des ordures, il n'a pas besoin de s'inquiéter de la désinscription en douceur. La référence faible permet à l'abonné d'achever sa transition pour devenir un candidat à la récupération de place. Lorsque les ordures sont finalement collectées, l'entrée de l'ensemble est supprimée.

Bien qu'aucun ensemble de ce type ne soit directement fourni avec les classes groupées, vous pouvez en créer un avec quelques appels.

Commençons par créer une Set de références faibles en utilisant la classe WeakHashMap . Ceci est indiqué dans la documentation de classe pour Collections.newSetFromMap .

Set< YourClassGoesHere > weakHashSet = 
    Collections
    .newSetFromMap(
        new WeakHashMap< YourClassGoesHere , Boolean >()
    )
;

La Valeur de la carte, Boolean, est sans importance ici car la Key de la carte constitue notre Set

Dans un scénario tel que pub-sub, nous avons besoin de la sécurité des threads si les abonnés et les éditeurs opèrent sur des threads distincts (très probablement le cas). 

Allez encore plus loin en créant un ensemble synchronisé pour le rendre compatible avec les threads. Passer un appel à Collections.synchronizedSet .

this.subscribers =
        Collections.synchronizedSet(
                Collections.newSetFromMap(
                        new WeakHashMap <>()  // Parameterized types `< YourClassGoesHere , Boolean >` are inferred, no need to specify.
                )
        );

Nous pouvons maintenant ajouter et supprimer des abonnés de notre Set résultante. Et tous les abonnés "disparus" seront finalement automatiquement supprimés après l'exécution de garbage collection. Le moment de cette exécution dépend de l’implémentation du récupérateur de place de votre machine virtuelle Java et de la situation d’exécution en ce moment. Pour une discussion et un exemple de la date et de la manière dont la WeakHashMap sous-jacente efface les entrées expirées, consultez cette question, * WeakHashMap est-il en train de grandir ou efface-t-il les clés invisibles? * .

0
Basil Bourque