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

135

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

1) CopyOnWriteArraySet

2) Collections.synchronizedSet (ensemble d'ensemble)

3) ConcurrentSkipListSet

4) Collections.newSetFromMap (nouveau ConcurrentHashMap ())

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

Ces exemples proviennent de Concurrency Pattern: Implémentations de jeux simultanés dans Java 6

Quelqu'un pourrait-il expliquer simplement les différences, les avantages et les inconvénients de ces exemples et d'autres? J'ai du mal à comprendre et à garder tout droit de la documentation Java Std.

Ben
la source

Réponses:

206

1) Le CopyOnWriteArraySetest une implémentation assez simple - il a essentiellement une liste d'éléments dans un tableau, et lors de la modification de la liste, il copie le tableau. Les itérations et autres accès qui sont en cours à ce moment continuent avec l'ancien tableau, évitant la nécessité d'une synchronisation entre les lecteurs et les écrivains (bien que l'écriture elle-même doive être synchronisée). Les opérations de réglage normalement rapides (en particulier contains()) sont assez lentes ici, car les tableaux seront recherchés en temps linéaire.

Utilisez ceci uniquement pour de très petits ensembles qui seront lus (itérés) souvent et rarement modifiés. (Les ensembles d'écouteurs Swings seraient 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.synchronizedSetenroulera simplement un bloc synchronisé autour de chaque méthode de l'ensemble d'origine. Vous ne devez pas accéder directement à l'ensemble d'origine. Cela signifie qu'aucune méthode de l'ensemble ne peut être exécutée simultanément (l'une se bloquera jusqu'à ce que l'autre se termine) - c'est thread-safe, mais vous n'aurez pas de concurrence si plusieurs threads utilisent vraiment 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érateur. Les performances seront similaires à celles de l'ensemble d'origine (mais avec une surcharge de synchronisation et un blocage si elles sont utilisées simultanément).

Utilisez cette option si vous n'avez qu'une faible concurrence d'accès et que vous voulez vous assurer que toutes les modifications sont immédiatement visibles pour les autres threads.

3) ConcurrentSkipListSetest l' SortedSetimplémentation simultanée , avec la plupart des opérations de base en O (log n). Il permet l'ajout / suppression et la lecture / itération simultanés, où l'itération peut ou non indiquer les changements depuis la création de l'itérateur. Les opérations en bloc sont simplement de multiples appels uniques, et non de manière atomique - d'autres threads peuvent n'en observer que certains.

Évidemment, vous ne pouvez l'utiliser que si vous avez une commande totale sur vos éléments. Cela ressemble à un candidat idéal pour les situations à forte concurrence, pour des ensembles pas trop grands (à cause du O (log n)).

4) Pour le ConcurrentHashMap(et l'ensemble qui en dérive): Ici, la plupart des options de base sont (en moyenne, si vous avez un bon et rapide hashCode()) dans O (1) (mais peuvent dégénérer en O (n)), comme pour HashMap / HashSet. Il y a une concurrence limitée pour l'écriture (la table est partitionnée et l'accès en écriture sera synchronisé sur la partition nécessaire), tandis que l'accès en lecture est entièrement simultané avec lui-même et les threads d'écriture (mais il se peut que les résultats des modifications actuellement en cours ne soient pas encore visibles. écrit). L'itérateur peut ou non 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 est redimensionné lorsqu'il est plein aux 3/4).

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

5) Y a-t-il d'autres implémentations de cartes concurrentes que l'on pourrait utiliser ici?

Paŭlo Ebermann
la source
1
Juste une correction visuelle en 1), le processus de copie des données dans le nouveau tableau doit être verrouillé par synchronisation. Par conséquent, CopyOnWriteArraySet n'évite pas totalement la nécessité de la synchronisation.
CaptainHastings
Sur la ConcurrentHashMapbase de l'ensemble, «essayez donc d'éviter cela en estimant la taille nécessaire à la création». La taille que vous donnez à la carte doit être plus de 33% plus grande que votre estimation (ou valeur connue), car l'ensemble se redimensionne à 75% de charge. J'utiliseexpectedSize + 4 / 3 + 1
Daren
@Daren Je suppose que le premier +est censé être un *?
Paŭlo Ebermann
@ PaŭloEbermann Bien sûr ... ça devrait êtreexpectedSize * 4 / 3 + 1
Daren
1
Pour ConcurrentMap(ou HashMap) en Java 8 si le nombre d'entrées mappées sur le même compartiment atteint la valeur seuil (je crois que c'est 16), alors la liste est changée en un arbre de recherche binaire (arbre rouge-noir à préciser) et dans ce cas, recherchez le temps serait O(lg n)et non O(n).
akhil_mittal
20

