2011-01-27 9 views

risposta

148

Ci sono due usi principali di AtomicInteger:

  • Come un contatore atomica (incrementAndGet(), ecc) che può essere utilizzato da molti fili contemporaneamente

  • Come primitiva che supporta compare-and-swap istruzione (compareAndSet()) per implementare algoritmi non bloccanti.

    Ecco un esempio di non-blocking generatore di numeri casuali da Brian Göetz's Java Concurrency In Practice:

    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 si può vedere, esso funziona praticamente quasi nello stesso modo come incrementAndGet(), ma esegui calcolo arbitrario (calculateNext()) invece di incremento (e elabora il risultato prima del ritorno).

+1

Penso di aver capito il primo utilizzo. Questo per assicurarsi che il contatore sia stato incrementato prima di accedere nuovamente a un attributo. Corretta? Potresti fare un breve esempio per il secondo utilizzo? –

+4

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

+3

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

1

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

+0

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

80

L'esempio più semplice che posso pensare è di incrementare un'operazione atomica.

Con int 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 mutazioni (specialmente contando, o unico indicizzazione), senza dover ricorrere alla sincronizzazione di tutti gli accessi.

logica privo di sincronizzazione Più complesso può essere impiegato utilizzando compareAndSet() come un tipo di blocco ottimistico - ottenere il valore corrente, risultato di calcolo sulla base di questo, impostare questo risultato sse valore è ancora l'ingresso utilizzato per eseguire il calcolo , altrimenti ricominciare - ma gli esempi di conteggio sono molto utili e userò spesso AtomicIntegers per il conteggio e generatori unici a livello di VM se c'è qualche suggerimento di più thread coinvolti, perché sono così facili da lavorare con I'd quasi considerare l'ottimizzazione prematura per usare il semplice ints.

Mentre è quasi sempre possibile ottenere le stesse garanzie di sincronizzazione con ints ed appropriate synchronized dichiarazioni, la bellezza di AtomicInteger è che il filo di sicurezza è incorporato nella all'oggetto vero e proprio, piuttosto che doversi preoccupare di possibili interleavings, e monitorati, di tutti i metodi che accedono al valore int. È molto più difficile violare accidentalmente il threadsafety quando si chiama getAndIncrement() di quando si restituisce i++ e si ricorda (o meno) di acquisire in anticipo il set corretto di monitor.

+2

Grazie per questa chiara spiegazione. Quali sarebbero i vantaggi dell'utilizzo di un oggetto AtomicInteger su una classe in cui i metodi sono tutti sincronizzati? Questo sarebbe considerato come "più pesante"? –

+3

Dal mio punto di vista, è principalmente l'incapsulamento che si ottiene con AtomicIntegers: la sincronizzazione avviene esattamente su ciò che è necessario e si ottengono metodi descrittivi che sono nell'API pubblica per spiegare quale sia il risultato desiderato. (In più hai ragione, spesso si finirebbe semplicemente sincronizzando tutti i metodi in una classe che è probabilmente troppo grossolana, anche se con HotSpot che esegue l'ottimizzazione del blocco e le regole contro l'ottimizzazione prematura, ritengo che la leggibilità sia un un vantaggio maggiore delle prestazioni.) –

+0

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

13

Ad esempio, ho una libreria che genera istanze di qualche classe. Ciascuna di queste istanze deve avere un ID intero univoco, poiché queste istanze rappresentano i comandi inviati a un server e ogni comando deve avere un ID univoco.Poiché più thread sono autorizzati a inviare comandi contemporaneamente, io uso un AtomicInteger per generare quegli ID. Un approccio alternativo sarebbe utilizzare una sorta di blocco e un numero intero regolare, ma è più lento e meno elegante.

+0

Grazie per aver condiviso questo esempio pratico. Sembra una cosa che dovrei usare in quanto ho bisogno di avere un ID univoco per ogni file che impongo nel mio programma :) –

29

