Usi pratici per AtomicInteger


230

Ho capito che AtomicInteger e altre variabili Atomic consentono accessi simultanei. In quali casi viene tuttavia utilizzata questa classe?

Risposte:


190

Esistono due usi principali di AtomicInteger:

  • Come contatore atomico ( incrementAndGet(), ecc.) Che può essere utilizzato contemporaneamente da molti thread

  • Come primitiva che supporta le istruzioni di confronto e scambio ( compareAndSet()) per implementare algoritmi non bloccanti.

    Ecco un esempio di generatore di numeri casuali senza blocco dalla concorrenza Java in pratica di Brian Göetz :

    public class AtomicPseudoRandom extends PseudoRandom {
        private AtomicInteger seed;
        AtomicPseudoRandom(int seed) {
            this.seed = new AtomicInteger(seed);
        }
    
        public int nextInt(int n) {
            while (true) {
                int s = seed.get();
                int nextSeed = calculateNext(s);
                if (seed.compareAndSet(s, nextSeed)) {
                    int remainder = s % n;
                    return remainder > 0 ? remainder : remainder + n;
                }
            }
        }
        ...
    }

    Come puoi vedere, funziona praticamente allo stesso modo di incrementAndGet(), ma esegue calcoli arbitrari ( calculateNext()) invece di incrementi (ed elabora il risultato prima del ritorno).


1
Penso di capire il primo utilizzo. Questo per assicurarsi che il contatore sia stato incrementato prima di accedere nuovamente a un attributo. Corretta? Potresti dare un breve esempio per il secondo utilizzo?
James P.

8
La comprensione del primo utilizzo è in qualche modo vera: garantisce semplicemente che se un altro thread modifica il contatore tra le operazioni reade write that value + 1, questo viene rilevato piuttosto che sovrascrivere il vecchio aggiornamento (evitando il problema "aggiornamento perso"). Questo è in realtà un caso speciale di compareAndSet- se il vecchio valore era 2, la classe effettivamente chiama compareAndSet(2, 3)- quindi se un altro thread ha modificato il valore nel frattempo, il metodo di incremento si riavvia effettivamente dall'inizio.
Andrzej Doyle,

3
"resto> 0? resto: resto + n;" in questa espressione c'è un motivo per aggiungere il resto a n quando è 0?
sandeepkunkunuru,

101

L'esempio più semplice in assoluto che mi viene in mente è quello di fare incrementare un'operazione atomica.

Con ints standard:

private volatile int counter;

public int getNextUniqueIndex() {
    return counter++; // Not atomic, multiple threads could get the same result
}

Con AtomicInteger:

private AtomicInteger counter;

public int getNextUniqueIndex() {
    return counter.getAndIncrement();
}

Quest'ultimo è un modo molto semplice per eseguire semplici effetti di mutazioni (in particolare il conteggio o l'indicizzazione univoca), senza dover ricorrere alla sincronizzazione di tutti gli accessi.

È possibile utilizzare compareAndSet()una logica senza sincronizzazione più complessa utilizzando come tipo di blocco ottimistico: ottenere il valore corrente, calcolare il risultato in base a questo, impostare questo risultato se il valore è ancora l'input utilizzato per eseguire il calcolo, altrimenti ricominciare, ma il gli esempi di conteggio sono molto utili, e lo userò spesso AtomicIntegersper il conteggio e generatori univoci a livello di VM se c'è qualche suggerimento su più thread coinvolti, perché sono così facili da lavorare che considererei quasi ottimizzazione prematura per usare plain ints.

Mentre puoi quasi sempre ottenere le stesse garanzie di sincronizzazione con intse synchronizeddichiarazioni appropriate , il bello AtomicIntegerè che la sicurezza del thread è integrata nell'oggetto reale stesso, piuttosto che preoccuparti delle possibili intrecci e monitoraggi di ogni metodo che succede per accedere al intvalore. È molto più difficile violare accidentalmente la sicurezza del thread durante la chiamata getAndIncrement()rispetto a quando si ritorna i++e si ricorda (o no) di acquisire in anticipo il set corretto di monitor.


