CodeGym /Corsi /All lectures for IT purposes /Operazioni atomiche in Java

Operazioni atomiche in Java

All lectures for IT purposes
Livello 1 , Lezione 426
Disponibile

Prerequisiti per l'emergere di operazioni atomiche

Diamo un'occhiata a questo esempio per aiutarti a capire come funzionano le operazioni atomiche:

public class Counter {
    int count;

    public void increment() {
        count++;
    }
}

Quando abbiamo un thread, tutto funziona alla grande, ma se aggiungiamo il multithreading, otteniamo risultati errati e tutto perché l'operazione di incremento non è un'operazione, ma tre: una richiesta per ottenere il valore correntecontare, quindi incrementalo di 1 e scrivi di nuovo sucontare.

E quando due thread vogliono incrementare una variabile, molto probabilmente perderai i dati. Cioè, entrambi i thread ricevono 100, di conseguenza, entrambi scriveranno 101 invece del valore previsto di 102.

E come risolverlo? Devi usare i lucchetti. La parola chiave sincronizzata aiuta a risolvere questo problema, il suo utilizzo ti dà la garanzia che un thread alla volta accederà al metodo.

public class SynchronizedCounterWithLock {
    private volatile int count;

    public synchronized void increment() {
        count++;
    }
}

Inoltre, è necessario aggiungere la parola chiave volatile , che garantisce la corretta visibilità dei riferimenti tra i thread. Abbiamo esaminato il suo lavoro sopra.

Ma ci sono ancora aspetti negativi. Il più grande è la prestazione, in quel momento in cui molti thread stanno tentando di acquisire un blocco e uno ottiene un'opportunità di scrittura, il resto dei thread verrà bloccato o sospeso fino al rilascio del thread.

Tutti questi processi, blocco, passaggio a un altro stato sono molto costosi per le prestazioni del sistema.

Operazioni atomiche

L'algoritmo utilizza istruzioni macchina di basso livello come compare-and-swap (CAS, compare-and-swap, che garantisce l'integrità dei dati e vi è già una grande quantità di ricerche su di esse).

Una tipica operazione CAS opera su tre operandi:

  • Spazio di memoria per il lavoro (M)
  • Valore atteso esistente (A) di una variabile
  • Nuovo valore (B) da impostare

CAS aggiorna atomicamente M a B, ma solo se il valore di M è lo stesso di A, altrimenti non viene intrapresa alcuna azione.

Nel primo e nel secondo caso, verrà restituito il valore di M. Ciò consente di combinare tre passaggi, vale a dire ottenere il valore, confrontare il valore e aggiornarlo. E tutto si trasforma in un'unica operazione a livello di macchina.

Nel momento in cui un'applicazione multi-thread accede a una variabile e tenta di aggiornarla e CAS viene applicato, uno dei thread la riceverà e sarà in grado di aggiornarla. Ma a differenza dei blocchi, altri thread riceveranno semplicemente errori relativi all'impossibilità di aggiornare il valore. Quindi passeranno a ulteriori lavori e il passaggio è completamente escluso in questo tipo di lavoro.

In questo caso, la logica diventa più difficile a causa del fatto che dobbiamo gestire la situazione in cui l'operazione CAS non ha funzionato correttamente. Modelleremo semplicemente il codice in modo che non vada avanti finché l'operazione non ha successo.

Introduzione ai tipi atomici

Ti sei imbattuto in una situazione in cui devi impostare la sincronizzazione per la variabile più semplice di tipo int ?

Il primo modo che abbiamo già trattato è l'utilizzo di volatile + synchroned . Ma ci sono anche speciali classi Atomic*.

Se utilizziamo CAS, le operazioni funzionano più velocemente rispetto al primo metodo. Inoltre, abbiamo metodi speciali e molto convenienti per aggiungere un valore e operazioni di incremento e decremento.

AtomicBoolean , AtomicInteger , AtomicLong , AtomicIntegerArray , AtomicLongArray sono classi in cui le operazioni sono atomiche. Di seguito analizzeremo il lavoro con loro.

AtomicoIntero

La classe AtomicInteger fornisce operazioni su un valore int che può essere letto e scritto in modo atomico, oltre a fornire operazioni atomiche estese.

Ha metodi get e set che funzionano come leggere e scrivere variabili.

Cioè, "accade prima" con ogni successiva ricezione della stessa variabile di cui abbiamo parlato prima. Anche il metodo atomico compareAndSet dispone di queste funzionalità di coerenza della memoria.

Tutte le operazioni che restituiscono un nuovo valore vengono eseguite atomicamente:

int addAndGet (int delta) Aggiunge un valore specifico al valore corrente.
boolean compareAndSet(expected int, update int) Imposta il valore sul valore aggiornato specificato se il valore corrente corrisponde al valore previsto.
int decrementAndGet() Diminuisce il valore corrente di uno.
int getAndAdd(int delta) Aggiunge il valore dato al valore corrente.
int getAndDecrement() Diminuisce il valore corrente di uno.
int getAndIncrement() Aumenta il valore corrente di uno.
int getAndSet(int nuovoValore) Imposta il valore dato e restituisce il vecchio valore.
int incrementoAndGet() Aumenta il valore corrente di uno.
lazySet(int nuovoValore) Infine impostato sul valore dato.
booleano weakCompareAndSet(previsto, aggiorna int) Imposta il valore sul valore aggiornato specificato se il valore corrente corrisponde al valore previsto.

Esempio:

ExecutorService executor = Executors.newFixedThreadPool(5);
IntStream.range(0, 50).forEach(i -> executor.submit(atomicInteger::incrementAndGet));
executor.shutdown();
executor.awaitTermination(Long.MAX_VALUE, TimeUnit.HOURS);

System.out.println(atomicInteger.get()); // prints 50
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION