Java Language
Tipi atomici
Ricerca…
introduzione
I tipi atomici di Java sono semplici tipi mutabili che forniscono operazioni di base che sono thread-safe e atomiche senza ricorrere al locking. Sono concepiti per l'uso nei casi in cui il blocco sarebbe un collo di bottiglia di concorrenza, o dove vi è il rischio di deadlock o livelock.
Parametri
Parametro | Descrizione |
---|---|
impostato | Set volatile del campo |
ottenere | Lettura volatile del campo |
lazySet | Questa è un'operazione ordinata dal punto vendita del campo |
compareAndSet | Se il valore è il valore di expansion, quindi inviato al nuovo valore |
getAndSet | ottenere il valore corrente e aggiornarlo |
Osservazioni
Molti su essenzialmente combinazioni di letture volatili o scritture e operazioni CAS . Il modo migliore per capire questo è guardare direttamente il codice sorgente. Ad esempio AtomicInteger , Unsafe.getAndSet
Creazione di tipi atomici
Per il codice multi-threaded semplice, l'utilizzo della sincronizzazione è accettabile. Tuttavia, l'utilizzo della sincronizzazione ha un impatto di vivacità e, man mano che la base di codice diventa più complessa, aumenta la probabilità che ti ritroverai con Deadlock , Starvation o Livelock .
In caso di concorrenza più complessa, l'utilizzo di variabili atomiche è spesso un'alternativa migliore, poiché consente di accedere a una singola variabile in modo thread-safe senza il sovraccarico dell'utilizzo di metodi o blocchi di codice sincronizzati.
Creazione di un tipo AtomicInteger
:
AtomicInteger aInt = new AtomicInteger() // Create with default value 0
AtomicInteger aInt = new AtomicInteger(1) // Create with initial value 1
Allo stesso modo per altri tipi di istanze.
AtomicIntegerArray aIntArray = new AtomicIntegerArray(10) // Create array of specific length
AtomicIntegerArray aIntArray = new AtomicIntegerArray(new int[] {1, 2, 3}) // Initialize array with another array
Allo stesso modo per altri tipi atomici.
C'è un'eccezione notevole che non ci sono float
e double
types. Questi possono essere simulati usando Float.floatToIntBits(float)
e Float.intBitsToFloat(int)
per float
, nonché Double.doubleToLongBits(double)
e Double.longBitsToDouble(long)
per i doppi.
Se si desidera utilizzare sun.misc.Unsafe
è possibile utilizzare qualsiasi variabile primitiva come atomica utilizzando l'operazione atomica in sun.misc.Unsafe
. Tutti i tipi primitivi devono essere convertiti o codificati in int o long per utilizzarli in questo modo. Per maggiori informazioni su questo: sun.misc.Unsafe .
Motivazione per i tipi atomici
Il modo più semplice per implementare applicazioni multi-thread è utilizzare la sincronizzazione integrata e le primitive di blocco di Java; ad esempio la parola chiave synchronized
. L'esempio seguente mostra come possiamo usare synchronized
per accumulare conteggi.
public class Counters {
private final int[] counters;
public Counters(int nosCounters) {
counters = new int[nosCounters];
}
/**
* Increments the integer at the given index
*/
public synchronized void count(int number) {
if (number >= 0 && number < counters.length) {
counters[number]++;
}
}
/**
* Obtains the current count of the number at the given index,
* or if there is no number at that index, returns 0.
*/
public synchronized int getCount(int number) {
return (number >= 0 && number < counters.length) ? counters[number] : 0;
}
}
Questa implementazione funzionerà correttamente. Tuttavia, se si dispone di un numero elevato di thread che effettuano molte chiamate simultanee sullo stesso oggetto Counters
, è possibile che la sincronizzazione sia un collo di bottiglia. In particolare:
- Ogni chiamata al metodo
synchronized
inizierà con il thread corrente acquisendo il blocco per l'istanzaCounters
. - Il thread manterrà il blocco mentre controlla il valore
number
e aggiorna il contatore. - Infine, rilascerà il blocco, consentendo l'accesso ad altri thread.
Se un thread tenta di acquisire il blocco mentre un altro lo trattiene, il thread tentante verrà bloccato (arrestato) al passaggio 1 finché il blocco non verrà rilasciato. Se più thread sono in attesa, uno di loro lo otterrà e gli altri continueranno a essere bloccati.
Questo può portare a un paio di problemi:
Se c'è un sacco di contesa per il lock (cioè un sacco di thread tenta di acquisirlo), quindi alcuni thread possono essere bloccati per un lungo periodo di tempo.
Quando un thread è bloccato in attesa del blocco, il sistema operativo in genere prova a passare l'esecuzione a un thread diverso. Questo cambio di contesto comporta un impatto relativamente grande sulle prestazioni del processore.
Quando ci sono più thread bloccati sullo stesso lock, non ci sono garanzie che ognuno di essi verrà trattato "abbastanza" (cioè ogni thread è garantito che sia programmato per essere eseguito). Questo può portare alla fame di thread .
Come si implementa i tipi atomici?
Cominciamo riscrivendo l'esempio sopra usando i contatori AtomicInteger
:
public class Counters {
private final AtomicInteger[] counters;
public Counters(int nosCounters) {
counters = new AtomicInteger[nosCounters];
for (int i = 0; i < nosCounters; i++) {
counters[i] = new AtomicInteger();
}
}
/**
* Increments the integer at the given index
*/
public void count(int number) {
if (number >= 0 && number < counters.length) {
counters[number].incrementAndGet();
}
}
/**
* Obtains the current count of the object at the given index,
* or if there is no number at that index, returns 0.
*/
public int getCount(int number) {
return (number >= 0 && number < counters.length) ?
counters[number].get() : 0;
}
}
Abbiamo sostituito int[]
con un AtomicInteger[]
e inizializzato con un'istanza in ogni elemento. Abbiamo anche aggiunto le chiamate a incrementAndGet()
e get()
al posto delle operazioni sui valori int
.
Ma la cosa più importante è che possiamo rimuovere la parola chiave synchronized
perché il blocco non è più necessario. Questo funziona perché le operazioni incrementAndGet()
e get()
sono atomiche e thread-safe . In questo contesto, significa che:
Ogni contatore dell'array sarà solo osservabile nello stato "prima" di un'operazione (come un "incremento") o nello stato "dopo".
Supponendo che l'operazione avvenga al tempo
T
, nessun thread sarà in grado di vedere lo stato "prima" dopo il tempoT
Inoltre, mentre due thread potrebbero effettivamente tentare di aggiornare la stessa istanza di AtomicInteger
nello stesso momento, le implementazioni delle operazioni assicurano che solo un incremento si verifica alla volta nell'istanza specificata. Questo viene fatto senza blocco, spesso con prestazioni migliori.
Come funzionano i tipi atomici?
Tipicamente i tipi atomici si basano su istruzioni hardware specializzate nel set di istruzioni della macchina target. Ad esempio, i set di istruzioni basati su Intel forniscono un'istruzione CAS
( Compare and Swap ) che eseguirà una sequenza specifica di operazioni di memoria atomicamente.
Queste istruzioni di basso livello vengono utilizzate per implementare operazioni di livello superiore nelle API delle rispettive classi AtomicXxx
. Ad esempio, (di nuovo, in pseudocodice C-like):
private volatile num;
int increment() {
while (TRUE) {
int old = num;
int new = old + 1;
if (old == compare_and_swap(&num, old, new)) {
return new;
}
}
}
Se non c'è contesa su AtomicXxxx
, il test if
avrà esito positivo e il ciclo terminerà immediatamente. Se c'è contesa, il if
fallirà per tutti tranne uno dei thread, e "ruoterà" nel ciclo per un piccolo numero di cicli del ciclo. In pratica, la rotazione è più veloce di ordini di grandezza (eccetto che per livelli non realistici di contesa, dove le prestazioni sincronizzate sono migliori rispetto alle classi atomiche perché quando l'operazione CAS fallisce, il tentativo di aggiungere altro contesa) che sospendere il thread e passare a un altro uno.
Per inciso, le istruzioni CAS vengono in genere utilizzate da JVM per implementare il blocco non mirato . Se la JVM può vedere che un blocco non è attualmente bloccato, tenterà di utilizzare un CAS per acquisire il blocco. Se il CAS ha esito positivo, non è necessario eseguire la costosa programmazione dei thread, il cambio di contesto e così via. Per ulteriori informazioni sulle tecniche utilizzate, vedere Bloccaggio di parte in HotSpot .