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.