Guia para CountDownLatch em Java
1. Introdução
Neste artigo, daremos um guia para a classeCountDownLatch e demonstraremos como ela pode ser usada em alguns exemplos práticos.
Essencialmente, usando umCountDownLatch, podemos fazer com que um thread seja bloqueado até que outros threads tenham concluído uma determinada tarefa.
2. Uso em programação simultânea
Simplificando, aCountDownLatch tem um campocounter, que você pode decrementar conforme necessário. Podemos então usá-lo para bloquear um thread de chamada até que seja contado até zero.
Se estivéssemos fazendo algum processamento paralelo, poderíamos instanciar oCountDownLatch com o mesmo valor para o contador como um número de threads que queremos trabalhar. Então, poderíamos simplesmente chamarcountdown() depois que cada thread terminar, garantindo que um thread dependente chamandoawait() bloqueará até que os threads de trabalho sejam concluídos.
3. Esperando a conclusão de um pool de threads
Vamos experimentar este padrão criando umWorkere usando um campoCountDownLatch para sinalizar quando ele for concluído:
public class Worker implements Runnable {
private List outputScraper;
private CountDownLatch countDownLatch;
public Worker(List outputScraper, CountDownLatch countDownLatch) {
this.outputScraper = outputScraper;
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
doSomeWork();
outputScraper.add("Counted down");
countDownLatch.countDown();
}
}
Então, vamos criar um teste para provar que podemos fazer com que umCountDownLatch espere até que as instânciasWorker sejam concluídas:
@Test
public void whenParallelProcessing_thenMainThreadWillBlockUntilCompletion()
throws InterruptedException {
List outputScraper = Collections.synchronizedList(new ArrayList<>());
CountDownLatch countDownLatch = new CountDownLatch(5);
List workers = Stream
.generate(() -> new Thread(new Worker(outputScraper, countDownLatch)))
.limit(5)
.collect(toList());
workers.forEach(Thread::start);
countDownLatch.await();
outputScraper.add("Latch released");
assertThat(outputScraper)
.containsExactly(
"Counted down",
"Counted down",
"Counted down",
"Counted down",
"Counted down",
"Latch released"
);
}
Naturalmente, "trava liberada" sempre será a última saída - pois depende da liberação deCountDownLatch.
Observe que, se não chamarmosawait(), não poderemos garantir a ordem de execução dos threads, portanto, o teste falhará aleatoriamente.
4. A Pool of Threads Waiting to Begin
Se pegamos o exemplo anterior, mas desta vez iniciamos milhares de threads em vez de cinco, é provável que muitos dos anteriores tenham terminado o processamento antes mesmo de chamarmosstart() nos posteriores. Isso pode dificultar a tentativa de reproduzir um problema de simultaneidade, pois não seríamos capazes de fazer todos os nossos threads rodarem em paralelo.
Para contornar isso, vamos fazer oCountdownLatch funcionar de maneira diferente do que no exemplo anterior. Em vez de bloquear um thread pai até que alguns threads filhos sejam concluídos, podemos bloquear cada segmento filho até que todos os outros sejam iniciados.
Vamos modificar nosso métodorun() para que ele seja bloqueado antes do processamento:
public class WaitingWorker implements Runnable {
private List outputScraper;
private CountDownLatch readyThreadCounter;
private CountDownLatch callingThreadBlocker;
private CountDownLatch completedThreadCounter;
public WaitingWorker(
List outputScraper,
CountDownLatch readyThreadCounter,
CountDownLatch callingThreadBlocker,
CountDownLatch completedThreadCounter) {
this.outputScraper = outputScraper;
this.readyThreadCounter = readyThreadCounter;
this.callingThreadBlocker = callingThreadBlocker;
this.completedThreadCounter = completedThreadCounter;
}
@Override
public void run() {
readyThreadCounter.countDown();
try {
callingThreadBlocker.await();
doSomeWork();
outputScraper.add("Counted down");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
completedThreadCounter.countDown();
}
}
}
Agora, vamos modificar nosso teste para que ele seja bloqueado até que todos osWorkers tenham começado, desbloqueie oWorkers,e então bloqueie até queWorkers tenha terminado:
@Test
public void whenDoingLotsOfThreadsInParallel_thenStartThemAtTheSameTime()
throws InterruptedException {
List outputScraper = Collections.synchronizedList(new ArrayList<>());
CountDownLatch readyThreadCounter = new CountDownLatch(5);
CountDownLatch callingThreadBlocker = new CountDownLatch(1);
CountDownLatch completedThreadCounter = new CountDownLatch(5);
List workers = Stream
.generate(() -> new Thread(new WaitingWorker(
outputScraper, readyThreadCounter, callingThreadBlocker, completedThreadCounter)))
.limit(5)
.collect(toList());
workers.forEach(Thread::start);
readyThreadCounter.await();
outputScraper.add("Workers ready");
callingThreadBlocker.countDown();
completedThreadCounter.await();
outputScraper.add("Workers complete");
assertThat(outputScraper)
.containsExactly(
"Workers ready",
"Counted down",
"Counted down",
"Counted down",
"Counted down",
"Counted down",
"Workers complete"
);
}
Esse padrão é realmente útil para tentar reproduzir erros de simultaneidade, pois pode ser usado para forçar milhares de threads a tentar executar alguma lógica em paralelo.
5. Encerrando umCountdownLatch mais cedo
Às vezes, podemos nos deparar com uma situação em queWorkers termina com erro antes da contagem regressiva deCountDownLatch.. Isso pode resultar em nunca chegar a zero eawait() nunca terminar:
@Override
public void run() {
if (true) {
throw new RuntimeException("Oh dear, I'm a BrokenWorker");
}
countDownLatch.countDown();
outputScraper.add("Counted down");
}
Vamos modificar nosso teste anterior para usar umBrokenWorker, a fim de mostrar comoawait() bloqueará para sempre:
@Test
public void whenFailingToParallelProcess_thenMainThreadShouldGetNotGetStuck()
throws InterruptedException {
List outputScraper = Collections.synchronizedList(new ArrayList<>());
CountDownLatch countDownLatch = new CountDownLatch(5);
List workers = Stream
.generate(() -> new Thread(new BrokenWorker(outputScraper, countDownLatch)))
.limit(5)
.collect(toList());
workers.forEach(Thread::start);
countDownLatch.await();
}
Claramente, esse não é o comportamento que queremos - seria muito melhor para o aplicativo continuar do que o bloqueio infinito.
Para contornar isso, vamos adicionar um argumento de tempo limite à nossa chamada paraawait().
boolean completed = countDownLatch.await(3L, TimeUnit.SECONDS);
assertThat(completed).isFalse();
Como podemos ver, o tempo limite do teste acabará eawait() retornaráfalse.
6. Conclusão
Neste guia rápido, demonstramos como podemos usar umCountDownLatch para bloquear um thread até que outros threads concluam algum processamento.
Também mostramos como ele pode ser usado para ajudar a depurar problemas de simultaneidade, garantindo que os threads sejam executados em paralelo.
A implementação desses exemplos pode ser encontradaover on GitHub; este é um projeto baseado em Maven, portanto, deve ser fácil de executar como está.