Guide du Phaser Java

1. Vue d’ensemble

Dans cet article, nous examinerons la construction Phaser du package java.util.concurrent . C’est une construction très similaire au lien:/java-countdown-latch[ CountDownLatch ]qui nous permet de coordonner l’exécution de threads. Comparé à CountDownLatch , il comporte des fonctionnalités supplémentaires.

Le Phaser est une barrière sur laquelle le nombre dynamique de threads doit attendre avant de poursuivre l’exécution. Dans CountDownLatch , ce numéro ne peut pas être configuré de manière dynamique et doit être fourni lors de la création de l’instance.

2. Phaser API

Le Phaser nous permet de construire une logique dans laquelle les threads doivent attendre sur la barrière avant de passer à la prochaine étape d’exécution .

Nous pouvons coordonner plusieurs phases d’exécution en réutilisant une instance de Phaser pour chaque phase du programme. Chaque phase peut avoir un nombre différent de threads en attente de passage à une autre phase. Nous verrons un exemple d’utilisation des phases plus tard.

Pour participer à la coordination, le thread doit s’inscrire lui-même avec l’instance Phaser . Notez que cela ne fait qu’augmenter le nombre de partis enregistrés, et nous ne pouvons pas vérifier si le fil de discussion actuel est enregistré - nous devrions sous-classer l’implémentation pour le supporter.

Le thread signale qu’il est arrivé à la barrière en appelant le arriveAndAwaitAdvance () , qui est une méthode de blocage. Lorsque le nombre de partis arrivés est égal au nombre de partis enregistrés, l’exécution du programme se poursuivra et le nombre de phases augmentera. Nous pouvons obtenir le numéro de phase actuel en appelant la méthode getPhase () .

Lorsque le thread a terminé son travail, nous devrions appeler la méthode arriveAndDeregister () pour signaler que le thread actuel ne doit plus être pris en compte dans cette phase particulière.

3. Implémentation de la logique à l’aide de l’API Phaser

Disons que nous voulons coordonner plusieurs phases d’actions. Trois threads vont traiter la première phase et deux threads vont traiter la deuxième phase.

Nous allons créer une classe LongRunningAction qui implémente l’interface Runnable :

class LongRunningAction implements Runnable {
    private String threadName;
    private Phaser ph;

    LongRunningAction(String threadName, Phaser ph) {
        this.threadName = threadName;
        this.ph = ph;
        ph.register();
    }

    @Override
    public void run() {
        ph.arriveAndAwaitAdvance();
        try {
            Thread.sleep(20);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        ph.arriveAndDeregister();
    }
}

Lorsque notre classe d’actions est instanciée, nous nous enregistrons auprès de l’instance Phaser à l’aide de la méthode register () . Cela augmentera le nombre de threads utilisant ce Phaser. spécifique

L’appel à arriveAndAwaitAdvance () fera en sorte que le thread actuel attend sur la barrière. Comme mentionné précédemment, lorsque le nombre de partis arrivés devient le même que le nombre de partis enregistrés, l’exécution se poursuivra.

Une fois le traitement terminé, le thread en cours se désenregistre en appelant la méthode arriveAndDeregister () .

Créons un scénario de test dans lequel nous allons démarrer trois threads LongRunningAction et bloquer sur la barrière. Ensuite, une fois l’action terminée, nous allons créer deux threads LongRunningAction supplémentaires qui traiteront la phase suivante.

Lors de la création de l’instance Phaser à partir du thread principal, nous transmettons 1 en tant qu’argument. Cela équivaut à appeler la méthode register () à partir du thread actuel. Nous faisons cela parce que, lorsque nous créons trois threads de travail, le thread principal est un coordinateur et que, par conséquent, le Phaser doit avoir quatre threads enregistrés:

ExecutorService executorService = Executors.newCachedThreadPool();
Phaser ph = new Phaser(1);

assertEquals(0, ph.getPhase());

La phase après l’initialisation est égale à zéro.

La classe Phaser a un constructeur dans lequel nous pouvons lui transmettre une instance parent. C’est utile dans les cas où nous avons un grand nombre de parties qui subiraient des coûts de conflit de synchronisation énormes.

Dans de telles situations, des instances de Phasers peuvent être configurées pour que des groupes de sous-phaseurs partagent un parent commun.

Commençons ensuite par trois threads d’action LongRunningAction , qui attendront sur la barrière jusqu’à ce que nous appelions la méthode arriveAndAwaitAdvance () à partir du thread principal.

N’oubliez pas que nous avons initialisé notre Phaser avec 1 et appelé register () trois fois de plus. Maintenant, trois threads d’action ont annoncé qu’ils étaient arrivés à la barrière. Un appel supplémentaire de arriveAndAwaitAdvance () est donc nécessaire - celui du thread principal:

executorService.submit(new LongRunningAction("thread-1", ph));
executorService.submit(new LongRunningAction("thread-2", ph));
executorService.submit(new LongRunningAction("thread-3", ph));

ph.arriveAndAwaitAdvance();

assertEquals(1, ph.getPhase());

Une fois cette phase terminée, la méthode getPhase () en renverra un car le programme a terminé le traitement de la première étape de l’exécution.

Disons que deux threads doivent effectuer la prochaine phase du traitement.

Nous pouvons utiliser Phaser pour y parvenir, car cela nous permet de configurer de manière dynamique le nombre de threads devant attendre sur la barrière. Nous commençons deux nouveaux threads, mais ceux-ci ne continueront pas à s’exécuter avant l’appel de la arriveAndAwaitAdvance () à partir du thread principal (comme dans le cas précédent):

executorService.submit(new LongRunningAction("thread-4", ph));
executorService.submit(new LongRunningAction("thread-5", ph));
ph.arriveAndAwaitAdvance();

assertEquals(2, ph.getPhase());

ph.arriveAndDeregister();

Après cela, la méthode getPhase () retournera un nombre de phase égal à deux. Lorsque nous voulons terminer notre programme, nous devons appeler la méthode arriveAndDeregister () car le thread principal est toujours enregistré dans le Phaser. Lorsque la désinscription provoque la suppression du nombre de partis enregistrés, le Phaser est terminated. les appels aux méthodes de synchronisation ne seront plus bloqués et seront immédiatement renvoyés.

L’exécution du programme produira la sortie suivante (le code source complet avec les instructions de ligne d’impression se trouve dans le référentiel de code):

This is phase 0
This is phase 0
This is phase 0
Thread thread-2 before long running action
Thread thread-1 before long running action
Thread thread-3 before long running action
This is phase 1
This is phase 1
Thread thread-4 before long running action
Thread thread-5 before long running action

Nous voyons que tous les threads attendent l’exécution jusqu’à ce que la barrière s’ouvre. La prochaine phase de l’exécution est effectuée uniquement lorsque la précédente s’est terminée avec succès.

4. Conclusion

Dans ce tutoriel, nous avons examiné la construction Phaser de java.util.concurrent et avons implémenté la logique de coordination à plusieurs phases à l’aide de la classe Phaser .

Vous trouverez la mise en œuvre de tous ces exemples et extraits de code dans le projet GitHub - il s’agit d’un projet Maven, il devrait donc être facile à importer et à exécuter tel quel.