 Variáveis Atômicas
Variáveis Atômicas
Já abordamos o que é atomicidade e os problemas que ela pode causar na primeira seção deste curso. Naquela ocasião, tratamos a questão utilizando blocos sincronizados ou métodos sincronizados. Agora, exploraremos como alcançar o mesmo resultado de forma mais simples utilizando uma classe atômica.
O que são Variáveis Atômicas?
Variáveis atômicas garantem que operações (leitura, escrita, incremento) em variáveis sejam realizadas atomicamente, ou seja, executadas de forma contínua e segura em um ambiente multithread. Isso assegura que a operação será concluída integralmente, sem a possibilidade de interferência de outras threads durante sua execução.
Por que Precisamos de Variáveis Atômicas?
Sem o uso de variáveis atômicas ou outros mecanismos de sincronização, operações como incremento (++) podem ser inseguras. Por exemplo, quando múltiplas threads acessam a mesma variável simultaneamente, atualizações podem ser perdidas, resultando em resultados incorretos. As variáveis atômicas resolvem esse problema ao garantir que as operações sobre elas sejam executadas sequencialmente.
Discutimos anteriormente esse problema quando a operação de incremento foi dividida em três etapas (leitura, incremento, escrita), mas com variáveis atômicas, tudo é feito em uma única operação!
Tipos de Variáveis Atômicas em Java
Em geral, existem muitas implementações atômicas, e não abordaremos todas aqui, pois isso levaria muito tempo.
Java fornece várias classes de variáveis atômicas no pacote java.util.concurrent.atomic, cada uma projetada para manipular um tipo de dado específico:
- AtomicInteger: para operações atômicas em int;
- AtomicLong: para operações atômicas em long;
- AtomicBoolean: para operações atômicas em boolean;
- AtomicReference<V>: para operações atômicas em objetos (tipo genérico).
Métodos
O método get() retorna o valor atual de uma variável. O método set(V newValue) define um novo valor para a variável. Por outro lado, lazySet(V newValue) é semelhante ao set(), mas pode postergar a atualização do valor, oferecendo uma atualização ordenada em determinadas situações.
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()); } }
O método compareAndSet(V expect, V update) atualiza o valor se o valor atual corresponder ao valor esperado. Retorna true se a atualização for bem-sucedida e false se o valor atual não corresponder ao valor esperado. Em contraste, o método getAndSet(V newValue) define um novo valor e retorna o valor anterior.
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()); } }
Os métodos getAndIncrement() e getAndDecrement() incrementam ou decrementam o valor atual em uma unidade e retornam o valor anterior. Esses métodos se aplicam a variáveis atômicas numéricas, como AtomicInteger e AtomicLong. Em contraste, os métodos incrementAndGet() e decrementAndGet() também incrementam ou decrementam o valor atual em uma unidade, mas retornam o novo valor.
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 } }
O método getAndAdd(int delta) adiciona o valor especificado (delta) ao valor atual e retorna o valor anterior. Este método é utilizado com variáveis atômicas numéricas. Por outro lado, o método addAndGet(int delta) também adiciona o valor especificado (delta) ao valor atual, mas retorna o novo valor.
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 } }
Exemplos de Uso de Variáveis Atômicas
Exemplo com AtomicInteger (AtomicLong, AtomicBoolean são equivalentes, portanto não haverá exemplos separados para eles).
Tarefa: Implementação de um contador que é incrementado de forma segura por várias threads.
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(); } } }
Como pode ser observado, não utilizamos nenhuma sincronização aqui, pois a própria variável atômica fornece essa funcionalidade.
Este exemplo utiliza AtomicInteger para incrementar o contador de forma segura. O método getAndIncrement() primeiro retorna o valor atual da variável e, em seguida, incrementa em um. Isso ocorre de forma atômica, garantindo que a variável seja atualizada corretamente.
Exemplo com AtomicReference
Tarefa: Fornecer atualização atômica de uma referência para um objeto.
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 é utilizado aqui para atualizar atomicamente o valor de uma referência de string. O método getAndSet() define atomicamente o novo valor e retorna o valor anterior.
Diferente das variáveis comuns, que exigem sincronização adicional, variáveis atômicas utilizam primitivas de baixo nível para minimizar a sobrecarga e aumentar a performance. Isso as torna especialmente adequadas para sistemas de alta concorrência.
1. Qual é a vantagem de utilizar variáveis atômicas em programação multithread?
2. Qual método de variável atômica fornece uma alteração atômica no valor se o valor atual for igual ao valor esperado?
3. O que o método set() garante em variáveis atômicas?
Obrigado pelo seu feedback!
Pergunte à IA
Pergunte à IA
Pergunte o que quiser ou experimente uma das perguntas sugeridas para iniciar nosso bate-papo
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 Variáveis Atômicas
Variáveis Atômicas
Deslize para mostrar o menu
Já abordamos o que é atomicidade e os problemas que ela pode causar na primeira seção deste curso. Naquela ocasião, tratamos a questão utilizando blocos sincronizados ou métodos sincronizados. Agora, exploraremos como alcançar o mesmo resultado de forma mais simples utilizando uma classe atômica.
O que são Variáveis Atômicas?
Variáveis atômicas garantem que operações (leitura, escrita, incremento) em variáveis sejam realizadas atomicamente, ou seja, executadas de forma contínua e segura em um ambiente multithread. Isso assegura que a operação será concluída integralmente, sem a possibilidade de interferência de outras threads durante sua execução.
Por que Precisamos de Variáveis Atômicas?
Sem o uso de variáveis atômicas ou outros mecanismos de sincronização, operações como incremento (++) podem ser inseguras. Por exemplo, quando múltiplas threads acessam a mesma variável simultaneamente, atualizações podem ser perdidas, resultando em resultados incorretos. As variáveis atômicas resolvem esse problema ao garantir que as operações sobre elas sejam executadas sequencialmente.
Discutimos anteriormente esse problema quando a operação de incremento foi dividida em três etapas (leitura, incremento, escrita), mas com variáveis atômicas, tudo é feito em uma única operação!
Tipos de Variáveis Atômicas em Java
Em geral, existem muitas implementações atômicas, e não abordaremos todas aqui, pois isso levaria muito tempo.
Java fornece várias classes de variáveis atômicas no pacote java.util.concurrent.atomic, cada uma projetada para manipular um tipo de dado específico:
- AtomicInteger: para operações atômicas em int;
- AtomicLong: para operações atômicas em long;
- AtomicBoolean: para operações atômicas em boolean;
- AtomicReference<V>: para operações atômicas em objetos (tipo genérico).
Métodos
O método get() retorna o valor atual de uma variável. O método set(V newValue) define um novo valor para a variável. Por outro lado, lazySet(V newValue) é semelhante ao set(), mas pode postergar a atualização do valor, oferecendo uma atualização ordenada em determinadas situações.
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()); } }
O método compareAndSet(V expect, V update) atualiza o valor se o valor atual corresponder ao valor esperado. Retorna true se a atualização for bem-sucedida e false se o valor atual não corresponder ao valor esperado. Em contraste, o método getAndSet(V newValue) define um novo valor e retorna o valor anterior.
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()); } }
Os métodos getAndIncrement() e getAndDecrement() incrementam ou decrementam o valor atual em uma unidade e retornam o valor anterior. Esses métodos se aplicam a variáveis atômicas numéricas, como AtomicInteger e AtomicLong. Em contraste, os métodos incrementAndGet() e decrementAndGet() também incrementam ou decrementam o valor atual em uma unidade, mas retornam o novo valor.
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 } }
O método getAndAdd(int delta) adiciona o valor especificado (delta) ao valor atual e retorna o valor anterior. Este método é utilizado com variáveis atômicas numéricas. Por outro lado, o método addAndGet(int delta) também adiciona o valor especificado (delta) ao valor atual, mas retorna o novo valor.
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 } }
Exemplos de Uso de Variáveis Atômicas
Exemplo com AtomicInteger (AtomicLong, AtomicBoolean são equivalentes, portanto não haverá exemplos separados para eles).
Tarefa: Implementação de um contador que é incrementado de forma segura por várias threads.
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(); } } }
Como pode ser observado, não utilizamos nenhuma sincronização aqui, pois a própria variável atômica fornece essa funcionalidade.
Este exemplo utiliza AtomicInteger para incrementar o contador de forma segura. O método getAndIncrement() primeiro retorna o valor atual da variável e, em seguida, incrementa em um. Isso ocorre de forma atômica, garantindo que a variável seja atualizada corretamente.
Exemplo com AtomicReference
Tarefa: Fornecer atualização atômica de uma referência para um objeto.
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 é utilizado aqui para atualizar atomicamente o valor de uma referência de string. O método getAndSet() define atomicamente o novo valor e retorna o valor anterior.
Diferente das variáveis comuns, que exigem sincronização adicional, variáveis atômicas utilizam primitivas de baixo nível para minimizar a sobrecarga e aumentar a performance. Isso as torna especialmente adequadas para sistemas de alta concorrência.
1. Qual é a vantagem de utilizar variáveis atômicas em programação multithread?
2. Qual método de variável atômica fornece uma alteração atômica no valor se o valor atual for igual ao valor esperado?
3. O que o método set() garante em variáveis atômicas?
Obrigado pelo seu feedback!