Méthodes wait and notify () en Java

Méthodes wait and notify () en Java

1. introduction

Dans cet article, nous allons examiner l'un des mécanismes les plus fondamentaux de Java: la synchronisation des threads.

Nous aborderons d'abord quelques termes et méthodologies essentiels liés à la concurrence.

Et nous allons développer une application simple - où nous traiterons des problèmes de concurrence, dans le but de mieux comprendrewait() etnotify().

2. Synchronisation des threads en Java

Dans un environnement multithread, plusieurs threads peuvent essayer de modifier la même ressource. Si les threads ne sont pas gérés correctement, cela entraînera bien sûr des problèmes de cohérence.

2.1. Blocs gardés à Java

Un outil que nous pouvons utiliser pour coordonner les actions de plusieurs threads en Java est constitué des blocs protégés. Ces blocs vérifient une condition particulière avant de reprendre l'exécution.

Dans cet esprit, nous utiliserons:

Cela peut être mieux compris à partir du diagramme suivant, qui décrit le cycle de vie d'unThread:

image

Veuillez noter qu'il existe de nombreuses façons de contrôler ce cycle de vie; cependant, dans cet article, nous allons nous concentrer uniquement surwait() etnotify().

3. La méthodewait()

En termes simples, lorsque nous appelonswait() –, cela force le thread actuel à attendre qu'un autre thread appellenotify() ounotifyAll() sur le même objet.

Pour cela, le thread actuel doit posséder le moniteur de l’objet. SelonJavadocs, cela peut se produire lorsque:

  • nous avons exécuté la méthode d'instancesynchronized pour l'objet donné

  • nous avons exécuté le corps d’un blocsynchronized sur l’objet donné

  • en exécutant les méthodessynchronized static pour les objets de typeClass

Notez qu’un seul thread actif peut posséder le moniteur d’un objet à la fois.

Cette méthodewait() est fournie avec trois signatures surchargées. Jetons un œil à ceux-ci.

3.1. wait()

La méthodewait() oblige le thread actuel à attendre indéfiniment jusqu'à ce qu'un autre thread appellenotify() pour cet objet ounotifyAll().

3.2. wait(long timeout)

En utilisant cette méthode, nous pouvons spécifier un délai après lequel le thread sera automatiquement réveillé. Un thread peut être réveillé avant d'atteindre le délai d'expiration en utilisantnotify() ounotifyAll().

Notez qu'appelerwait(0) équivaut à appelerwait().

3.3. wait(long timeout, int nanos)

C’est là une autre signature offrant les mêmes fonctionnalités, la seule différence étant que nous pouvons fournir une précision supérieure.

Le délai d'expiration total (en nanosecondes) est calculé en1_000_000*timeout + nanos.

4. notify() etnotifyAll()

La méthodenotify() est utilisée pour réveiller les threads qui attendent un accès au moniteur de cet objet.

Il existe deux manières de notifier les threads en attente.

4.1. notify()

Pour tous les threads en attente sur le moniteur de cet objet (en utilisant l’une des méthodeswait()), la méthodenotify() avertit l’un d’entre eux de se réveiller arbitrairement. Le choix du fil à réveiller n’est pas déterministe et dépend de la mise en œuvre.

Puisquenotify() réveille un seul thread aléatoire, il peut être utilisé pour implémenter un verrouillage mutuellement exclusif où les threads effectuent des tâches similaires, mais dans la plupart des cas, il serait plus viable d'implémenternotifyAll().

4.2. notifyAll()

Cette méthode réveille simplement tous les threads en attente sur le moniteur de cet objet.

Les threads réveillés se termineront de la manière habituelle - comme n'importe quel autre thread.

Mais avant de permettre à leur exécution de continuer, toujoursdefine a quick check for the condition required to proceed with the thread - car il peut y avoir des situations où le thread s'est réveillé sans recevoir de notification (ce scénario est discuté plus loin dans un exemple).

5. Problème de synchronisation émetteur-récepteur

Maintenant que nous comprenons les bases, passons par une simple applicationSender -Receiver - qui utilisera les méthodeswait() etnotify() pour configurer la synchronisation entre elles:

  • LeSender est censé envoyer un paquet de données auxReceiver

  • LeReceiver ne peut pas traiter le paquet de données tant que leSender n'a pas fini de l'envoyer

  • De même, lesSender ne doivent pas tenter d’envoyer un autre paquet à moins que leReceiver n’ait déjà traité le paquet précédent

Créons d'abord la classeData qui se compose des donnéespacket qui seront envoyées deSender àReceiver. Nous utiliseronswait() etnotifyAll() pour configurer la synchronisation entre eux:

public class Data {
    private String packet;

    // True if receiver should wait
    // False if sender should wait
    private boolean transfer = true;

    public synchronized void send(String packet) {
        while (!transfer) {
            try {
                wait();
            } catch (InterruptedException e)  {
                Thread.currentThread().interrupt();
                Log.error("Thread interrupted", e);
            }
        }
        transfer = false;

        this.packet = packet;
        notifyAll();
    }

    public synchronized String receive() {
        while (transfer) {
            try {
                wait();
            } catch (InterruptedException e)  {
                Thread.currentThread().interrupt();
                Log.error("Thread interrupted", e);
            }
        }
        transfer = true;

        notifyAll();
        return packet;
    }
}

