Synchronized e atomic package to control the order of thread execution

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 synchronized no método, garantimos que apenas uma thread por vez possa executar qualquer um dos métodos que alteram o valor de count, 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 AtomicInteger oferece métodos como incrementAndGet() e decrementAndGet(), que garantem que as operações de incremento e decremento sejam realizadas de forma atômica, evitando a necessidade de sincronização explícita com synchronized.

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: AtomicReference permite a manipulação atômica de referências a objetos. Aqui, set() e get() 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 t1 e t2 acessam o método incrementar(), que é synchronized. Isso garante que uma thread execute o método de cada vez, evitando acesso simultâneo ao contador.

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 que synchronized para manipulações simples, pois não há bloqueio de threads.

Erros comuns ao usar synchronized e Atomic:

  1. Esquecer de sincronizar recursos compartilhados: Se um recurso compartilhado não for sincronizado corretamente, pode ocorrer uma Race Condition.
  2. Excesso de sincronização: Sincronizar métodos ou blocos de código desnecessariamente pode causar problemas de desempenho devido ao bloqueio excessivo de threads.
  3. Usar AtomicInteger de forma inadequada: AtomicInteger não substitui a sincronização para operações complexas. Caso a operação dependa de múltiplas variáveis, pode ser necessário usar synchronized.

✔️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, como AtomicInteger e AtomicReference.

📝 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

  1. B – Apenas uma thread entra no método synchronized por vez, as outras esperam.
  2. CAtomicInteger é a classe correta.
  3. BincrementAndGet() é atômico e thread-safe.
  4. C – Pode ocorrer race condition se não houver controle.
  5. DaddAndGet(3) soma 3 ao valor atual (5), retornando 8.
  6. Dsynchronized controla o acesso concorrente a recursos.
  7. CAtomicInteger é mais eficiente para operações simples.
  8. D – Métodos sincronizados de instâncias diferentes podem ser executados ao mesmo tempo.
  9. BAtomicReference garante atomicidade em referências.
  10. B – O método não é thread-safe. Pode haver condição de corrida.