Classes de Sincronização JUC em Java

O pacote JUC é inevitável para quem estuda programação concorrente em Java. O JUC oferece suporte fundamental para programação concorrente, como pools de threads, filas de mensagens e mecanismos de sincronização. Vamos explorar algumas das classes mais importantes do JUC.

CountdownLatch

Primeiro, vamos entender o que é um latch:

Pense em uma porta com travas. Cada thread que chega à porta remove uma trava. Quando todas as travas são removidas, a porta se abre e as threads podem continuar sua execução.

public class ExemploCountDownLatch {
    public static void main(String[] args) throws InterruptedException {
        // Define quantas travas serão usadas
        CountDownLatch latch = new CountDownLatch(10);
        
        // Inicia 10 threads
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + " removeu uma trava");
                // Cada thread decrementa o contador quando termina
                latch.countDown();
            }, "Thread-" + i).start();
        }
        // A thread principal aguarda até que todas as travas sejam removidas
        latch.await();

        System.out.println("A porta foi aberta!");
    }
}


No desenvolvimento real, usamos o CountDownLatch para controlar a ordem de execução de múltiplas threads.

CyclicBarrier

Para entender bem uma classe, precisamos conhecer seu nome. A nomenclatura em programação é muito significativa.

CyclicBarrier pode ser traduzido como "barreira cíclica". Pense nisso como uma barreira que exige um número mínimo de threads para ser superada. Uma thread sozinha não consegue passar, mas quando um grupo suficiente se junta, a barreira é superada e todas podem continuar.

public class ExemploCyclicBarrier {
    public static void main(String[] args) {
        CyclicBarrier barrier = new CyclicBarrier(10, () -> {
            // Ação a ser executada quando todas as threads chegam
            System.out.println("Grupo completo, vamos começar!");
        });
        
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + " está pronto");
                try {
                    // As threads esperam aqui até o grupo completo
                    barrier.await();
                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }, "Participante-" + i).start();
        }
    }
}


Diferente do CountDownLatch, o CyclicBarrier tem um comportamento cíclico. Depois que a barreira é superada, ela se restabelece, exigindo novamente um número específico de threads para ser ultrapassada novamente.

Semaphore

Semaphore significa "semaforo", um mecanismo comum na sincronização de threads. Pense nele como um contador de recursos disponíveis:

  • Quando o contador > 0, recursos estão disponíveis
  • Quando o contador <= 0, não há recursos disponíveis

Por exemplo, imagine um escritório com uma impressora compartilhada:

public class ExemploSemaphore {
    public static void main(String[] args) {
        // Apenas uma impressora disponível
        Semaphore semaphore = new Semaphore(1);

        for (int i = 0; i < 20; i++) {
            new Thread(() -> {
                try {
                    // Tenta adquirir o recurso (a impressora)
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName() + " está usando a impressora");
                    // Simula o tempo de impressão
                    Thread.sleep(1000);
                    System.out.println(Thread.currentThread().getName() + " liberou a impressora");
                    // Libera o recurso
                    semaphore.release();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }, "Funcionario-" + i).start();
        }
    }
}


O Semaphore é ideal para cenários onde há recursos limitados e consumidores em excesso. Ao inicializar com um valor maior que 1, ele também pode ser usado para limitar a taxa de acesso a um serviço, funcionando como um mecanismo de limitação de fluxo.

ReadWriteLock

Leitura-Escrever (Read-Write Lock):

  • Leitura (Read Lock): Lock compartilhado, permite que múltiplas threads o mantenham simultaneamente
  • Escrita (Write Lock): Lock exclusivo, permite apenas uma thread por vez

Em termos simples: múltiplas threads podem ler simultaneamente, mas apenas uma pode escrever por vez.

Sabemos que locking tem um custo de performance. Se apenas estamos lendo dados, não há problema de concorrência. Usar um lock completo seria desnecessário. Por isso, o ReadWriteLock permite:

  • Leituras simultâneas (sem bloqueio mútuo)
  • Escrita exclusiva (bloqueia todas as outras operações)