L'utilizzo principale di AtomicInteger è quando si è in un contesto con multithreading ed è necessario eseguire operazioni di sicurezza thread su un numero intero senza utilizzare synchronized. L'assegnazione e il recupero sul tipo primitivo int sono già atomici ma AtomicInteger viene fornito con molte operazioni che non sono atomiche su int.

I più semplici sono getAndXXX o xXXAndGet. Ad esempio, getAndIncrement() è un equivalente atomico a i++ che non è atomico perché è in realtà una scorciatoia per tre operazioni: recupero, aggiunta e assegnazione. compareAndSet è molto utile per implementare semafori, serrature, fermi, ecc.

L'utilizzo di AtomicInteger è più veloce e più leggibile rispetto all'utilizzo dello stesso tramite 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 viene eseguito il test atomico in 3 secondi, mentre quello sincronizzato viene eseguito in circa 5,5 secondi. Il problema qui è che l'operazione di sincronizzazione (notAtomic++) è veramente breve. Quindi il costo della sincronizzazione è molto importante rispetto all'operazione.

Beside atomicity AtomicInteger può essere utilizzato come versione mutabile di Integer per esempio in Map s come valori.

+1

Non penso che vorrei usare 'AtomicInteger' come una chiave di mappa, perché usa l'impostazione predefinita' equals() 'implementazione, che non è quasi certamente quello che ci si aspetterebbe dalla semantica se usata in una mappa. –

+1

@Andrzej sicuro, non come chiave che deve essere immutabile ma un valore. – gabuzo

+0

@gabuzo Qualche idea sul perché il numero intero atomico funzioni bene rispetto alla sincronizzazione? –

49

Se si osservano i metodi di AtomicInteger, si noterà che tendono a corrispondere alle operazioni comuni sugli interi. 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; 

I metodi mappa simili:
++i è i.incrementAndGet()
i++ è i.getAndIncrement()
--i è i.decrementAndGet()
i-- è i.getAndDecrement()
i = x è i.set(x)
x = i è x = i.get()

ci sono altri metodi di convenienza pure, come compareAndSet o addAndGet

5

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

3

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

Associamo uno speciale controllo di versione write-lock con ogni transato posizione di memoria. Nella sua forma più semplice, il blocco scrittura versione è uno spinlock a parola singola che utilizza un'operazione CAS per acquisire il blocco e lo store per il rilascio. Dal momento che è sufficiente un solo bit per indicare per il blocco, usiamo il resto della parola di blocco per contenere un numero di versione .

Quello che viene descritto è la prima lettura del numero intero atomico. Dividi questo in un lock-bit ignorato e il numero di versione. Tentativo di CAS scriverlo come lock-bit cancellato con il numero di versione corrente sul set di lock-bit e sul numero di versione successivo. Fai il ciclo fino a quando non ci riesci e sei il filo che possiede il lucchetto. Sbloccare impostando il numero di versione corrente con il bit di blocco deselezionato. Il documento descrive l'uso dei numeri di versione nei blocchi per coordinare i thread che hanno un insieme coerente di letture quando scrivono.

This article descrive che i processori dispongono di supporto hardware per operazioni di confronto e scambio che rendono molto efficienti. Essa sostiene anche:

non-blocking contatori CAS-based utilizzando le variabili atomiche hanno prestazioni migliori di contatori di lock-base in basso a moderato contesa

4

io di solito uso AtomicInteger quando ho bisogno di dare Ids agli oggetti che possono essere aggiunti o creati da più thread e di solito li uso come attributo statico sulla classe a cui accedo nel costruttore degli oggetti.

5

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

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

Entrambi utilizzano l'updateFunction a eseguire l'aggiornamento del valore atomico. La differenza è che il primo restituisce il vecchio valore e il secondo restituisce il nuovo valore. La funzione update può essere implementata per eseguire operazioni di "confronto e impostazione" più complesse rispetto a quella standard. Ad esempio si può verificare che contatore atomico non va sotto zero, normalmente richiederebbe sincronizzazione, e qui il codice è-lock gratis:

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 viene preso da Java Atomic Example.

Problemi correlati