Diversi tipi di set thread-safe in Java


135

Sembra che ci siano molte implementazioni e modi diversi per generare insiemi thread-safe in Java. Alcuni esempi includono

1) CopyOnWriteArraySet

2) Collections.synchronizedSet (Set set)

3) ConcurrentSkipListSet

4) Collections.newSetFromMap (new ConcurrentHashMap ())

5) Altri set generati in modo simile a (4)

Questi esempi provengono da Concurrency Pattern: implementazioni di Concurrent Set in Java 6

Qualcuno potrebbe semplicemente spiegare le differenze, i vantaggi e gli svantaggi di questi esempi e di altri? Sto avendo problemi a capire e mantenere tutto da Java Std Docs.

Risposte:


206

1) Si CopyOnWriteArraySettratta di un'implementazione abbastanza semplice: fondamentalmente ha un elenco di elementi in un array e quando si modifica l'elenco, copia l'array. Iterazioni e altri accessi in esecuzione in questo momento continuano con il vecchio array, evitando la necessità di sincronizzazione tra lettori e scrittori (sebbene la scrittura stessa debba essere sincronizzata). Le operazioni di impostazione normalmente veloci (in particolare contains()) sono piuttosto lente qui, poiché gli array verranno cercati in tempo lineare.

Usalo solo per set veramente piccoli che verranno letti (ripetuti) spesso e cambiati di rado. (I set di listener di swing potrebbero essere un esempio, ma questi non sono realmente set e dovrebbero comunque essere usati solo dall'EDT.)

2) Collections.synchronizedSetavvolgerà semplicemente un blocco sincronizzato attorno a ciascun metodo dell'insieme originale. Non dovresti accedere direttamente al set originale. Ciò significa che non è possibile eseguire contemporaneamente due metodi dell'insieme (uno bloccherà fino a quando l'altro non termina): questo è sicuro per i thread, ma non si avrà concorrenza se più thread stanno realmente utilizzando l'insieme. Se si utilizza l'iteratore, in genere è comunque necessario sincronizzarsi esternamente per evitare ConcurrentModificationExceptions quando si modifica il set tra le chiamate dell'iteratore. Le prestazioni saranno come quelle del set originale (ma con un certo sovraccarico di sincronizzazione e blocco se utilizzate contemporaneamente).

Usalo se hai solo una bassa concorrenza e vuoi essere sicuro che tutte le modifiche siano immediatamente visibili agli altri thread.

3) ConcurrentSkipListSetè l' SortedSetimplementazione concorrente , con la maggior parte delle operazioni di base in O (log n). Consente l'aggiunta / rimozione e la lettura / iterazione simultanee, in cui l'iterazione può o meno indicare i cambiamenti dalla creazione dell'iteratore. Le operazioni collettive sono semplicemente più chiamate singole e non atomicamente - altri thread possono osservarne solo alcune.

Ovviamente puoi usarlo solo se hai un po 'di ordine totale sui tuoi elementi. Sembra un candidato ideale per situazioni ad alta concorrenza, per insiemi non troppo grandi (a causa della O (log n)).

4) Per ConcurrentHashMap(e il set derivato da esso): qui la maggior parte delle opzioni di base sono (in media, se hai una buona e veloce hashCode()) in O (1) (ma potrebbe degenerare in O (n)), come per HashMap / HashSet. Esiste una concorrenza limitata per la scrittura (la tabella è partizionata e l'accesso in scrittura sarà sincronizzato sulla partizione necessaria), mentre l'accesso in lettura è completamente concorrente a se stesso e ai thread di scrittura (ma potrebbe non vedere ancora i risultati delle modifiche in corso scritto). L'iteratore può o meno vedere le modifiche da quando è stato creato e le operazioni in blocco non sono atomiche. Il ridimensionamento è lento (come per HashMap / HashSet), quindi cerca di evitarlo stimando le dimensioni necessarie al momento della creazione (e usandone circa 1/3 in più, dato che si ridimensiona quando è pieno per 3/4).

Utilizzalo quando hai set di grandi dimensioni, una buona (e veloce) funzione hash e puoi stimare la dimensione del set e la concorrenza necessaria prima di creare la mappa.

5) Esistono altre implementazioni di mappe simultanee che è possibile utilizzare qui?


1
Solo una correzione visiva in 1), il processo di copia dei dati nel nuovo array deve essere bloccato sincronizzando. Pertanto, CopyOnWriteArraySet non evita totalmente la necessità della sincronizzazione.
CaptainHastings,

Sul ConcurrentHashMapset basato, "cerca quindi di evitarlo stimando la dimensione necessaria alla creazione". La dimensione assegnata alla mappa dovrebbe essere maggiore del 33% rispetto alla stima (o al valore noto), poiché il set viene ridimensionato al 75% del carico. Io usoexpectedSize + 4 / 3 + 1
Daren il

@Daren Immagino che il primo +debba essere un *?
Paŭlo Ebermann,

@ PaŭloEbermann Certo ... dovrebbe essereexpectedSize * 4 / 3 + 1
Daren,

