Una HashMap è thread-safe per chiavi diverse?


87

Se ho due thread multipli che accedono a una HashMap, ma garantisco che non accederanno mai alla stessa chiave contemporaneamente, ciò potrebbe comunque portare a una condizione di competizione?

Risposte:


99

Nella risposta di @ dotsid dice questo:

Se modifichi una HashMap in qualsiasi modo, il tuo codice è semplicemente rotto.

Ha ragione. Una HashMap aggiornata senza sincronizzazione si interromperà anche se i thread utilizzano set di chiavi disgiunti. Ecco alcune delle cose che possono andare storte.

  • Se un thread esegue un put, un altro thread potrebbe visualizzare un valore non aggiornato per la dimensione della hashmap.

  • Quando un thread esegue un'operazione putche attiva una ricostruzione della tabella, un altro thread può visualizzare versioni transitorie o obsolete del riferimento all'array hashtable, le sue dimensioni, i suoi contenuti o le catene hash. Potrebbe derivarne il caos.

  • Quando un thread esegue una putper una chiave che si scontra con una chiave utilizzata da un altro thread e quest'ultimo thread esegue una putper la sua chiave, quest'ultimo potrebbe vedere una copia obsoleta del riferimento alla catena hash. Potrebbe derivarne il caos.

  • Quando un thread sonda la tabella con una chiave che entra in collisione con una delle chiavi di un altro thread, potrebbe incontrare quella chiave sulla catena. Chiamerà uguale su quella chiave e, se i thread non sono sincronizzati, il metodo equals potrebbe riscontrare uno stato non aggiornato in quella chiave.

E se hai due thread che eseguono puto removerichieste contemporaneamente , ci sono numerose opportunità per le condizioni di gara.

Posso pensare a tre soluzioni:

  1. Usa un file ConcurrentHashMap.
  2. Usa un normale HashMapma sincronizzato all'esterno; es. usando mutex primitivi, Lockoggetti, eccetera.
  3. Usa un diverso HashMapper ogni thread. Se i thread hanno davvero un set di chiavi disgiunto, non dovrebbe essere necessario (dal punto di vista algoritmico) che condividano una singola mappa. In effetti, se i tuoi algoritmi coinvolgono i thread che iterano le chiavi, i valori o le voci della mappa a un certo punto, la divisione della singola mappa in più mappe potrebbe dare una velocità significativa per quella parte dell'elaborazione.

30

Usa solo un ConcurrentHashMap. ConcurrentHashMap utilizza più blocchi che coprono una gamma di bucket di hash per ridurre le possibilità che un blocco venga contestato. Esiste un impatto marginale sulle prestazioni nell'acquisizione di un blocco non contestato.

Per rispondere alla tua domanda originale: secondo il javadoc, finché la struttura della mappa non cambia, stai bene. Ciò significa nessuna rimozione di elementi e nessuna aggiunta di nuove chiavi che non sono già nella mappa. La sostituzione del valore associato alle chiavi esistenti va bene.

Se più thread accedono contemporaneamente a una mappa hash e almeno uno dei thread modifica strutturalmente la mappa, è necessario sincronizzarla esternamente. (Una modifica strutturale è qualsiasi operazione che aggiunge o elimina una o più mappature; la semplice modifica del valore associato a una chiave già contenuta in un'istanza non è una modifica strutturale.)

Anche se non garantisce la visibilità. Quindi devi essere disposto ad accettare il recupero di associazioni obsolete di tanto in tanto.


6

Dipende da cosa intendi per "accesso". Se stai leggendo, puoi leggere anche le stesse chiavi purché la visibilità dei dati sia garantita secondo le regole " accade prima ". Ciò significa che HashMapnon dovrebbe cambiare e tutte le modifiche (costruzioni iniziali) dovrebbero essere completate prima che qualsiasi lettore inizi ad accedere HashMap.

Se modifichi un HashMapin qualsiasi modo, il tuo codice è semplicemente rotto. @Stephen C fornisce un'ottima spiegazione del perché.

EDIT: Se il primo caso è la tua situazione attuale, ti consiglio di usarlo Collections.unmodifiableMap()per assicurarti che la tua HashMap non venga mai modificata. Anche gli oggetti puntati da HashMapnon dovrebbero cambiare, quindi l'uso aggressivo di finalparole chiave può aiutarti.

E come dice @Lars Andren, ConcurrentHashMapè la scelta migliore nella maggior parte dei casi.


2
ConcurrentHashMap è la scelta migliore secondo me. L'unico motivo per cui non l'ho consigliato, perché l'autore non l'ha chiesto :) Ha un throughput inferiore a causa delle operazioni CAS, ma come dice la regola d'oro della programmazione concorrente: "Fallo bene, e solo allora fallo veloce ":)
Denis Bazhenov,

unmodifiableMapassicura che il client non possa modificare la mappa. Non fa nulla per garantire che la mappa sottostante non venga modificata.
Pete Kirkham

Come ho già sottolineato: "Anche gli oggetti puntati da HashMap non dovrebbero cambiare"
Denis Bazhenov

4

La modifica di una HashMap senza una corretta sincronizzazione da due thread può facilmente portare a una condizione di competizione.

  • Quando a put()porta a un ridimensionamento della tabella interna, questo richiede del tempo e l'altro thread continua a scrivere nella vecchia tabella.
  • Due put()per chiavi diverse portano ad un aggiornamento dello stesso bucket se i codici hash delle chiavi sono uguali al modulo della dimensione della tabella. (In realtà, la relazione tra codice hash e indice del bucket è più complicata, ma possono comunque verificarsi collisioni.)

1
È peggio delle sole condizioni di gara. A seconda degli interni HashMapdell'implementazione che stai utilizzando, puoi ottenere il danneggiamento delle HashMapstrutture dati, eccetera a causa di anomalie della memoria.
Stephen C
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.