2
Grazie per questa chiara spiegazione. Quali sarebbero i vantaggi dell'utilizzo di AtomicInteger rispetto a una classe in cui i metodi sono tutti sincronizzati? Quest'ultimo sarebbe considerato "più pesante"?
James P.

3
Dal mio punto di vista, è principalmente l'incapsulamento che ottieni con AtomicIntegers: la sincronizzazione avviene esattamente su ciò di cui hai bisogno e ottieni metodi descrittivi che si trovano nell'API pubblica per spiegare qual è il risultato desiderato. (Inoltre, in una certa misura hai ragione, spesso si finirebbe semplicemente per sincronizzare tutti i metodi in una classe che è probabilmente a grana grossa, sebbene con HotSpot esegua ottimizzazioni di blocco e le regole contro l'ottimizzazione prematura, considero la leggibilità una maggiore vantaggio rispetto alle prestazioni.)
Andrzej Doyle,

Questa è una spiegazione molto chiara e precisa, grazie !!
Akash5288,

Finalmente una spiegazione che mi ha chiarito correttamente.
Benny Bottema,

58

Se osservi i metodi di AtomicInteger, noterai che tendono a corrispondere alle operazioni comuni sugli ints. Per esempio:

static AtomicInteger i;

// Later, in a thread
int current = i.incrementAndGet();

è la versione thread-safe di questo:

static int i;

// Later, in a thread
int current = ++i;

La mappa dei metodi in questo modo:
++iis i.incrementAndGet()
i++is i.getAndIncrement()
--iis i.decrementAndGet()
i--is i.getAndDecrement()
i = xis i.set(x)
x = iisx = i.get()

Esistono anche altri metodi di praticità, come compareAndSetoaddAndGet


37

L'uso principale di AtomicIntegerè quando ci si trova in un contesto multithread ed è necessario eseguire operazioni thread-safe su un numero intero senza utilizzare synchronized. L'assegnazione e il recupero sul tipo primitivo intsono già atomici ma vengono AtomicIntegerforniti con molte operazioni che non sono atomiche int.

I più semplici sono getAndXXXo xXXAndGet. Ad esempio getAndIncrement()è un equivalente atomico al i++quale non è atomico perché in realtà è una scorciatoia per tre operazioni: recupero, addizione e assegnazione. compareAndSetè molto utile per implementare semafori, lucchetti, chiavistelli, ecc.

L'uso di AtomicIntegerè più veloce e più leggibile che eseguire lo stesso utilizzando la sincronizzazione.

Un semplice test:

public synchronized int incrementNotAtomic() {
    return notAtomic++;
}

public void performTestNotAtomic() {
    final long start = System.currentTimeMillis();
    for (int i = 0 ; i < NUM ; i++) {
        incrementNotAtomic();
    }
    System.out.println("Not atomic: "+(System.currentTimeMillis() - start));
}

public void performTestAtomic() {
    final long start = System.currentTimeMillis();
    for (int i = 0 ; i < NUM ; i++) {
        atomic.getAndIncrement();
    }
    System.out.println("Atomic: "+(System.currentTimeMillis() - start));
}

Sul mio PC con Java 1.6 il test atomico viene eseguito in 3 secondi mentre quello sincronizzato viene eseguito in circa 5,5 secondi. Il problema qui è che l'operazione di sincronizzazione ( notAtomic++) è davvero breve. Quindi il costo della sincronizzazione è davvero importante rispetto all'operazione.

Oltre all'atomicità AtomicInteger può essere utilizzato come versione mutabile, Integerad esempio, in Maps come valori.


1
Non penso che vorrei usare AtomicIntegercome chiave della mappa, perché utilizza l' equals()implementazione predefinita , che non è quasi certamente quella che ti aspetteresti che la semantica fosse usata in una mappa.
Andrzej Doyle,

1
@Andrzej certo, non come chiave che deve essere immutabile ma un valore.
gabuzo,

@gabuzo Qualche idea sul perché l'intero atomico funzioni ben oltre la sincronizzazione?
Supun Wijerathne,

I test sono piuttosto vecchi ora (più di 6 anni), potrebbe essere interessante ripetere il test con un JRE recente. Non ho approfondito abbastanza AtomicInteger per rispondere, ma poiché si tratta di un'attività molto specifica, utilizzerà tecniche di sincronizzazione che funzionano solo in questo caso specifico. Ricorda anche che il test è monotread e fare un test simile in un ambiente pesantemente caricato potrebbe non dare una vittoria così chiara per AtomicInteger
gabuzo

Credo che sia 3 ms e 5,5 ms
Sathesh,

18

Ad esempio, ho una libreria che genera istanze di qualche classe. Ognuna di queste istanze deve avere un ID intero univoco, poiché queste istanze rappresentano comandi inviati a un server e ogni comando deve avere un ID univoco. Poiché a più thread è consentito inviare comandi contemporaneamente, utilizzo AtomicInteger per generare tali ID. Un approccio alternativo sarebbe quello di utilizzare una sorta di blocco e un numero intero normale, ma è sia più lento che meno elegante.


Grazie per aver condiviso questo esempio pratico. Sembra qualcosa che dovrei usare in quanto ho bisogno di avere un ID univoco per ogni file che importa nel mio programma :)
James P.

7

Come ha detto gabuzo, a volte uso AtomicIntegers quando voglio passare un int per riferimento. È una classe integrata che ha un codice specifico per l'architettura, quindi è più facile e probabilmente più ottimizzata di qualsiasi MutableInteger che potrei codificare rapidamente. Detto questo, sembra un abuso della classe.


7

In Java 8 le classi atomiche sono state estese con due funzioni interessanti:

  • int getAndUpdate (IntUnaryOperator updateFunction)
  • int updateAndGet (IntUnaryOperator updateFunction)

Entrambi utilizzano updateFunction per eseguire l'aggiornamento del valore atomico. La differenza è che il primo restituisce il vecchio valore e il secondo restituisce il nuovo valore. UpdateFunction può essere implementato per eseguire operazioni di "confronto e impostazione" più complesse rispetto a quella standard. Ad esempio, può verificare che il contatore atomico non scenda al di sotto dello zero, normalmente richiederebbe la sincronizzazione, e qui il codice è senza blocco:

    public class Counter {

      private final AtomicInteger number;

      public Counter(int number) {
        this.number = new AtomicInteger(number);
      }

      /** @return true if still can decrease */
      public boolean dec() {
        // updateAndGet(fn) executed atomically:
        return number.updateAndGet(n -> (n > 0) ? n - 1 : n) > 0;
      }
    }

Il codice è preso dall'esempio atomico Java .


5

Di solito uso AtomicInteger quando devo fornire Id agli oggetti che possono essere acceduti o creati da più thread e di solito lo uso come un attributo statico sulla classe a cui accedo nel costruttore degli oggetti.


4

È possibile implementare blocchi non bloccanti utilizzando compareAndSwap (CAS) su numeri interi o long atomici. Il documento "Tl2" Software Transactional Memory descrive questo:

Associamo un blocco di scrittura con versione speciale a ogni posizione di memoria eseguita. Nella sua forma più semplice, il lock-write con versione è uno spinlock a parola singola che utilizza un'operazione CAS per acquisire il lock e un archivio per rilasciarlo. Dal momento che è necessario solo un singolo bit per indicare che il blocco è attivo, usiamo il resto della parola di blocco per contenere un numero di versione.

Quello che sta descrivendo è prima di leggere l'intero atomico. Dividilo in un bit di blocco ignorato e il numero di versione. Tentare di CAS scrivere come il bit di blocco cancellato con il numero di versione corrente sul set di bit di blocco e il numero di versione successivo. Ripeti fino a quando non ci riesci e sei il thread che possiede il blocco. Sbloccare impostando il numero della versione corrente con il bit di blocco cancellato. Il documento descrive l'utilizzo dei numeri di versione nei blocchi per coordinare il fatto che i thread hanno una serie coerente di letture quando scrivono.

