Guide sur java.util.concurrent.BlockingQueue

Guide de java.util.concurrent.BlockingQueue

1. Vue d'ensemble

Dans cet article, nous examinerons l'une des constructionsjava.util.concurrent les plus utiles pour résoudre le problème producteur-consommateur simultané. Nous allons examiner une API de l'interfaceBlockingQueue et comment les méthodes de cette interface facilitent l'écriture de programmes simultanés.

Nous présenterons plus loin dans cet exemple un exemple de programme simple comportant plusieurs threads producteurs et plusieurs threads consommateurs.

2. TypesBlockingQueue

On peut distinguer deux types deBlockingQueue:

  • file d'attente illimitée - peut grossir presque indéfiniment

  • file d'attente limitée - avec capacité maximale définie

2.1. File d'attente illimitée

La création de files d'attente sans limites est simple:

BlockingQueue blockingQueue = new LinkedBlockingDeque<>();

La capacité deblockingQueue sera définie surInteger.MAX_VALUE. Toutes les opérations qui ajoutent un élément à la file d'attente illimitée ne seront jamais bloquées, donc elle pourrait atteindre une très grande taille.

Lors de la conception d'un programme producteur-consommateur utilisant BlockingQueue sans limite, le plus important est que les consommateurs soient en mesure de consommer des messages aussi rapidement que les producteurs ajoutent des messages à la file d'attente. Sinon, la mémoire pourrait se remplir et nous obtiendrions une exceptionOutOfMemory.

2.2. File d'attente limitée

Le deuxième type de files d'attente est la file d'attente limitée. Nous pouvons créer de telles files d'attente en transmettant la capacité en tant qu'argument à un constructeur:

BlockingQueue blockingQueue = new LinkedBlockingDeque<>(10);

Ici, nous avons unblockingQueue qui a une capacité égale à 10. Cela signifie que lorsqu'un consommateur tente d'ajouter un élément à une file d'attente déjà pleine, en fonction d'une méthode qui a été utilisée pour l'ajouter (offer(),add() ouput()), il bloquera jusqu'à ce que l'espace pour insérer l'objet devienne disponible. Sinon, les opérations échoueront.

L'utilisation de la file d'attente liée est un bon moyen de concevoir des programmes simultanés, car lorsque nous insérons un élément dans une file d'attente déjà complète, ces opérations doivent attendre que les utilisateurs rattrapent leur retard et libèrent de l'espace dans la file d'attente. Cela nous donne une limitation sans aucun effort de notre part.

3. APIBlockingQueue

Il existe deux types de méthodes dans l'interfaceBlockingQueue Les méthodes responsables de l'ajout d'éléments à une file d'attente et les méthodes qui récupèrent ces éléments. Chaque méthode de ces deux groupes se comporte différemment dans le cas où la file d'attente est pleine / vide.

3.1. Ajout d'éléments

  • add() – renvoietrue si l'insertion a réussi, sinon renvoie unIllegalStateException

  • put() – insère l'élément spécifié dans une file d'attente, en attendant un emplacement libre si nécessaire

  • offer() – renvoietrue si l'insertion a réussi, sinonfalse

  • offer(E e, long timeout, TimeUnit unit) – essaie d'insérer un élément dans une file d'attente et attend un emplacement disponible dans un délai spécifié

3.2. Récupération d'éléments

  • take() - attend un élément head d'une file d'attente et le supprime. Si la file d'attente est vide, il se bloque et attend qu'un élément soit disponible

  • poll(long timeout, TimeUnit unit) – récupère et supprime la tête de la file d'attente, en attendant le temps d'attente spécifié si nécessaire pour qu'un élément devienne disponible. Renvoienull après un timeout __

Ces méthodes sont les éléments de base les plus importants de l'interfaceBlockingQueue lors de la création de programmes producteur-consommateur.

4. Exemple de producteur-consommateur multithread

Créons un programme composé de deux parties: un producteur et un consommateur.

Le producteur produira un nombre aléatoire de 0 à 100 et mettra ce nombre dans unBlockingQueue. Nous aurons 4 threads producteurs et utiliserons la méthodeput() pour bloquer jusqu'à ce qu'il y ait de l'espace disponible dans la file d'attente.

Il est important de se rappeler que nous devons empêcher nos threads consommateurs d'attendre qu'un élément apparaisse dans une file d'attente indéfiniment.

Une bonne technique pour signaler au producteur qu'il n'y a plus de messages à traiter consiste à envoyer un message spécial appelé pilule empoisonnée. Nous devons envoyer autant de pilules empoisonnées que nous avons aux consommateurs. Ensuite, lorsqu'un consommateur prendra ce message spécial de pilule empoisonnée dans une file d'attente, il terminera son exécution correctement.

Examinons une classe de producteurs:

public class NumbersProducer implements Runnable {
    private BlockingQueue numbersQueue;
    private final int poisonPill;
    private final int poisonPillPerProducer;

    public NumbersProducer(BlockingQueue numbersQueue, int poisonPill, int poisonPillPerProducer) {
        this.numbersQueue = numbersQueue;
        this.poisonPill = poisonPill;
        this.poisonPillPerProducer = poisonPillPerProducer;
    }
    public void run() {
        try {
            generateNumbers();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

    private void generateNumbers() throws InterruptedException {
        for (int i = 0; i < 100; i++) {
            numbersQueue.put(ThreadLocalRandom.current().nextInt(100));
        }
        for (int j = 0; j < poisonPillPerProducer; j++) {
            numbersQueue.put(poisonPill);
        }
     }
}

Notre constructeur producteur prend comme argument leBlockingQueue qui est utilisé pour coordonner le traitement entre le producteur et le consommateur. Nous voyons que la méthodegenerateNumbers() mettra 100 éléments dans une file d'attente. Il faut aussi un message de pilule empoisonnée pour savoir quel type de message est mis en file d'attente lorsque l'exécution sera terminée. Ce message doit être mispoisonPillPerProducer fois dans une file d'attente.

Chaque consommateur prendra un élément d'unBlockingQueue en utilisant la méthodetake() afin qu'il se bloque jusqu'à ce qu'il y ait un élément dans une file d'attente. Après avoir pris unInteger d'une file d'attente, il vérifie si le message est une pilule empoisonnée, si oui, l'exécution d'un thread est terminée. Sinon, il imprimera le résultat sur la sortie standard avec le nom du thread actuel.

Cela nous donnera un aperçu du fonctionnement interne de nos consommateurs:

public class NumbersConsumer implements Runnable {
    private BlockingQueue queue;
    private final int poisonPill;

    public NumbersConsumer(BlockingQueue queue, int poisonPill) {
        this.queue = queue;
        this.poisonPill = poisonPill;
    }
    public void run() {
        try {
            while (true) {
                Integer number = queue.take();
                if (number.equals(poisonPill)) {
                    return;
                }
                System.out.println(Thread.currentThread().getName() + " result: " + number);
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

La chose importante à noter est l'utilisation d'une file d'attente. Comme dans le constructeur du producteur, une file d'attente est transmise en tant qu'argument. Nous pouvons le faire carBlockingQueue peut être partagé entre les threads sans aucune synchronisation explicite.

Maintenant que nous avons notre producteur et notre consommateur, nous pouvons commencer notre programme. Nous devons définir la capacité de la file d’attente et la définir sur 100 éléments.

Nous voulons avoir 4 threads producteurs et un nombre de threads consommateurs sera égal au nombre de processeurs disponibles:

int BOUND = 10;
int N_PRODUCERS = 4;
int N_CONSUMERS = Runtime.getRuntime().availableProcessors();
int poisonPill = Integer.MAX_VALUE;
int poisonPillPerProducer = N_CONSUMERS / N_PRODUCERS;
int mod = N_CONSUMERS % N_PRODUCERS;

BlockingQueue queue = new LinkedBlockingQueue<>(BOUND);

for (int i = 1; i < N_PRODUCERS; i++) {
    new Thread(new NumbersProducer(queue, poisonPill, poisonPillPerProducer)).start();
}

for (int j = 0; j < N_CONSUMERS; j++) {
    new Thread(new NumbersConsumer(queue, poisonPill)).start();
}

new Thread(new NumbersProducer(queue, poisonPill, poisonPillPerProducer + mod)).start();

BlockingQueue est créé en utilisant une construction avec une capacité. Nous créons 4 producteurs et N consommateurs. Nous spécifions notre message de pilule empoisonnée comme étant unInteger.MAX_VALUE car une telle valeur ne sera jamais envoyée par notre producteur dans des conditions de travail normales. La chose la plus importante à noter ici est queBlockingQueue est utilisé pour coordonner le travail entre eux.

Lorsque nous exécutons le programme, 4 threads producteurs placeront desIntegers aléatoires dans unBlockingQueue et les consommateurs prendront ces éléments de la file d'attente. Chaque thread imprimera en sortie standard le nom du thread avec un résultat.

5. Conclusion

Cet article présente une utilisation pratique deBlockingQueue et explique les méthodes utilisées pour y ajouter et en récupérer des éléments. Nous avons également montré comment créer un programme producteur-consommateur multithread utilisantBlockingQueue pour coordonner le travail entre les producteurs et les consommateurs.

L'implémentation de tous ces exemples et extraits de code se trouve dans leGitHub project - il s'agit d'un projet basé sur Maven, il devrait donc être facile à importer et à exécuter tel quel.