Agora vamos explorar como usar a palavra-chave synchronized e o pacote java.util.concurrent.atomic para controlar a ordem de execução das threads. Esses recursos são fundamentais para evitar problemas de sincronização, como Race Conditions, em programas multithreaded.
1. Usando synchronized para sincronizar o acesso aos recursos compartilhados
A palavra-chave synchronized é usada para garantir que apenas uma thread acesse um bloco de código ou método específico de cada vez, prevenindo condições de corrida. Ela pode ser aplicada tanto a métodos quanto a blocos de código.
Exemplo: Sincronizando um método
public class Contador {
private int count = 0;
// Método sincronizado
public synchronized void incrementar() {
count++;
}
public synchronized void decrementar() {
count--;
}
public int getCount() {
return count;
}
}
- Explicação: Ao usar
synchronizedno método, garantimos que apenas uma thread por vez possa executar qualquer um dos métodos que alteram o valor decount, evitando acesso simultâneo e inconsistências.
Exemplo: Sincronizando um bloco de código
public class Contador {
private int count = 0;
public void incrementar() {
synchronized (this) { // Sincronizando bloco específico
count++;
}
}
public void decrementar() {
synchronized (this) { // Sincronizando bloco específico
count--;
}
}
public int getCount() {
return count;
}
}
- Explicação: A sincronização pode ser feita em blocos de código específicos, em vez de métodos inteiros. Isso permite maior flexibilidade e desempenho quando só é necessário controlar o acesso a uma parte do código.
2. Usando java.util.concurrent.atomic para controle atômico
O pacote java.util.concurrent.atomic oferece classes que permitem operações atômicas em variáveis, ou seja, operações que são realizadas de forma indivisível e sem interferência de outras threads. As classes mais comuns são AtomicInteger, AtomicLong, AtomicReference, entre outras.
Exemplo: Usando AtomicInteger para evitar condições de corrida
import java.util.concurrent.atomic.AtomicInteger;
public class Contador {
private AtomicInteger count = new AtomicInteger(0);
public void incrementar() {
count.incrementAndGet(); // Incrementa de forma atômica
}
public void decrementar() {
count.decrementAndGet(); // Decrementa de forma atômica
}
public int getCount() {
return count.get(); // Retorna o valor atual
}
}
- Explicação: A classe
AtomicIntegeroferece métodos comoincrementAndGet()edecrementAndGet(), que garantem que as operações de incremento e decremento sejam realizadas de forma atômica, evitando a necessidade de sincronização explícita comsynchronized.
Exemplo: Usando AtomicReference para objetos
import java.util.concurrent.atomic.AtomicReference;
public class AtomicObjeto {
private AtomicReference<String> valor = new AtomicReference<>("Inicial");
public void atualizarValor(String novoValor) {
valor.set(novoValor); // Atualiza de forma atômica
}
public String obterValor() {
return valor.get(); // Retorna o valor atual
}
}
- Explicação:
AtomicReferencepermite a manipulação atômica de referências a objetos. Aqui,set()eget()garantem que o acesso ao objeto seja seguro em um ambiente multithreaded.
3. Usando synchronized com múltiplas threads
Aqui está um exemplo de controle da execução de múltiplas threads com synchronized:
public class ThreadSincronizada {
private static int contador = 0;
public synchronized static void incrementar() {
contador++;
System.out.println(Thread.currentThread().getName() + " - Contador: " + contador);
}
public static void main(String[] args) {
Runnable tarefa = () -> {
for (int i = 0; i < 5; i++) {
incrementar(); // Acesso sincronizado
}
};
Thread t1 = new Thread(tarefa, "Thread 1");
Thread t2 = new Thread(tarefa, "Thread 2");
t1.start();
t2.start();
}
}
- Explicação: As threads
t1et2acessam o métodoincrementar(), que ésynchronized. Isso garante que uma thread execute o método de cada vez, evitando acesso simultâneo aocontador.
4. Comparação entre synchronized e AtomicInteger
synchronized: Necessário para proteger blocos de código ou métodos. Usado quando múltiplas threads devem acessar ou modificar variáveis ou recursos compartilhados de forma segura.AtomicInteger: Útil para operações atômicas simples em tipos primitivos (como int). Ele é mais eficiente do quesynchronizedpara manipulações simples, pois não há bloqueio de threads.
❌ Erros comuns ao usar synchronized e Atomic:
- Esquecer de sincronizar recursos compartilhados: Se um recurso compartilhado não for sincronizado corretamente, pode ocorrer uma Race Condition.
- Excesso de sincronização: Sincronizar métodos ou blocos de código desnecessariamente pode causar problemas de desempenho devido ao bloqueio excessivo de threads.
- Usar
AtomicIntegerde forma inadequada:AtomicIntegernão substitui a sincronização para operações complexas. Caso a operação dependa de múltiplas variáveis, pode ser necessário usarsynchronized.
✔️Resumo:
synchronized: Ideal para proteger blocos de código ou métodos inteiros em um ambiente multithreaded.java.util.concurrent.atomic: Útil para operações atômicas em tipos primitivos ou objetos imutáveis, comoAtomicIntegereAtomicReference.
📝 Questões
1. O que ocorre quando dois threads acessam simultaneamente um método marcado como synchronized?
A) Ambos executam o método ao mesmo tempo
B) Um espera o outro terminar
C) O método é executado apenas uma vez
D) O compilador lança uma exceção
2. Qual das seguintes classes fornece operações atômicas para um valor int?
A) AtomicInt
B) ConcurrentInteger
C) AtomicInteger
D) SynchronizedInt
3. Dado o seguinte código, qual afirmação é verdadeira?
AtomicInteger ai = new AtomicInteger(0);
ai.incrementAndGet();
A) O valor de ai agora é -1
B) O método incrementAndGet() é thread-safe
C) incrementAndGet() é equivalente a ai++
D) AtomicInteger só pode ser usado em classes synchronized
4. O que acontece se vários threads acessarem um método não-synchronized que modifica uma variável compartilhada?
A) O código compila mas ocorre exceção em tempo de execução
B) A execução é garantidamente segura
C) Pode ocorrer race condition
D) O compilador impede esse acesso
5. Qual é a saída esperada?
AtomicInteger ai = new AtomicInteger(5);
System.out.println(ai.addAndGet(3));
A) 3
B) 5
C) 7
D) 8
6. Qual é a função principal da palavra-chave synchronized?
A) Controlar tempo de execução
B) Prevenir deadlocks
C) Prevenir starvation
D) Controlar acesso concorrente a recursos compartilhados
7. Quando devo preferir AtomicInteger em vez de synchronized?
A) Sempre que precisar de operações matemáticas complexas
B) Quando houver múltiplas variáveis para sincronizar
C) Para operações simples e frequentes sobre um valor int
D) Nunca, synchronized é sempre melhor
8. O que é necessário para que dois métodos sincronizados sejam executados simultaneamente por threads diferentes?
A) Que sejam chamados da mesma instância
B) Que não usem nenhuma instância
C) Que sejam métodos static
D) Que sejam de instâncias diferentes
9. Qual dessas instruções é correta sobre AtomicReference<T>?
A) É usada para acessar listas de forma atômica
B) Garante atomicidade em referências a objetos
C) Só funciona com tipos primitivos
D) Não é thread-safe
10. Dado o código abaixo, qual problema pode ocorrer?
public class Conta {
private int saldo = 0;
public void depositar() {
saldo++;
}
}
A) Nenhum, está correto
B) Pode haver race condition se acessado por múltiplas threads
C) O compilador não permite incremento sem synchronized
D) O método depositar() sempre lança exceção
✅ Gabarito Comentado
- B – Apenas uma thread entra no método
synchronizedpor vez, as outras esperam. - C –
AtomicIntegeré a classe correta. - B –
incrementAndGet()é atômico e thread-safe. - C – Pode ocorrer race condition se não houver controle.
- D –
addAndGet(3)soma 3 ao valor atual (5), retornando 8. - D –
synchronizedcontrola o acesso concorrente a recursos. - C –
AtomicIntegeré mais eficiente para operações simples. - D – Métodos sincronizados de instâncias diferentes podem ser executados ao mesmo tempo.
- B –
AtomicReferencegarante atomicidade em referências. - B – O método não é thread-safe. Pode haver condição de corrida.