Il est possible de combiner les contains()performances de HashSetavec les propriétés liées à la concurrence de CopyOnWriteArraySeten utilisant le AtomicReference<Set>et en remplaçant l'ensemble complet à chaque modification.

L'esquisse 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;
            }
        }
    }

}
Oleg Estekhin
la source
Marque en fait AtomicReferencela valeur volatile. Cela signifie qu'il s'assure qu'aucun thread ne lit des données périmées et fournit une happens-beforegarantie car le code ne peut pas être réorganisé par le compilateur. Mais si seules les méthodes get / set de AtomicReferencesont utilisées, nous marquons en fait notre variable comme volatile d'une manière sophistiquée.
akhil_mittal
Cette réponse ne peut pas être assez votée car (1) à moins que j'aie manqué quelque chose, cela fonctionnera pour tous les types de collection (2) aucune des autres classes ne fournit un moyen de mettre à jour atomiquement toute la collection à la fois ... C'est très utile .
Gili
J'ai essayé de m'approprier ce mot mot pour mot mais j'ai trouvé qu'il était étiqueté abstract, apparemment pour éviter d'avoir à écrire plusieurs des méthodes. Je me suis mis à les ajouter, mais je suis tombé sur un barrage routier avec iterator(). Je ne sais pas comment maintenir un itérateur sur cette chose sans casser le modèle. Il semble que je doive toujours passer par le ref, et que je pourrais obtenir un ensemble sous-jacent différent à chaque fois, ce qui nécessite un nouvel itérateur sur l'ensemble sous-jacent, ce qui m'est inutile, car il commencera par l'élément zéro. Des idées?
nclark le
D'accord, je suppose que la garantie est que chaque client reçoit un instantané fixe à temps, de sorte que l'itérateur de la collection sous-jacente fonctionnerait bien si c'est tout ce dont vous avez besoin. Mon cas d'utilisation est d'autoriser les threads concurrents à "revendiquer" des ressources individuelles, et cela ne fonctionnera pas s'ils ont des versions différentes de l'ensemble. À la seconde cependant ... Je suppose que mon thread a juste besoin d'obtenir un nouvel itérateur et réessayer si CopyOnWriteSet.remove (selected_item) renvoie false ... Ce qu'il devrait faire quoi qu'il en soit :)
nclark
11

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 un coup d'oeil:

  • 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 sont rapides et cohérents.
  • Collections.synchronizedSet () utilise des appels de méthode synchronisée à l'ancienne pour rendre un Set threadsafe. Ce serait une version peu performante.
  • ConcurrentSkipListSet offre des écritures performantes avec des opérations par lots incohérentes (addAll, removeAll, etc.) et des itérateurs.
  • Collections.newSetFromMap (new ConcurrentHashMap ()) a la sémantique de ConcurrentHashMap, qui, je crois, n'est pas nécessairement optimisée pour les lectures ou les écritures, mais comme ConcurrentSkipListSet, a des opérations par lots incohérentes.
Ryan Stewart
la source
1
developer.com/java/article.php/10922_3829891_2/… <encore mieux qu'un livre)
ycomp
1

Ensemble simultané de références faibles

Une autre torsion est un ensemble de références faibles thread-safe .

Un tel ensemble est pratique pour suivre les abonnés dans un scénario pub-sub . Lorsqu'un abonné est hors de portée à d'autres endroits, et se dirige donc vers le candidat pour le ramassage des ordures, l'abonné n'a pas besoin de se désabonner gracieusement. La référence faible permet à l'abonné d'achever sa transition pour devenir un candidat au garbage collection. Lorsque le garbage est finalement collecté, l'entrée de l'ensemble est supprimée.

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

Nous commençons par créer Setdes références faibles en tirant parti de la WeakHashMapclasse. Ceci est montré dans la documentation de classe pour Collections.newSetFromMap.

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

La valeur de la carte Booleann'est pas pertinente ici car la clé 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 fonctionnent sur des threads séparés (très probablement le cas).

Allez un peu plus loin en enveloppant comme un ensemble synchronisé pour rendre cet ensemble thread-safe. Participez à 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 résultat Set. Et tous les abonnés «disparaissant» seront finalement automatiquement supprimés après l'exécution du ramasse-miettes. Le moment où cette exécution se produit dépend de l'implémentation du garbage collector de votre JVM et de la situation d'exécution actuelle. Pour une discussion et un exemple de quand et comment le sous-jacent WeakHashMapefface les entrées expirées, voir cette question, * WeakHashMap est-il en constante croissance ou efface-t-il les clés de garbage? * .

Basil Bourque
la source