Guia para o Java TransferQueue

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á.