O pacote java.util.concurrent (JUC) fornece utilitários poderosos para coordenar o fluxo de execução entre múltiplas threads. Entre as ferramentas mais utilizadas para controle de sincronização avançada, destacam-se o CountDownLatch, o CyclicBarrier e o Semaphore. Essas classes permitem gerenciar estados de espera e acesso a recursos de forma mais eficiente do que o uso manual de wait() e notify().
- Sincronização com CountDownLatch
O CountDownLatch funciona como um contador regressivo. Ele permite que uma ou mais threads aguardem até que um conjunto de operações exceutadas em outras threads seja concluído. O contador é inicializado com um valor inteiro e diminui a cada chamada do método countDown(). As threads que chamam await() ficam bloqueadas até que o contador chegue a zero.
import java.util.concurrent.CountDownLatch;
public class ExemploCountDownLatch {
public static void main(String[] args) throws InterruptedException {
int totalTarefas = 4;
CountDownLatch sincronizador = new CountDownLatch(totalTarefas);
for (int i = 1; i <= totalTarefas; i++) {
final int idTarefa = i;
new Thread(() -> {
try {
System.out.println("Processando sub-tarefa: " + idTarefa);
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
sincronizador.countDown();
}
}).start();
}
sincronizador.await();
System.out.println("Todas as tarefas concluídas. Iniciando processo principal.");
}
}
- Barreiras de Execução com CyclicBarrier
Diferente do CountDownLatch, o CyclicBarrier é focado em fazer com que um grupo de threads aguarde umas pelas outras em um ponto comum de exeecução (barreira). Ele é "cíclico" porque pode ser reutilizado após a liberação das threads. É ideal para algoritmos paralelos que dividem um problema em partes e precisam sincronizar os resultados parciais antes de prosseguir.
import java.util.concurrent.CyclicBarrier;
public class ExemploCyclicBarrier {
public static void main(String[] args) {
int limiteParticipantes = 3;
CyclicBarrier barreira = new CyclicBarrier(limiteParticipantes, () -> {
System.out.println("Ponto de sincronização atingido! Executando ação de consolidação.");
});
for (int i = 1; i <= limiteParticipantes; i++) {
final int workerId = i;
new Thread(() -> {
try {
System.out.println("Worker " + workerId + " chegou na barreira.");
barreira.await();
System.out.println("Worker " + workerId + " continuou a execução.");
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}
}
- Controle de Acesso com Semaphore
O Semaphore gerencia um conjunto de permissões (permits). Ele é frequentemente utilizado para limitar o número de threads que podem acessar um determinado recurso físico ou lógico simultaneamente, como um pool de conexões de banco de dados ou acesso a uma API externa.
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
public class ExemploSemaphore {
public static void main(String[] args) {
// Limitando o acesso simultâneo a apenas 2 threads
Semaphore controleAcesso = new Semaphore(2);
for (int i = 1; i <= 5; i++) {
final int userId = i;
new Thread(() -> {
try {
System.out.println("Usuário " + userId + " tentando obter acesso...");
controleAcesso.acquire();
System.out.println("Usuário " + userId + " [DENTRO] do recurso crítico.");
// Simulando tempo de uso do recurso
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("Usuário " + userId + " [SAIU] - Liberando permissão.");
controleAcesso.release();
}
}).start();
}
}
}
Resumo das Diferenças
- CountDownLatch: Ideal para esperar que N eventos ocorram. O contador não pode ser resetado.
- CyclicBarrier: Ideal para sincronizar N threads em um ponto de encontro. Pode ser reutilizado.
- Semaphore: Utilizado para controle de concorrência e limitação de recursos compartilhados.