O AbstractQueuedSynchronizer (AQS) é a espinha dorsal para a implementação de diversos componentes de sincronização no ecossistema Java. Ele fornece uma estrutura robusta para gerenciar o estado de sincronização, o enfileiramento de threads bloqueadas e a sinalização de prontidão.
Fundamentos do AQS
O funcionamento do AQS baseia-se em dois pilares principais:
- Estado Atômico: Um valor inteiro (
state) que representa a condição do sincronizador (por exemplo: 0 para desbloqueado, 1 para bloqueado). - Fila de Espera: Uma estrutura de lista duplamente ligada composta por objetos
Node, operando no modelo FIFO (First-In-First-Out).
Embora seu nome comece com "Abstract", o AQS não é uma interface, mas uma classe que fornece métodos de modelo (Template Methods). Desenvolvedores estendem o AQS para definir como o estado deve ser adquirido e liberado, seja de forma exclusiva (Exclusive) ou compartilhada (Shared).
Implementando um Lock Customizado com AQS
Para criar um lock funcional, geralmente implementamos uma classe interna que estende o AbstractQueuedSynchronizer e sobrescreve os métodos tryAcquire e tryRelease. Abaixo, um exemplo de um lock reentrante simplificado:
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.TimeUnit;
public class ReentrantCustomLock implements Lock {
private final SyncController controller = new SyncController();
private static class SyncController extends AbstractQueuedSynchronizer {
@Override
protected boolean tryAcquire(int permits) {
Thread current = Thread.currentThread();
int currentState = getState();
if (currentState == 0) {
if (compareAndSetState(0, permits)) {
setExclusiveOwnerThread(current);
return true;
}
} else if (current == getExclusiveOwnerThread()) {
setState(currentState + permits);
return true;
}
return false;
}
@Override
protected boolean tryRelease(int permits) {
if (Thread.currentThread() != getExclusiveOwnerThread()) {
throw new IllegalMonitorStateException();
}
int nextState = getState() - permits;
boolean free = false;
if (nextState == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(nextState);
return free;
}
Condition newConditionObject() {
return new ConditionObject();
}
}
@Override
public void lock() { controller.acquire(1); }
@Override
public void unlock() { controller.release(1); }
@Override
public void lockInterruptibly() throws InterruptedException {
controller.acquireInterruptibly(1);
}
@Override
public boolean tryLock() { return controller.tryAcquire(1); }
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return controller.tryAcquireNanos(1, unit.toNanos(time));
}
@Override
public Condition newCondition() { return controller.newConditionObject(); }
}
Componentes de Sincronização do J.U.C
1. CountDownLatch
O CountDownLatch permite que uma ou mais threads esperem até que um conjunto de operações realizadas em outras threads seja concluído. O contador é inicializado com um valor e decresce conforme o método countDown() é chamado. Quando chega a zero, todas as threads em await() são liberadas.
import java.util.concurrent.CountDownLatch;
public class LatchExample {
public static void main(String[] args) throws InterruptedException {
int tasks = 3;
CountDownLatch latch = new CountDownLatch(tasks);
for (int i = 0; i < tasks; i++) {
new Thread(() -> {
System.out.println("Processando sub-tarefa...");
latch.countDown();
}).start();
}
latch.await();
System.out.println("Todas as tarefas concluídas. Prosseguindo...");
}
}
2. Semaphore
Diferente de um lock exclusivo, o Semaphore controla o acesso a um recurso limitado através de um conjunto de permissões. É ideal para limitar o número de conexões simultâneas a um banco de dados ou largura de banda.
import java.util.concurrent.Semaphore;
public class ResourceThrottling {
private final Semaphore pool = new Semaphore(2); // Limite de 2 acessos
public void accessResource() {
try {
pool.acquire();
System.out.println(Thread.currentThread().getName() + " acessando recurso.");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
pool.release();
}
}
}
3. CyclicBarrier
O CyclicBarrier é um ponto de sincronização onde as threads devem esperar umas pelas outras até que todas cheguem à barreira antes de continuar. Ao contrário do Latch, ele pode ser reiniciado.
import java.util.concurrent.CyclicBarrier;
public class BarrierTask {
public static void main(String[] args) {
CyclicBarrier barrier = new CyclicBarrier(3, () -> System.out.println("Barreira atingida. Executando ação conjunta."));
for (int i = 0; i < 3; i++) {
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + " chegou na barreira.");
barrier.await();
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}
}
Fork/Join Framework
Introduzido no Java 7, o framework Fork/Join é projetado para tarefas que podem ser divididas recursivamente em subtarefas menores (divide and conquer). Ele utiliza o algoritmo de Work-Stealing, onde threads ociosas podem "roubar" tarefas da fila de outras threads ocupadas para maximizar o uso da CPU.
Limitações importantes:
- As tarefas devem evitar operações de I/O bloqeuantes.
- O sincronismo deve ocorrer apenas via métodos
fork()ejoin(). - Exceções checadas não devem ser lançadas de dentro das tarefas; elas precisam ser traatdas internamente.