Um guia para Java SynchronousQueue
1. Visão geral
Neste artigo, veremosSynchronousQueue do pacotejava.util.concurrent.
Simplificando, essa implementação nos permite trocar informações entre threads de maneira segura.
2. Visão geral da API
OSynchronousQueue tem apenastwo supported operations: take() and put(), and both of them are blocking.
Por exemplo, quando queremos adicionar um elemento à fila, precisamos chamar o métodoput(). Esse método será bloqueado até que algum outro thread chame o métodotake(), sinalizando que está pronto para receber um elemento.
Embora oSynchronousQueue tenha uma interface de fila, devemos pensar nele como um ponto de troca para um único elemento entre dois threads, no qual um thread está transferindo um elemento e outro está pegando esse elemento.
3. Implementando Handoffs Usando uma Variável Compartilhada
Para ver por queSynchronousQueue pode ser tão útil, implementaremos uma lógica usando uma variável compartilhada entre dois threads e, a seguir, reescreveremos essa lógica usandoSynchronousQueue tornando nosso código muito mais simples e legível.
Digamos que temos dois threads - um produtor e um consumidor - e quando o produtor está definindo um valor de uma variável compartilhada, queremos sinalizar esse fato para o thread do consumidor. Em seguida, o encadeamento do consumidor buscará um valor de uma variável compartilhada.
UsaremosCountDownLatch para coordenar essas duas threads, para evitar uma situação em que o consumidor acesse um valor de uma variável compartilhada que ainda não foi definida.
Vamos definir uma variávelsharedState e umCountDownLatch que serão usados para coordenar o processamento:
ExecutorService executor = Executors.newFixedThreadPool(2);
AtomicInteger sharedState = new AtomicInteger();
CountDownLatch countDownLatch = new CountDownLatch(1);
O produtor salvará um número inteiro aleatório na variávelsharedState e executará o métodocountDown() na sinalização decountDownLatch, para o consumidor que pode buscar um valor desharedState:
Runnable producer = () -> {
Integer producedElement = ThreadLocalRandom
.current()
.nextInt();
sharedState.set(producedElement);
countDownLatch.countDown();
};
O consumidor aguardarácountDownLatch usando o métodoawait(). Quando o produtor sinalizar que a variável foi definida, o consumidor irá buscá-la emsharedState:
Runnable consumer = () -> {
try {
countDownLatch.await();
Integer consumedElement = sharedState.get();
} catch (InterruptedException ex) {
ex.printStackTrace();
}
};
Por último, mas não menos importante, vamos começar nosso programa:
executor.execute(producer);
executor.execute(consumer);
executor.awaitTermination(500, TimeUnit.MILLISECONDS);
executor.shutdown();
assertEquals(countDownLatch.getCount(), 0);
Produzirá a seguinte saída:
Saving an element: -1507375353 to the exchange point
consumed an element: -1507375353 from the exchange point
Podemos ver que há muito código para implementar uma funcionalidade tão simples como trocar um elemento entre dois threads. Na próxima seção, tentaremos melhorar.
4. Implementando Handoffs usandoSynchronousQueue
Vamos agora implementar a mesma funcionalidade da seção anterior, mas com umSynchronousQueue.. Ele tem um efeito duplo porque podemos usá-lo para trocar estado entre threads e para coordenar essa ação de forma que não precisemos usar nada além deSynchronousQueue.
Primeiramente, definiremos uma fila:
ExecutorService executor = Executors.newFixedThreadPool(2);
SynchronousQueue queue = new SynchronousQueue<>();
O produtor chamará um métodoput() que será bloqueado até que algum outro encadeamento pegue um elemento da fila:
Runnable producer = () -> {
Integer producedElement = ThreadLocalRandom
.current()
.nextInt();
try {
queue.put(producedElement);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
};
O consumidor simplesmente recuperará esse elemento usando o métodotake():
Runnable consumer = () -> {
try {
Integer consumedElement = queue.take();
} catch (InterruptedException ex) {
ex.printStackTrace();
}
};
Em seguida, iniciaremos nosso programa:
executor.execute(producer);
executor.execute(consumer);
executor.awaitTermination(500, TimeUnit.MILLISECONDS);
executor.shutdown();
assertEquals(queue.size(), 0);
Produzirá a seguinte saída:
Saving an element: 339626897 to the exchange point
consumed an element: 339626897 from the exchange point
Podemos ver que umSynchronousQueue é usado como um ponto de troca entre os threads, o que é muito melhor e mais compreensível do que o exemplo anterior, que usou o estado compartilhado junto com umCountDownLatch.
5. Conclusão
Neste tutorial rápido, vimos a construçãoSynchronousQueue. Criamos um programa que troca dados entre duas threads usando o estado compartilhado e, em seguida, reescrevemos esse programa para alavancar a construçãoSynchronousQueue. Isso serve como um ponto de troca que coordena o produtor e o segmento do consumidor.
A implementação de todos esses exemplos e trechos de código pode ser encontrada emGitHub project - este é um projeto Maven, portanto, deve ser fácil de importar e executar como está.