Dominando o AbstractQueuedSynchronizer (AQS) e Componentes de Sincronização em Java

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() e join().
  • Exceções checadas não devem ser lançadas de dentro das tarefas; elas precisam ser traatdas internamente.

Tags: java Concurrency AQS Multithreading juc

Publicado em 9-5 18:24