Guide de la Java TransferQueue

1. Vue d’ensemble

Dans cet article, nous examinerons la construction TransferQueue à partir du fichier standard java.util. paquet concurrent .

En termes simples, cette file d’attente nous permet de créer des programmes selon le modèle producteur-consommateur et de coordonner les messages transmis des producteurs aux consommateurs.

L’implémentation est en fait similaire à BlockingQueue , mais nous donne la nouvelle possibilité d’implémenter une forme de contrepression. Cela signifie que, lorsque le producteur envoie un message au consommateur à l’aide de la méthode transfer () , le producteur reste bloqué jusqu’à ce que le message soit consommé.

2. Un producteur - zéro consommateur

Testons une méthode transfer () à partir de TransferQueue - le comportement attendu est que le producteur sera bloqué jusqu’à ce que le consommateur reçoive le message de la file d’attente à l’aide d’une méthode take () .

Pour ce faire, nous allons créer un programme qui n’a qu’un seul producteur, mais aucun consommateur. Le premier appel de transfer () à partir du thread producteur bloque indéfiniment, car nous n’avons aucun consommateur pour extraire cet élément de la file d’attente.

Voyons à quoi ressemble la classe Producer :

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
}

Nous transmettons au constructeur une instance de TransferQueue avec le nom que nous voulons donner à notre producteur et le nombre d’éléments à transférer dans la file d’attente.

Notez que nous utilisons la méthode tryTransfer () , avec un délai d’expiration donné.

Nous attendons quatre secondes et si un producteur n’est pas en mesure de transférer le message dans le délai imparti, il renvoie false et passe au message suivant. Le producteur dispose d’une variable numberOfProducedMessages pour garder trace du nombre de messages produits.

Ensuite, regardons la 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
}

Cela ressemble au producteur, mais nous recevons des éléments de la file en utilisant la méthode take () . Nous simulons également une action longue en utilisant la méthode longProcessing () dans laquelle nous incrémentons la variable numberOfConsumedMessages qui est un compteur des messages reçus.

Maintenant, commençons notre programme avec un seul producteur:

@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);
}

Nous voulons envoyer trois éléments à la file d’attente, mais le producteur est bloqué sur le premier élément et il n’y a pas de consommateur pour extraire cet élément de la file d’attente _. Nous utilisons la méthode tryTransfer () _ qui bloquera jusqu’au message est consommé ou le délai d’attente est atteint.

Après le délai, il retournera false pour indiquer que le transfert a échoué et essaiera de transférer le suivant. Voici le résultat de l’exemple précédent:

Producer: 1 is waiting to transfer...
can not add an element due to the timeout
Producer: 1 is waiting to transfer...

3. Un producteur - un consommateur

Essayons une situation où il y a un producteur et un consommateur:

@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);
}

La TransferQueue est utilisée comme point d’échange et tant que le consommateur n’a pas utilisé un élément de la file d’attente, le producteur ne peut pas lui ajouter un autre élément. Regardons le résultat du programme:

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

Nous constatons que la production et la consommation d’éléments de la file d’attente sont séquentielles en raison de la spécification de TransferQueue.

4. De nombreux producteurs - De nombreux consommateurs

Dans le dernier exemple, nous envisagerons d’avoir plusieurs consommateurs et plusieurs producteurs:

@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);
}

Dans cet exemple, nous avons deux consommateurs et deux producteurs. Lorsque le programme démarre, nous voyons que les deux producteurs peuvent produire un élément et ensuite, ils bloquent jusqu’à ce que l’un des consommateurs retire cet élément de la file d’attente:

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

Dans cet article, nous examinions la construction TransferQueue du package java.util.concurrent .

Nous avons vu comment mettre en œuvre le programme producteur-consommateur en utilisant cette construction. Nous avons utilisé une méthode transfer () pour créer une forme de contre-pression, dans laquelle un producteur ne peut pas publier un autre élément tant que le consommateur n’a pas récupéré un élément de la file d’attente.

La TransferQueue peut être très utile lorsque nous ne voulons pas d’un producteur surproducteur qui inondera la file d’attente de messages, ce qui entraînera les erreurs OutOfMemory . Dans une telle conception, le consommateur dictera la vitesse à laquelle le producteur produira des messages.

Tous ces exemples et extraits de code sont disponibles à l’adresse over sur GitHub - il s’agit d’un projet Maven, il devrait donc être facile importer et exécuter tel quel.