1
Per ConcurrentMap(o HashMap) in Java 8 se il numero di voci mappate sullo stesso bucket raggiunge il valore di soglia (credo che sia 16), l'elenco viene modificato in un albero di ricerca binario (albero rosso-nero da precedere) e in quel caso cercare il tempo sarebbe O(lg n)e non O(n).
akhil_mittal,

20

È possibile combinare le contains()prestazioni di HashSetcon le proprietà relative alla concorrenza di CopyOnWriteArraySetutilizzando AtomicReference<Set>e sostituendo l'intero set su ogni modifica.

Lo schizzo di implementazione:

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;
            }
        }
    }

}

In realtà AtomicReferencesegna il valore volatile. Significa che nessun thread legge i dati obsoleti e happens-beforegarantisce che il compilatore non può riordinare il codice. Ma se AtomicReferencevengono utilizzati solo i metodi get / set di, in realtà stiamo marcando la nostra variabile volatile in modo fantasioso.
akhil_mittal,

Questa risposta non può essere sufficientemente votata perché (1) a meno che non mi sia perso qualcosa, funzionerà per tutti i tipi di raccolta (2) nessuna delle altre classi fornisce un modo per aggiornare atomicamente l'intera raccolta in una sola volta ... Questo è molto utile .
Gili,

Ho provato ad appropriarmi alla lettera, ma ho scoperto che era etichettato abstract, apparentemente per evitare di dover scrivere molti dei metodi. Ho deciso di aggiungerli, ma ho incontrato un blocco con iterator(). Non so come mantenere un iteratore su questa cosa senza rompere il modello. Sembra che debba sempre passare attraverso ref, e potrei ottenere ogni volta un set sottostante diverso, il che richiede di ottenere un nuovo iteratore sul set sottostante, il che è inutile per me, poiché inizierà con l'articolo zero. Qualche intuizione?
nclark,

Va bene immagino che la garanzia sia che ogni cliente riceva un'istantanea fissa in tempo, quindi l'iteratore della raccolta sottostante funzionerebbe bene se questo è tutto ciò di cui hai bisogno. Il mio caso d'uso è quello di consentire ai thread in competizione di "rivendicare" singole risorse al suo interno e non funzionerà se hanno versioni diverse del set. Al secondo però ... immagino che il mio thread abbia solo bisogno di ottenere un nuovo iteratore e riprovare se CopyOnWriteSet.remove (chosen_item) restituisce false ... Cosa che dovrebbe fare a prescindere :)
nclark,

11

Se i Javadocs non aiutano, probabilmente dovresti semplicemente trovare un libro o un articolo da leggere sulle strutture di dati. A prima vista:

  • CopyOnWriteArraySet crea una nuova copia dell'array sottostante ogni volta che si modifica la raccolta, quindi le scritture sono lente e gli iteratori sono veloci e coerenti.
  • Collections.synchronizedSet () utilizza le chiamate al metodo sincronizzato della vecchia scuola per creare un thread thread sicuro. Questa sarebbe una versione a basse prestazioni.
  • ConcurrentSkipListSet offre scritture performanti con operazioni batch incoerenti (addAll, removeAll, ecc.) E Iteratori.
  • Collections.newSetFromMap (new ConcurrentHashMap ()) ha la semantica di ConcurrentHashMap, che credo non sia necessariamente ottimizzata per letture o scritture, ma come ConcurrentSkipListSet, ha operazioni batch incoerenti.


1

Insieme simultaneo di riferimenti deboli

Un'altra svolta è un insieme sicuro di riferimenti deboli .

Tale set è utile per tracciare gli abbonati in uno scenario pub-sub . Quando un abbonato esce dal campo di applicazione in altri luoghi e quindi si avvia a diventare un candidato per la raccolta dei rifiuti, non è necessario che l'abbonato si preoccupi di annullare l'iscrizione con garbo. Il riferimento debole consente all'abbonato di completare la transizione per diventare un candidato per la raccolta dei rifiuti. Quando alla fine la spazzatura viene raccolta, la voce nel set viene rimossa.

Sebbene tale set non sia fornito direttamente con le classi in bundle, è possibile crearne uno con poche chiamate.

Per prima cosa iniziamo con un Setriferimento debole facendo leva sulla WeakHashMapclasse. Questo è mostrato nella documentazione di classe per Collections.newSetFromMap.

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

Il valore della mappa, Booleanqui, è irrilevante in quanto la chiave della mappa costituisce la nostra Set.

In uno scenario come pub-sub, abbiamo bisogno della sicurezza dei thread se gli abbonati e gli editori operano su thread separati (molto probabilmente il caso).

Fai un ulteriore passo avanti avvolgendolo come un set sincronizzato per rendere sicuro questo set. Inserisci una chiamata a Collections.synchronizedSet.

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

Ora possiamo aggiungere e rimuovere gli abbonati dal nostro risultato Set. E tutti gli abbonati "a scomparsa" verranno infine rimossi automaticamente dopo l'esecuzione della garbage collection. Quando si verifica questa esecuzione dipende dall'implementazione del Garbage Collector di JVM e dipende dalla situazione di runtime al momento. Per la discussione e l'esempio di quando e come il sottostante WeakHashMapcancella le voci scadute, vedere questa domanda: * WeakHashMap è in continua crescita o cancella le immondizie? * .

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.