Variabili Atomiche
Abbiamo già trattato cosa sia l’atomicità e i problemi che può causare nella prima sezione di questo corso. In quel contesto, abbiamo affrontato la questione utilizzando blocchi o metodi sincronizzati. Ora, esamineremo come ottenere lo stesso risultato in modo più semplice utilizzando una classe atomica.
Cosa sono le Variabili Atomiche?
Le variabili atomiche garantiscono che le operazioni (lettura, scrittura, incremento) sulle variabili vengano eseguite atomicamente, ovvero in modo contiguo e sicuro in un ambiente multithread. Questo assicura che l’operazione venga completata interamente, senza la possibilità di interferenze da parte di altri thread durante la sua esecuzione.
Perché abbiamo bisogno delle variabili atomiche?
Senza l'utilizzo di variabili atomiche o altri meccanismi di sincronizzazione, operazioni come incremento (++) possono risultare non sicure. Ad esempio, quando più thread accedono contemporaneamente alla stessa variabile, alcuni aggiornamenti potrebbero andare persi, causando risultati errati. Le variabili atomiche risolvono questo problema garantendo che le operazioni su di esse vengano eseguite sequenzialmente.
Abbiamo già discusso di questo problema quando l'operazione di incremento è stata suddivisa in tre passaggi (lettura, incremento, scrittura), ma con le variabili atomiche tutto avviene in un'unica operazione!
Tipi di variabili atomiche in Java
In generale, esistono molte implementazioni atomiche e non le tratteremo tutte qui, poiché richiederebbe troppo tempo.
Java fornisce diverse classi di variabili atomiche nel package java.util.concurrent.atomic, ciascuna progettata per gestire un tipo di dato specifico:
AtomicInteger: per operazioni atomiche su int;AtomicLong: per operazioni atomiche su long;AtomicBoolean: per operazioni atomiche su boolean;AtomicReference<V>: per operazioni atomiche su oggetti (tipo generico).
Metodi
Il metodo get() restituisce il valore corrente di una variabile. Il metodo set(V newValue) imposta un nuovo valore per la variabile. Invece, lazySet(V newValue) è simile a set(), ma può differire l'aggiornamento del valore, offrendo un aggiornamento ordinato in determinate situazioni.
Main.java
123456789101112131415161718192021package com.example; import java.util.concurrent.atomic.AtomicReference; public class Main { public static void main(String[] args) { AtomicReference<String> atomicString = new AtomicReference<>("Initial Value"); // Using `get()` to retrieve the current value String value = atomicString.get(); System.out.println("Current Value: " + value); // Using `set()` to update the value atomicString.set("New Value"); System.out.println("Value after set(): " + atomicString.get()); // Using `lazySet()` to update the value atomicString.lazySet("Lazy Set Value"); System.out.println("Value after lazySet(): " + atomicString.get()); } }
Il metodo compareAndSet(V expect, V update) aggiorna il valore se il valore attuale corrisponde al valore atteso. Restituisce true se l'aggiornamento è stato effettuato con successo, e false se il valore attuale non corrispondeva al valore atteso. Al contrario, il metodo getAndSet(V newValue) imposta un nuovo valore e restituisce il valore precedente.
Main.java
123456789101112131415161718192021222324package com.example; import java.util.concurrent.atomic.AtomicReference; public class Main { public static void main(String[] args) { // Initialize an `AtomicReference` with an initial value AtomicReference<String> atomicString = new AtomicReference<>("Initial Value"); // Demonstrate `compareAndSet` boolean success = atomicString.compareAndSet("Initial Value", "Updated Value"); System.out.println("compareAndSet success (expected true): " + success); System.out.println("Current value after compareAndSet: " + atomicString.get()); success = atomicString.compareAndSet("Wrong Value", "Another Update"); System.out.println("compareAndSet success (expected false): " + success); System.out.println("Current value after compareAndSet: " + atomicString.get()); // Demonstrate `getAndSet` String previousValue = atomicString.getAndSet("New Value with getAndSet"); System.out.println("Previous value from getAndSet: " + previousValue); System.out.println("Current value after getAndSet: " + atomicString.get()); } }
I metodi getAndIncrement() e getAndDecrement() incrementano o decrementano il valore corrente di uno e restituiscono il valore precedente. Questi metodi si applicano alle variabili atomiche numeriche come AtomicInteger e AtomicLong. Al contrario, i metodi incrementAndGet() e decrementAndGet() incrementano o decrementano anch'essi il valore corrente di uno ma restituiscono il nuovo valore.
Main.java
123456789101112131415161718192021222324252627282930package com.example; import java.util.concurrent.atomic.AtomicInteger; public class Main { public static void main(String[] args) { // Initialize `AtomicInteger` with initial value AtomicInteger atomicInt = new AtomicInteger(10); // Demonstrate `getAndIncrement()` for `AtomicInteger` int oldValueInt = atomicInt.getAndIncrement(); System.out.println("Value before getAndIncrement(): " + oldValueInt); // Should print 10 System.out.println("Value after getAndIncrement(): " + atomicInt.get()); // Should print 11 // Demonstrate `getAndDecrement()` for `AtomicInteger` int oldValueIntDec = atomicInt.getAndDecrement(); System.out.println("Value before getAndDecrement(): " + oldValueIntDec); // Should print 11 System.out.println("Value after getAndDecrement(): " + atomicInt.get()); // Should print 10 // Demonstrate `incrementAndGet()` for `AtomicInteger` int newValueInt = atomicInt.incrementAndGet(); System.out.println("Value after incrementAndGet(): " + newValueInt); // Should print 11 System.out.println("Current value after incrementAndGet(): " + atomicInt.get()); // Should print 11 // Demonstrate `decrementAndGet()` for `AtomicInteger` int newValueIntDec = atomicInt.decrementAndGet(); System.out.println("Value after decrementAndGet(): " + newValueIntDec); // Should print 10 System.out.println("Current value after decrementAndGet(): " + atomicInt.get()); // Should print 10 } }
Il metodo getAndAdd(int delta) aggiunge il valore specificato (delta) al valore corrente e restituisce il valore precedente. Questo metodo viene utilizzato con variabili atomiche numeriche. D'altra parte, il metodo addAndGet(int delta) aggiunge anch'esso il valore specificato (delta) al valore corrente ma restituisce il nuovo valore.
Main.java
123456789101112131415161718192021222324252627282930package com.example; import java.util.concurrent.atomic.AtomicInteger; public class Main { public static void main(String[] args) { // Initialize `AtomicInteger` with an initial value AtomicInteger atomicInt = new AtomicInteger(50); // Demonstrate `getAndAdd(int delta)` int previousValue = atomicInt.getAndAdd(10); System.out.println("Value before getAndAdd(10): " + previousValue); // Should print 50 System.out.println("Value after getAndAdd(10): " + atomicInt.get()); // Should print 60 // Demonstrate `getAndAdd()` with another delta int previousValue2 = atomicInt.getAndAdd(5); System.out.println("Value before getAndAdd(5): " + previousValue2); // Should print 60 System.out.println("Value after getAndAdd(5): " + atomicInt.get()); // Should print 65 // Demonstrate `addAndGet(int delta)` int newValue = atomicInt.addAndGet(20); System.out.println("Value after addAndGet(20): " + newValue); // Should print 85 System.out.println("Current value after addAndGet(20): " + atomicInt.get()); // Should print 85 // Demonstrate `addAndGet()` with another delta int newValue2 = atomicInt.addAndGet(-15); System.out.println("Value after addAndGet(-15): " + newValue2); // Should print 70 System.out.println("Current value after addAndGet(-15): " + atomicInt.get()); // Should print 70 } }
Esempi di utilizzo delle variabili atomiche
Esempio con AtomicInteger (AtomicLong, AtomicBoolean sono analoghi, quindi non verranno forniti esempi separati per essi).
Obiettivo: Implementazione di un contatore che viene incrementato in modo sicuro da più thread.
Main.java
1234567891011121314151617181920package com.example; import java.util.concurrent.atomic.AtomicInteger; public class Main { private AtomicInteger counter = new AtomicInteger(0); public void increment() { int oldValue = counter.getAndIncrement(); System.out.println(Thread.currentThread().getName() + ": Counter was " + oldValue + ", now " + counter.get()); } public static void main(String[] args) { Main atomicCounter = new Main(); for (int i = 0; i < 5; i++) { new Thread(atomicCounter::increment).start(); } } }
Come puoi vedere, non è stata utilizzata alcuna sincronizzazione qui, poiché la variabile atomica stessa fornisce questa funzione.
Questo esempio utilizza AtomicInteger per incrementare in modo sicuro il contatore. Il metodo getAndIncrement() restituisce prima il valore attuale della variabile, quindi lo incrementa di uno. Questo avviene atomicamente, garantendo che la variabile venga aggiornata correttamente.
Esempio con AtomicReference
Compito: Fornire aggiornamento atomico di un riferimento a un oggetto.
Main.java
123456789101112131415161718192021222324package com.example; import java.util.concurrent.atomic.AtomicReference; public class Main { // `AtomicReference` to safely update and access a shared `String` value private AtomicReference<String> sharedString = new AtomicReference<>("Initial"); public void updateValue(String newValue) { // Atomically sets the new value and gets the old value String oldValue = sharedString.getAndSet(newValue); // Prints the old and new values, along with the thread name System.out.println(Thread.currentThread().getName() + ": Value was " + oldValue + ", now " + sharedString.get()); } public static void main(String[] args) { Main example = new Main(); // Creates and starts 3 threads, each updating the shared value for (int i = 0; i < 3; i++) { new Thread(() -> example.updateValue("Updated by " + Thread.currentThread().getName())).start(); } } }
AtomicReference viene utilizzato qui per aggiornare atomicamente il valore di un riferimento a stringa. Il metodo getAndSet() imposta atomicamente il nuovo valore e restituisce il valore precedente.
A differenza delle variabili normali, che richiedono una sincronizzazione aggiuntiva, le variabili atomiche utilizzano primitive di basso livello per ridurre al minimo l'overhead e migliorare le prestazioni. Questo le rende particolarmente adatte per sistemi ad alta concorrenza.
1. Qual è il vantaggio dell'utilizzo delle variabili atomiche nella programmazione multithread?
2. Quale metodo delle variabili atomiche fornisce una modifica atomica del valore se il valore corrente è uguale a quello atteso?
3. Cosa garantisce il metodo set() nelle variabili atomiche?
Grazie per i tuoi commenti!
Chieda ad AI
Chieda ad AI
Chieda pure quello che desidera o provi una delle domande suggerite per iniziare la nostra conversazione
Can you explain how atomic variables differ from using synchronized blocks?
What are some real-world scenarios where atomic variables are especially useful?
Could you provide a simple code example using AtomicInteger?
Awesome!
Completion rate improved to 3.33
Variabili Atomiche
Scorri per mostrare il menu
Abbiamo già trattato cosa sia l’atomicità e i problemi che può causare nella prima sezione di questo corso. In quel contesto, abbiamo affrontato la questione utilizzando blocchi o metodi sincronizzati. Ora, esamineremo come ottenere lo stesso risultato in modo più semplice utilizzando una classe atomica.
Cosa sono le Variabili Atomiche?
Le variabili atomiche garantiscono che le operazioni (lettura, scrittura, incremento) sulle variabili vengano eseguite atomicamente, ovvero in modo contiguo e sicuro in un ambiente multithread. Questo assicura che l’operazione venga completata interamente, senza la possibilità di interferenze da parte di altri thread durante la sua esecuzione.
Perché abbiamo bisogno delle variabili atomiche?
Senza l'utilizzo di variabili atomiche o altri meccanismi di sincronizzazione, operazioni come incremento (++) possono risultare non sicure. Ad esempio, quando più thread accedono contemporaneamente alla stessa variabile, alcuni aggiornamenti potrebbero andare persi, causando risultati errati. Le variabili atomiche risolvono questo problema garantendo che le operazioni su di esse vengano eseguite sequenzialmente.
Abbiamo già discusso di questo problema quando l'operazione di incremento è stata suddivisa in tre passaggi (lettura, incremento, scrittura), ma con le variabili atomiche tutto avviene in un'unica operazione!
Tipi di variabili atomiche in Java
In generale, esistono molte implementazioni atomiche e non le tratteremo tutte qui, poiché richiederebbe troppo tempo.
Java fornisce diverse classi di variabili atomiche nel package java.util.concurrent.atomic, ciascuna progettata per gestire un tipo di dato specifico:
AtomicInteger: per operazioni atomiche su int;AtomicLong: per operazioni atomiche su long;AtomicBoolean: per operazioni atomiche su boolean;AtomicReference<V>: per operazioni atomiche su oggetti (tipo generico).
Metodi
Il metodo get() restituisce il valore corrente di una variabile. Il metodo set(V newValue) imposta un nuovo valore per la variabile. Invece, lazySet(V newValue) è simile a set(), ma può differire l'aggiornamento del valore, offrendo un aggiornamento ordinato in determinate situazioni.
Main.java
123456789101112131415161718192021package com.example; import java.util.concurrent.atomic.AtomicReference; public class Main { public static void main(String[] args) { AtomicReference<String> atomicString = new AtomicReference<>("Initial Value"); // Using `get()` to retrieve the current value String value = atomicString.get(); System.out.println("Current Value: " + value); // Using `set()` to update the value atomicString.set("New Value"); System.out.println("Value after set(): " + atomicString.get()); // Using `lazySet()` to update the value atomicString.lazySet("Lazy Set Value"); System.out.println("Value after lazySet(): " + atomicString.get()); } }
Il metodo compareAndSet(V expect, V update) aggiorna il valore se il valore attuale corrisponde al valore atteso. Restituisce true se l'aggiornamento è stato effettuato con successo, e false se il valore attuale non corrispondeva al valore atteso. Al contrario, il metodo getAndSet(V newValue) imposta un nuovo valore e restituisce il valore precedente.
Main.java
123456789101112131415161718192021222324package com.example; import java.util.concurrent.atomic.AtomicReference; public class Main { public static void main(String[] args) { // Initialize an `AtomicReference` with an initial value AtomicReference<String> atomicString = new AtomicReference<>("Initial Value"); // Demonstrate `compareAndSet` boolean success = atomicString.compareAndSet("Initial Value", "Updated Value"); System.out.println("compareAndSet success (expected true): " + success); System.out.println("Current value after compareAndSet: " + atomicString.get()); success = atomicString.compareAndSet("Wrong Value", "Another Update"); System.out.println("compareAndSet success (expected false): " + success); System.out.println("Current value after compareAndSet: " + atomicString.get()); // Demonstrate `getAndSet` String previousValue = atomicString.getAndSet("New Value with getAndSet"); System.out.println("Previous value from getAndSet: " + previousValue); System.out.println("Current value after getAndSet: " + atomicString.get()); } }
I metodi getAndIncrement() e getAndDecrement() incrementano o decrementano il valore corrente di uno e restituiscono il valore precedente. Questi metodi si applicano alle variabili atomiche numeriche come AtomicInteger e AtomicLong. Al contrario, i metodi incrementAndGet() e decrementAndGet() incrementano o decrementano anch'essi il valore corrente di uno ma restituiscono il nuovo valore.
Main.java
123456789101112131415161718192021222324252627282930package com.example; import java.util.concurrent.atomic.AtomicInteger; public class Main { public static void main(String[] args) { // Initialize `AtomicInteger` with initial value AtomicInteger atomicInt = new AtomicInteger(10); // Demonstrate `getAndIncrement()` for `AtomicInteger` int oldValueInt = atomicInt.getAndIncrement(); System.out.println("Value before getAndIncrement(): " + oldValueInt); // Should print 10 System.out.println("Value after getAndIncrement(): " + atomicInt.get()); // Should print 11 // Demonstrate `getAndDecrement()` for `AtomicInteger` int oldValueIntDec = atomicInt.getAndDecrement(); System.out.println("Value before getAndDecrement(): " + oldValueIntDec); // Should print 11 System.out.println("Value after getAndDecrement(): " + atomicInt.get()); // Should print 10 // Demonstrate `incrementAndGet()` for `AtomicInteger` int newValueInt = atomicInt.incrementAndGet(); System.out.println("Value after incrementAndGet(): " + newValueInt); // Should print 11 System.out.println("Current value after incrementAndGet(): " + atomicInt.get()); // Should print 11 // Demonstrate `decrementAndGet()` for `AtomicInteger` int newValueIntDec = atomicInt.decrementAndGet(); System.out.println("Value after decrementAndGet(): " + newValueIntDec); // Should print 10 System.out.println("Current value after decrementAndGet(): " + atomicInt.get()); // Should print 10 } }
Il metodo getAndAdd(int delta) aggiunge il valore specificato (delta) al valore corrente e restituisce il valore precedente. Questo metodo viene utilizzato con variabili atomiche numeriche. D'altra parte, il metodo addAndGet(int delta) aggiunge anch'esso il valore specificato (delta) al valore corrente ma restituisce il nuovo valore.
Main.java
123456789101112131415161718192021222324252627282930package com.example; import java.util.concurrent.atomic.AtomicInteger; public class Main { public static void main(String[] args) { // Initialize `AtomicInteger` with an initial value AtomicInteger atomicInt = new AtomicInteger(50); // Demonstrate `getAndAdd(int delta)` int previousValue = atomicInt.getAndAdd(10); System.out.println("Value before getAndAdd(10): " + previousValue); // Should print 50 System.out.println("Value after getAndAdd(10): " + atomicInt.get()); // Should print 60 // Demonstrate `getAndAdd()` with another delta int previousValue2 = atomicInt.getAndAdd(5); System.out.println("Value before getAndAdd(5): " + previousValue2); // Should print 60 System.out.println("Value after getAndAdd(5): " + atomicInt.get()); // Should print 65 // Demonstrate `addAndGet(int delta)` int newValue = atomicInt.addAndGet(20); System.out.println("Value after addAndGet(20): " + newValue); // Should print 85 System.out.println("Current value after addAndGet(20): " + atomicInt.get()); // Should print 85 // Demonstrate `addAndGet()` with another delta int newValue2 = atomicInt.addAndGet(-15); System.out.println("Value after addAndGet(-15): " + newValue2); // Should print 70 System.out.println("Current value after addAndGet(-15): " + atomicInt.get()); // Should print 70 } }
Esempi di utilizzo delle variabili atomiche
Esempio con AtomicInteger (AtomicLong, AtomicBoolean sono analoghi, quindi non verranno forniti esempi separati per essi).
Obiettivo: Implementazione di un contatore che viene incrementato in modo sicuro da più thread.
Main.java
1234567891011121314151617181920package com.example; import java.util.concurrent.atomic.AtomicInteger; public class Main { private AtomicInteger counter = new AtomicInteger(0); public void increment() { int oldValue = counter.getAndIncrement(); System.out.println(Thread.currentThread().getName() + ": Counter was " + oldValue + ", now " + counter.get()); } public static void main(String[] args) { Main atomicCounter = new Main(); for (int i = 0; i < 5; i++) { new Thread(atomicCounter::increment).start(); } } }
Come puoi vedere, non è stata utilizzata alcuna sincronizzazione qui, poiché la variabile atomica stessa fornisce questa funzione.
Questo esempio utilizza AtomicInteger per incrementare in modo sicuro il contatore. Il metodo getAndIncrement() restituisce prima il valore attuale della variabile, quindi lo incrementa di uno. Questo avviene atomicamente, garantendo che la variabile venga aggiornata correttamente.
Esempio con AtomicReference
Compito: Fornire aggiornamento atomico di un riferimento a un oggetto.
Main.java
123456789101112131415161718192021222324package com.example; import java.util.concurrent.atomic.AtomicReference; public class Main { // `AtomicReference` to safely update and access a shared `String` value private AtomicReference<String> sharedString = new AtomicReference<>("Initial"); public void updateValue(String newValue) { // Atomically sets the new value and gets the old value String oldValue = sharedString.getAndSet(newValue); // Prints the old and new values, along with the thread name System.out.println(Thread.currentThread().getName() + ": Value was " + oldValue + ", now " + sharedString.get()); } public static void main(String[] args) { Main example = new Main(); // Creates and starts 3 threads, each updating the shared value for (int i = 0; i < 3; i++) { new Thread(() -> example.updateValue("Updated by " + Thread.currentThread().getName())).start(); } } }
AtomicReference viene utilizzato qui per aggiornare atomicamente il valore di un riferimento a stringa. Il metodo getAndSet() imposta atomicamente il nuovo valore e restituisce il valore precedente.
A differenza delle variabili normali, che richiedono una sincronizzazione aggiuntiva, le variabili atomiche utilizzano primitive di basso livello per ridurre al minimo l'overhead e migliorare le prestazioni. Questo le rende particolarmente adatte per sistemi ad alta concorrenza.
1. Qual è il vantaggio dell'utilizzo delle variabili atomiche nella programmazione multithread?
2. Quale metodo delle variabili atomiche fornisce una modifica atomica del valore se il valore corrente è uguale a quello atteso?
3. Cosa garantisce il metodo set() nelle variabili atomiche?
Grazie per i tuoi commenti!