Guia para o Java TransferQueue
*1. Visão geral *
Neste artigo, veremos a construção https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/TransferQueue.html [TransferQueue] _ do padrão _java.util. pacote concorrente.
Simplificando, essa fila nos permite criar programas de acordo com o padrão produtor-consumidor e coordenar as mensagens que passam de produtores para consumidores.
A implementação é realmente semelhante ao link:/java-blocking-fila [BlockingQueue] –, mas nos oferece a nova capacidade de implementar uma forma de contrapressão. Isso significa que, quando o produtor envia uma mensagem ao consumidor usando o método _transfer () _, ele permanece bloqueado até que a mensagem seja consumida.
===* 2. Um Produtor - Zero Consumidores *
Vamos testar um método transfer () _ do _TransferQueue - o comportamento esperado é que o produtor seja bloqueado até que o consumidor receba a mensagem da fila usando o método _take () _.
Para conseguir isso, criaremos um programa que tem um produtor, mas zero consumidor. A primeira chamada de _transfer () _ do encadeamento do produtor será bloqueada indefinidamente, pois não temos consumidores para buscar esse elemento da fila.
Vamos ver como a classe Producer se parece:
class Producer implements Runnable {
private TransferQueue<String> transferQueue;
private String name;
private Integer numberOfMessagesToProduce;
public AtomicInteger numberOfProducedMessages
= new AtomicInteger();
@Override
public void run() {
for (int i = 0; i < numberOfMessagesToProduce; i++) {
try {
boolean added
= transferQueue.tryTransfer("A" + i, 4000, TimeUnit.MILLISECONDS);
if(added){
numberOfProducedMessages.incrementAndGet();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//standard constructors
}
Estamos passando uma instância do TransferQueue para o construtor, juntamente com um nome que queremos fornecer ao nosso produtor e o número de elementos que devem ser transferidos para a fila.
Observe que estamos usando o método tryTransfer () _, com um determinado tempo limite. Estamos aguardando quatro segundos e, se um produtor não puder transferir a mensagem dentro do tempo limite especificado, ele retornará _false e passará para a próxima mensagem. O produtor possui uma variável numberOfProducedMessages para acompanhar quantas mensagens foram produzidas.
A seguir, vejamos a classe Consumer:
class Consumer implements Runnable {
private TransferQueue<String> transferQueue;
private String name;
private int numberOfMessagesToConsume;
public AtomicInteger numberOfConsumedMessages
= new AtomicInteger();
@Override
public void run() {
for (int i = 0; i < numberOfMessagesToConsume; i++) {
try {
String element = transferQueue.take();
longProcessing(element);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private void longProcessing(String element)
throws InterruptedException {
numberOfConsumedMessages.incrementAndGet();
Thread.sleep(500);
}
//standard constructors
}
É semelhante ao produtor, mas estamos recebendo elementos da fila usando o método take () _. Também estamos simulando alguma ação de longa execução usando o método _longProcessing () _ no qual estamos incrementando a variável _numberOfConsumedMessages que é um contador das mensagens recebidas.
Agora, vamos começar nosso programa com apenas um produtor:
@Test
public void whenUseOneProducerAndNoConsumers_thenShouldFailWithTimeout()
throws InterruptedException {
//given
TransferQueue<String> transferQueue = new LinkedTransferQueue<>();
ExecutorService exService = Executors.newFixedThreadPool(2);
Producer producer = new Producer(transferQueue, "1", 3);
//when
exService.execute(producer);
//then
exService.awaitTermination(5000, TimeUnit.MILLISECONDS);
exService.shutdown();
assertEquals(producer.numberOfProducedMessages.intValue(), 0);
}
Queremos enviar três elementos para a fila, mas o produtor está bloqueado no primeiro elemento e não há consumidor para buscar esse elemento na fila . Estamos usando o método tryTransfer () _ _ que bloqueará até a mensagem é consumido ou o tempo limite é atingido. Após o tempo limite, ele retornará false para indicar que a transferência falhou e tentará transferir a próxima. Esta é a saída do exemplo anterior:
Producer: 1 is waiting to transfer...
can not add an element due to the timeout
Producer: 1 is waiting to transfer...
===* 3. Um produtor - um consumidor *
Vamos testar uma situação em que há um produtor e um consumidor:
@Test
public void whenUseOneConsumerAndOneProducer_thenShouldProcessAllMessages()
throws InterruptedException {
//given
TransferQueue<String> transferQueue = new LinkedTransferQueue<>();
ExecutorService exService = Executors.newFixedThreadPool(2);
Producer producer = new Producer(transferQueue, "1", 3);
Consumer consumer = new Consumer(transferQueue, "1", 3);
//when
exService.execute(producer);
exService.execute(consumer);
//then
exService.awaitTermination(5000, TimeUnit.MILLISECONDS);
exService.shutdown();
assertEquals(producer.numberOfProducedMessages.intValue(), 3);
assertEquals(consumer.numberOfConsumedMessages.intValue(), 3);
}
O TransferQueue é usado como um ponto de troca e, até o consumidor consumir um elemento da fila, o produtor não pode continuar adicionando outro elemento a ele. Vejamos a saída do programa:
Producer: 1 is waiting to transfer...
Consumer: 1 is waiting to take element...
Producer: 1 transferred element: A0
Producer: 1 is waiting to transfer...
Consumer: 1 received element: A0
Consumer: 1 is waiting to take element...
Producer: 1 transferred element: A1
Producer: 1 is waiting to transfer...
Consumer: 1 received element: A1
Consumer: 1 is waiting to take element...
Producer: 1 transferred element: A2
Consumer: 1 received element: A2
Vemos que a produção e o consumo de elementos da fila são seqüenciais devido à especificação de TransferQueue.
===* 4. Muitos produtores - muitos consumidores *
No último exemplo, consideraremos ter vários consumidores e vários produtores:
@Test
public void whenMultipleConsumersAndProducers_thenProcessAllMessages()
throws InterruptedException {
//given
TransferQueue<String> transferQueue = new LinkedTransferQueue<>();
ExecutorService exService = Executors.newFixedThreadPool(3);
Producer producer1 = new Producer(transferQueue, "1", 3);
Producer producer2 = new Producer(transferQueue, "2", 3);
Consumer consumer1 = new Consumer(transferQueue, "1", 3);
Consumer consumer2 = new Consumer(transferQueue, "2", 3);
//when
exService.execute(producer1);
exService.execute(producer2);
exService.execute(consumer1);
exService.execute(consumer2);
//then
exService.awaitTermination(10_000, TimeUnit.MILLISECONDS);
exService.shutdown();
assertEquals(producer1.numberOfProducedMessages.intValue(), 3);
assertEquals(producer2.numberOfProducedMessages.intValue(), 3);
}
Neste exemplo, temos dois consumidores e dois produtores. Quando o programa é iniciado, vemos que ambos os produtores podem produzir um elemento e, depois disso, bloquearão até que um dos consumidores retire esse elemento da fila:
Producer: 1 is waiting to transfer...
Consumer: 1 is waiting to take element...
Producer: 2 is waiting to transfer...
Producer: 1 transferred element: A0
Producer: 1 is waiting to transfer...
Consumer: 1 received element: A0
Consumer: 1 is waiting to take element...
Producer: 2 transferred element: A0
Producer: 2 is waiting to transfer...
Consumer: 1 received element: A0
Consumer: 1 is waiting to take element...
Producer: 1 transferred element: A1
Producer: 1 is waiting to transfer...
Consumer: 1 received element: A1
Consumer: 2 is waiting to take element...
Producer: 2 transferred element: A1
Producer: 2 is waiting to transfer...
Consumer: 2 received element: A1
Consumer: 2 is waiting to take element...
Producer: 1 transferred element: A2
Consumer: 2 received element: A2
Consumer: 2 is waiting to take element...
Producer: 2 transferred element: A2
Consumer: 2 received element: A2
===* 5. Conclusão*
Neste artigo, analisamos a construção TransferQueue do pacote java.util.concurrent.
Vimos como implementar o programa produtor-consumidor usando essa construção. Usamos um método _transfer () _ para criar uma forma de contrapressão, em que um produtor não pode publicar outro elemento até que o consumidor recupere um elemento da fila.
O TransferQueue pode ser muito útil quando não queremos um produtor com excesso de produção que inundará a fila com mensagens, resultando nos erros OutOfMemory. Nesse projeto, o consumidor ditará a velocidade com que o produtor produzirá mensagens.
Todos esses exemplos e trechos de código podem ser encontrados over no GitHub - este é um projeto do Maven, portanto, deve ser fácil importar e executar como está.