Come può CopyOnWriteArrayList essere thread-safe?

Ho dato un’occhiata al codice sorgente OpenJDK di CopyOnWriteArrayList e sembra che tutte le operazioni di scrittura siano protette dallo stesso blocco e che le operazioni di lettura non siano affatto protette. Come ho capito, sotto JMM tutti gli accessi a una variabile (sia in lettura che in scrittura) devono essere protetti da blocco o possono verificarsi effetti di riordino.

Ad esempio, il metodo set(int, E) contiene queste righe (sotto il blocco):

 /* 1 */ int len = elements.length; /* 2 */ Object[] newElements = Arrays.copyOf(elements, len); /* 3 */ newElements[index] = element; /* 4 */ setArray(newElements); 

Il metodo get(int) , d’altra parte, return get(getArray(), index); solo return get(getArray(), index); .

Nella mia comprensione di JMM, ciò significa che get può osservare l’array in uno stato incoerente se le istruzioni 1-4 vengono riordinate come 1-2 (nuovo) -4-2 (copyOf) -3.

Capisco JMM in modo errato o ci sono altre spiegazioni sul perché CopyOnWriteArrayList è thread-safe?

Se guardi il riferimento dell’array sottostante vedrai che è marcato come volatile . Quando si verifica un’operazione di scrittura (come nell’estratto precedente) questo riferimento volatile viene aggiornato solo setArray finale tramite setArray . Fino a questo punto qualsiasi operazione di lettura restituirà elementi dalla vecchia copia dell’array.

Il punto importante è che l’ aggiornamento dell’array è un’operazione atomica e quindi le letture vedranno sempre la matrice in uno stato coerente.

Il vantaggio di ottenere un blocco solo per le operazioni di scrittura è un miglioramento della velocità di lettura: ciò è dovuto al fatto che le operazioni di scrittura per un object CopyOnWriteArrayList possono essere molto lente poiché comportano la copia dell’intero elenco.

Ottenere il riferimento dell’array è un’operazione atomica. Quindi, i lettori potranno vedere il vecchio array o il nuovo array, indipendentemente dal fatto che lo stato sia coerente. ( set(int,E) calcola il nuovo contenuto dell’array prima di impostare il riferimento, quindi la matrice è coerente quando viene effettuato l’allineamento.)

Il riferimento dell’array stesso è contrassegnato come volatile modo che i lettori non debbano usare un lock per vedere le modifiche all’array referenziato. (EDIT: Inoltre, volatile garantisce che l’assegnazione non venga riordinata, il che comporterebbe l’assegnazione quando l’array è eventualmente in uno stato incoerente).

Il blocco di scrittura è necessario per impedire modifiche simultanee, il che può causare la perdita di dati o modifiche incoerenti della matrice.