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:

  1. Ogni chiamata al metodo synchronized inizierà con il thread corrente acquisendo il blocco per l'istanza Counters .
  2. Il thread manterrà il blocco mentre controlla il valore number e aggiorna il contatore.
  3. 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 tempo T

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 .



Modified text is an extract of the original Stack Overflow Documentation
Autorizzato sotto CC BY-SA 3.0
Non affiliato con Stack Overflow