public class ExemploReadWriteLock {
    private static final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
    private static final ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();
    private static final ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();

    public static void main(String[] args) {
        // Testando leituras simultâneas
        for (int i = 0; i < 10; i++) {
            new Thread(ExemploReadWriteLock::leitura, "Leitor-" + i).start();
        }

        // Testando escritas exclusivas
        for (int i = 0; i < 3; i++) {
            new Thread(ExemploReadWriteLock::escrita, "Escritor-" + i).start();
        }
    }

    public static void leitura() {
        readLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + " está lendo dados");
            // Simula tempo de leitura
            Thread.sleep(100);
            System.out.println(Thread.currentThread().getName() + " concluiu a leitura");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            readLock.unlock();
        }
    }

    public static void escrita() {
        writeLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + " está escrevendo dados");
            // Simula tempo de escrita
            Thread.sleep(500);
            System.out.println(Thread.currentThread().getName() + " concluiu a escrita");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            writeLock.unlock();
        }
    }
}


Com o ReadWriteLock, múltiplas threads podem ler simultaneamente, mas qualquer operação de escrita bloqueará todas as outras operações (leitura e escrita).

BlockingQueue

**Fila de Bloqueio (BlockingQueue):**Você certamente conhece as coleções do Java, divididas em duas categorias principais: Collection e Map. Dentro de Collection, temos List e Set, mas Queue é menos conhecido.

No entanto, a importância da Queue não é menor que as outras coleções. A BlockingQueue é fundamental para o funcionamento de pools de threads e filas de mensagens (MQ).

Uma Queue segue o princípio FIFO (First-In, First-Out): elementos entram no final e saem do início.

A interface Queue oferece métodos básicos:

  • add() - Adiciona um elemento, lança exceção se a fila estiver cheia
  • offer() - Adiciona um elemento, retorna false se a fila estiver cheia
  • poll() - Remove e retorna o elemento da cabeça da fila
  • peek() - Retorna o elemento da cabeça sem removê-lo

A BlockingQueue adiciona métodos de bloqueio:

  • put() - Adiciona um elemento, bolqueia se a fila estiver cheia até haver espaço
  • take() - Remove um elemento, bloqueia se a fila estiver vazia até houver elementos

Existem implementações com e sem limite de capacidade:

  • Com limite: ArrayBlockingQueue (implementada com array, tamanho fixo)
  • Sem limite: LinkedBlockingQueue (implementada com lista encadeada, teoricamente ilimitada)

Vamos implementar um simples modelo produtor-consumidor:

public class ExemploBlockingQueue {
    public static void main(String[] args) {
        // Fila com capacidade para 5 itens
        ArrayBlockingQueue<Integer> fila = new ArrayBlockingQueue<>(5);

        // Thread produtora
        new Thread(() -> {
            for (int i = 1; i <= 20; i++) {
                try {
                    fila.put(i);
                    System.out.println("Produtor produziu: " + i);
                    Thread.sleep(100); // Simula tempo de produção
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();

        // Thread consumidora
        new Thread(() -> {
            for (int i = 1; i <= 20; i++) {
                try {
                    Integer item = fila.take();
                    System.out.println("Consumidor consumiu: " + item);
                    Thread.sleep(200); // Simula tempo de consumo
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}


Este é um exemplo simplificado de uma fila de mensagens (MQ).

SynchronousQueue

A SynchronousQueue é um tipo especial de BlockingQueue que não tem capacidade. Isso significa que um produtor só pode passar um item diretamente para um consumidor quando ele está pronto para receber. Não há armazenamento intermediário.

conclusão

Existem outras classes importantes no pacote JUC, como DelayQueue (fila de atraso) e TransferQueue (para controle de taxa de produção). No próximo artigo, exploraremos os pools de threads em Java.

Tags: java programação concorrente juc Sincronização threads

Publicado em 7-15 18:40