Risposte:
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).
read
e 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.
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 AtomicIntegers
per 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 ints
e synchronized
dichiarazioni 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 int
valore. È 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.
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:
++i
is i.incrementAndGet()
i++
is i.getAndIncrement()
--i
is i.decrementAndGet()
i--
is i.getAndDecrement()
i = x
is i.set(x)
x = i
isx = i.get()
Esistono anche altri metodi di praticità, come compareAndSet
oaddAndGet
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 int
sono già atomici ma vengono AtomicInteger
forniti con molte operazioni che non sono atomiche int
.
I più semplici sono getAndXXX
o 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, Integer
ad esempio, in Map
s come valori.
AtomicInteger
come 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.
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.
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.
In Java 8 le classi atomiche sono state estese con due funzioni interessanti:
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 .
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.
È 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
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.
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.
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