Décrivons ce qui se passe ici:

  • La variablepacket indique les données qui sont transférées sur le réseau

  • Nous avons une variablebooleantransfer – que lesSender etReceiver utiliseront pour la synchronisation:

    • Si cette variable esttrue, alors lesReceiver doivent attendreSender pour envoyer le message

    • S'il s'agit defalse, alorsSender doit attendre queReceiver reçoive le message

  • LeSender utilise la méthodesend() pour envoyer des données auxReceiver:

    • Sitransfer estfalse, nous attendrons en appelantwait() sur ce thread

    • Mais quand c'esttrue, nous basculons le statut, définissons notre message et appelonsnotifyAll() pour réveiller d'autres threads pour spécifier qu'un événement significatif s'est produit et ils peuvent vérifier s'ils peuvent continuer l'exécution

  • De même, lesReceiver utiliseront la méthodereceive():

    • Sitransfer a été défini surfalse parSender, alors seulement il continuera, sinon nous appelleronswait() sur ce thread

    • Lorsque la condition est remplie, nous basculons l'état, notifions à tous les threads en attente de se réveiller et renvoyons le paquet de données qui étaitReceiver

5.1. Pourquoi inclurewait() dans une bouclewhile?

Étant donné quenotify() etnotifyAll() réveille aléatoirement les threads qui attendent sur le moniteur de cet objet, il n’est pas toujours important que la condition soit remplie. Parfois, il peut arriver que le thread soit réveillé, mais la condition n’est pas encore satisfaite.

Nous pouvons également définir un chèque pour nous protéger des faux réveils - dans lesquels un thread peut sortir d'une attente sans avoir jamais reçu de notification.

5.2. Pourquoi avons-nous besoin de synchroniser les méthodes send() etreceive()?

Nous avons placé ces méthodes dans les méthodessynchronized pour fournir des verrous intrinsèques. Si un thread appelant la méthodewait() ne possède pas le verrou inhérent, une erreur sera générée.

Nous allons maintenant créerSender etReceiver et implémenter l'interfaceRunnable sur les deux afin que leurs instances puissent être exécutées par un thread.

Voyons d'abord comment fonctionneraSender:

public class Sender implements Runnable {
    private Data data;

    // standard constructors

    public void run() {
        String packets[] = {
          "First packet",
          "Second packet",
          "Third packet",
          "Fourth packet",
          "End"
        };

        for (String packet : packets) {
            data.send(packet);

            // Thread.sleep() to mimic heavy server-side processing
            try {
                Thread.sleep(ThreadLocalRandom.current().nextInt(1000, 5000));
            } catch (InterruptedException e)  {
                Thread.currentThread().interrupt();
                Log.error("Thread interrupted", e);
            }
        }
    }
}

Pour ceSender:

  • Nous créons des paquets de données aléatoires qui seront envoyés sur le réseau dans le tableaupackets[]

  • Pour chaque paquet, nous appelons simplementsend()

  • Ensuite, nous appelonsThread.sleep() avec un intervalle aléatoire pour imiter un traitement lourd côté serveur

Enfin, implémentons nosReceiver:

public class Receiver implements Runnable {
    private Data load;

    // standard constructors

    public void run() {
        for(String receivedMessage = load.receive();
          !"End".equals(receivedMessage);
          receivedMessage = load.receive()) {

            System.out.println(receivedMessage);

            // ...
            try {
                Thread.sleep(ThreadLocalRandom.current().nextInt(1000, 5000));
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                Log.error("Thread interrupted", e);
            }
        }
    }
}

Ici, nous appelons simplementload.receive() dans la boucle jusqu'à ce que nous obtenions le dernier paquet de données“End”.

Voyons maintenant cette application en action:

public static void main(String[] args) {
    Data data = new Data();
    Thread sender = new Thread(new Sender(data));
    Thread receiver = new Thread(new Receiver(data));

    sender.start();
    receiver.start();
}

Nous recevrons le résultat suivant:

First packet
Second packet
Third packet
Fourth packet

Et nous voici -we’ve received all data packets in the right, sequential order et avons réussi à établir la communication correcte entre notre émetteur et notre destinataire.

6. Conclusion

Dans cet article, nous avons abordé certains concepts de base de la synchronisation en Java; plus spécifiquement, nous nous sommes concentrés sur la façon dont nous pouvons utiliserwait() etnotify() pour résoudre des problèmes de synchronisation intéressants. Enfin, nous avons analysé un exemple de code dans lequel nous avons appliqué ces concepts dans la pratique.

Avant de terminer ici, il convient de mentionner que toutes ces API de bas niveau, telles quewait(),notify() etnotifyAll() - sont des méthodes traditionnelles qui fonctionnent bien, mais les mécanismes de plus haut niveau sont souvent plus simples et meilleures - comme les interfaces nativesLock etCondition de Java (disponibles dans le packagejava.util.concurrent.locks).

Pour plus d'informations sur le packagejava.util.concurrent, consultez notre article suroverview of the java.util.concurrent, etLock etCondition sont couverts dans lesguide to java.util.concurrent.Locks, here.

Comme toujours, les extraits de code complets utilisés dans cet article sont disponiblesover on GitHub.