Questo articolo descrive che i processori hanno il supporto hardware per operazioni di confronto e scambio che rendono molto efficiente. Afferma inoltre:

i contatori basati su CAS non bloccanti che utilizzano variabili atomiche hanno prestazioni migliori rispetto ai contatori basati su blocco in contesa da bassa a moderata


3

La chiave è che consentono l'accesso simultaneo e la modifica in modo sicuro. Sono comunemente usati come contatori in un ambiente multithread - prima della loro introduzione doveva essere una classe scritta dall'utente che racchiudeva i vari metodi in blocchi sincronizzati.


Vedo. È questo nei casi in cui un attributo o un'istanza agisce come una sorta di variabile globale all'interno di un'applicazione. O ci sono altri casi a cui puoi pensare?
James P.

1

Ho usato AtomicInteger per risolvere il problema di Dining Philosopher.

Nella mia soluzione, le istanze AtomicInteger sono state usate per rappresentare le forcelle, ce ne sono due necessarie per filosofo. Ogni filosofo viene identificato come un numero intero, da 1 a 5. Quando un fork viene utilizzato da un filosofo, AtomicInteger contiene il valore del filosofo, da 1 a 5, altrimenti il ​​fork non viene utilizzato, quindi il valore di AtomicInteger è -1 .

AtomicInteger consente quindi di verificare se un fork è libero, valore == - 1 e impostarlo sul proprietario del fork se libero, in un'unica operazione atomica. Vedi il codice sotto.

AtomicInteger fork0 = neededForks[0];//neededForks is an array that holds the forks needed per Philosopher
AtomicInteger fork1 = neededForks[1];
while(true){    
    if (Hungry) {
        //if fork is free (==-1) then grab it by denoting who took it
        if (!fork0.compareAndSet(-1, p) || !fork1.compareAndSet(-1, p)) {
          //at least one fork was not succesfully grabbed, release both and try again later
            fork0.compareAndSet(p, -1);
            fork1.compareAndSet(p, -1);
            try {
                synchronized (lock) {//sleep and get notified later when a philosopher puts down one fork                    
                    lock.wait();//try again later, goes back up the loop
                }
            } catch (InterruptedException e) {}

        } else {
            //sucessfully grabbed both forks
            transition(fork_l_free_and_fork_r_free);
        }
    }
}

Poiché il metodo compareAndSet non si blocca, dovrebbe aumentare la velocità effettiva, più lavoro svolto. Come forse saprai, il problema Dining Philosophers viene utilizzato quando è necessario un accesso controllato alle risorse, ovvero forchette, come un processo ha bisogno di risorse per continuare a lavorare.


0

Semplice esempio per la funzione compareAndSet ():

import java.util.concurrent.atomic.AtomicInteger; 

public class GFG { 
    public static void main(String args[]) 
    { 

        // Initially value as 0 
        AtomicInteger val = new AtomicInteger(0); 

        // Prints the updated value 
        System.out.println("Previous value: "
                           + val); 

        // Checks if previous value was 0 
        // and then updates it 
        boolean res = val.compareAndSet(0, 6); 

        // Checks if the value was updated. 
        if (res) 
            System.out.println("The value was"
                               + " updated and it is "
                           + val); 
        else
            System.out.println("The value was "
                               + "not updated"); 
      } 
  } 

La stampa è: valore precedente: 0 Il valore è stato aggiornato ed è 6 Un altro semplice esempio:

    import java.util.concurrent.atomic.AtomicInteger; 

public class GFG { 
    public static void main(String args[]) 
    { 

        // Initially value as 0 
        AtomicInteger val 
            = new AtomicInteger(0); 

        // Prints the updated value 
        System.out.println("Previous value: "
                           + val); 

         // Checks if previous value was 0 
        // and then updates it 
        boolean res = val.compareAndSet(10, 6); 

          // Checks if the value was updated. 
          if (res) 
            System.out.println("The value was"
                               + " updated and it is "
                               + val); 
        else
            System.out.println("The value was "
                               + "not updated"); 
    } 
} 

La stampa è: Valore precedente: 0 Il valore non è stato aggiornato

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.