Un scénario simple utilisant wait () et notify () en java

181

Puis-je obtenir un scénario simple complet, c'est-à-dire un didacticiel qui suggère comment cela doit être utilisé, en particulier avec une file d'attente?

Olaseni
la source

Réponses:

269

Le wait()etnotify() méthodes sont conçues pour fournir un mécanisme permettant à un thread de se bloquer jusqu'à ce qu'une condition spécifique soit remplie. Pour cela, je suppose que vous souhaitez écrire une implémentation de file d'attente de blocage, où vous avez un stockage d'éléments de taille fixe.

La première chose à faire est d'identifier les conditions que vous souhaitez que les méthodes attendent. Dans ce cas, vous souhaiterez que la put()méthode se bloque jusqu'à ce qu'il y ait de l'espace libre dans le magasin, et vous souhaiterez que la take()méthode se bloque jusqu'à ce qu'il y ait un élément à retourner.

public class BlockingQueue<T> {

    private Queue<T> queue = new LinkedList<T>();
    private int capacity;

    public BlockingQueue(int capacity) {
        this.capacity = capacity;
    }

    public synchronized void put(T element) throws InterruptedException {
        while(queue.size() == capacity) {
            wait();
        }

        queue.add(element);
        notify(); // notifyAll() for multiple producer/consumer threads
    }

    public synchronized T take() throws InterruptedException {
        while(queue.isEmpty()) {
            wait();
        }

        T item = queue.remove();
        notify(); // notifyAll() for multiple producer/consumer threads
        return item;
    }
}

Il y a quelques points à noter sur la manière dont vous devez utiliser les mécanismes d'attente et de notification.

Tout d'abord, vous devez vous assurer que tous les appels vers wait()ou se notify()trouvent dans une région de code synchronisée (les appels wait()et notify()étant synchronisés sur le même objet). La raison à cela (autre que les problèmes de sécurité des threads standard) est due à quelque chose que l'on appelle un signal manqué.

Un exemple de ceci est qu'un thread peut appeler put() lorsque la file d'attente est pleine, il vérifie ensuite la condition, voit que la file d'attente est pleine, mais avant de pouvoir bloquer un autre thread est planifié. Ce deuxième thread est alors take()un élément de la file d'attente et notifie aux threads en attente que la file d'attente n'est plus pleine. Cependant, comme le premier thread a déjà vérifié la condition, il appellera simplementwait() après avoir été replanifié, même s'il pourrait progresser.

En synchronisant sur un objet partagé, vous pouvez vous assurer que ce problème ne se produit pas, car le deuxième thread take() appel ne pourra pas progresser tant que le premier thread n'aura pas été bloqué.

Deuxièmement, vous devez placer la condition que vous vérifiez dans une boucle while, plutôt qu'une instruction if, en raison d'un problème connu sous le nom de faux réveils. C'est là qu'un thread en attente peut parfois être réactivé sans notify()être appelé. Mettre cette vérification dans une boucle while garantira que si un faux réveil se produit, la condition sera revérifiée et le thread appellerawait() nouveau.


Comme certaines des autres réponses l'ont mentionné, Java 1.5 a introduit une nouvelle bibliothèque de concurrence (dans le java.util.concurrentpackage) qui a été conçue pour fournir une abstraction de plus haut niveau sur le mécanisme d'attente / notification. En utilisant ces nouvelles fonctionnalités, vous pouvez réécrire l'exemple d'origine comme ceci:

public class BlockingQueue<T> {

    private Queue<T> queue = new LinkedList<T>();
    private int capacity;
    private Lock lock = new ReentrantLock();
    private Condition notFull = lock.newCondition();
    private Condition notEmpty = lock.newCondition();

    public BlockingQueue(int capacity) {
        this.capacity = capacity;
    }

    public void put(T element) throws InterruptedException {
        lock.lock();
        try {
            while(queue.size() == capacity) {
                notFull.await();
            }

            queue.add(element);
            notEmpty.signal();
        } finally {
            lock.unlock();
        }
    }

    public T take() throws InterruptedException {
        lock.lock();
        try {
            while(queue.isEmpty()) {
                notEmpty.await();
            }

            T item = queue.remove();
            notFull.signal();
            return item;
        } finally {
            lock.unlock();
        }
    }
}

Bien sûr, si vous avez réellement besoin d'une file d'attente de blocage, vous devez utiliser une implémentation de l' interface BlockingQueue .

En outre, pour des choses comme celle-ci, je recommande vivement Java Concurrency en pratique , car il couvre tout ce que vous pourriez vouloir savoir sur les problèmes et les solutions liés à la concurrence.

Jared Russell
la source
7
@greuze, notifyne réveille qu'un seul thread. Si deux threads consommateurs sont en concurrence pour supprimer un élément, une notification peut réveiller l'autre thread consommateur, qui ne peut rien y faire et va se rendormir (au lieu du producteur, qui nous espérions insérer un nouvel élément.) Parce que le thread producteur n'est pas réveillé, rien n'est inséré et maintenant les trois threads vont dormir indéfiniment. J'ai supprimé mon commentaire précédent car il disait (à tort) qu'un faux réveil était la cause du problème (ce n'est pas le cas.)
finnw
1
@finnw Autant que je sache, le problème que vous avez repéré peut être résolu en utilisant notifyAll (). Ai-je raison?
Clint Eastwood
1
L'exemple donné ici par @Jared est plutôt bon mais a une sérieuse chute. Dans le code, toutes les méthodes ont été marquées comme synchronisées, mais AUCUNE DEUX MÉTHODES SYNCHRONISÉES NE PEUVENT ÊTRE EXÉCUTÉES EN MÊME TEMPS, alors comment se fait-il qu'il y ait un deuxième fil dans l'image.
Shivam Aggarwal
10
@ Brut3Forc3 vous devez lire le javadoc de wait (): il dit: Le thread libère la propriété de ce moniteur . Ainsi, dès que wait () est appelé, le moniteur est libéré et un autre thread peut exécuter une autre méthode synchronisée de la file d'attente.
JB Nizet
1
@JBNizet. "Un exemple de ceci est qu'un thread peut appeler put () lorsque la file d'attente est pleine, il vérifie ensuite la condition, voit que la file d'attente est pleine, mais avant de pouvoir bloquer un autre thread est planifié". Voici comment se fait le deuxième thread est programmé si wait n'a pas encore été appelé.
Shivam Aggarwal
148

Pas un exemple de file d'attente, mais extrêmement simple :)

class MyHouse {
    private boolean pizzaArrived = false;

    public void eatPizza(){
        synchronized(this){
            while(!pizzaArrived){
                wait();
            }
        }
        System.out.println("yumyum..");
    }

    public void pizzaGuy(){
        synchronized(this){
             this.pizzaArrived = true;
             notifyAll();
        }
    }
}

Quelques points importants:
1) NE JAMAIS faire

 if(!pizzaArrived){
     wait();
 }

Utilisez toujours while (condition), car

  • a) les threads peuvent sporadiquement se réveiller de l'état d'attente sans être averti par personne. (même si le pizzaiolo ne sonnait pas le carillon, quelqu'un déciderait d'essayer de manger la pizza.).
  • b) Vous devez vérifier à nouveau la condition après avoir acquis le verrou synchronisé. Disons que la pizza ne dure pas éternellement. Vous vous réveillez, faites la queue pour la pizza, mais ce n'est pas suffisant pour tout le monde. Si vous ne vérifiez pas, vous pourriez manger du papier! :) (probablement un meilleur exemple serait while(!pizzaExists){ wait(); }.

2) Vous devez maintenir le verrou (synchronisé) avant d'appeler wait / nofity. Les threads doivent également acquérir un verrouillage avant de se réveiller.

3) Essayez d'éviter d'acquérir un verrou dans votre bloc synchronisé et efforcez-vous de ne pas invoquer de méthodes étrangères (méthodes dont vous ne savez pas avec certitude ce qu'elles font). Si nécessaire, assurez-vous de prendre des mesures pour éviter les blocages.

4) Soyez prudent avec notify (). Restez avec notifyAll () jusqu'à ce que vous sachiez ce que vous faites.

5) Dernier point, mais non le moindre, lisez Java Concurrency en pratique !

Enno Shioji
la source
1
Pourriez-vous expliquer pourquoi ne pas utiliser "if (! PizzaArrived) {wait ();}"?
Tout le monde
2
@Everyone: Ajout d'une explication. HTH.
Enno Shioji
1
pourquoi utiliser le pizzaArriveddrapeau? si le drapeau est changé sans un appel, notifyil n'aura aucun effet. Aussi juste avec waitet notifyappelle l'exemple fonctionne.
Pablo Fernandez
2
Je ne comprends pas - le thread 1 exécute la méthode eatPizza () et entre dans le bloc synchronisé supérieur et se synchronise sur la classe MyHouse. Aucune pizza n'est encore arrivée, elle attend donc. Le thread 2 essaie maintenant de livrer la pizza en appelant la méthode pizzaGuy (); mais ne peut pas car le thread 1 possède déjà le verrou et il ne l'abandonne pas (il attend perpétuellement). En fait, le résultat est un blocage - le thread 1 attend que le thread 2 exécute la méthode notifyAll (), tandis que le thread 2 attend que le thread 1 abandonne le verrou sur la classe MyHouse ... Qu'est-ce qui me manque ici?
flamming_python
1
Non, lorsqu'une variable est gardée par synchronizedmot-clé, il est redondant de déclarer la variable volatile, et il est recommandé de l'éviter pour éviter toute confusion @mrida
Enno Shioji
37

Même si vous avez demandé wait()et notify()spécifiquement, je pense que cette citation est encore assez importante:

Josh Bloch, Effective Java 2nd Edition , Item 69: Préférez les utilitaires de concurrence à waitet notify(soulignement):

Étant donné la difficulté à utiliser waitet notifycorrectement, vous devriez utiliser les utilitaires de concurrence de plus haut niveau à la place [...] d'utiliser waitet c'est notifydirectement comme la programmation en "langage d'assemblage de concurrence", par rapport au langage de plus haut niveau fourni par java.util.concurrent. Il y a rarement, voire jamais, de raison d'utiliser waitet notifydans un nouveau code .

lubrifiants polygènes
la source
Les BlockingQueueS fournis dans le package java.util.concurrent ne sont pas persistants. Que pouvons-nous utiliser lorsque la file d'attente doit être persistante? c'est-à-dire que si le système tombe en panne avec 20 éléments dans la file d'attente, j'ai besoin de ceux-ci lorsque le système redémarre. Comme les files d'attente java.util.concurrent semblent toutes être «en mémoire», y a-t-il un moyen de les utiliser telles quelles / piratées / remplacées pour fournir des implémentations capables de persistance?
Volksman
1
Peut-être que la file d'attente de sauvegarde pourrait être fournie? c'est-à-dire que nous fournirions une implémentation d'interface de file d'attente persistante.
Volksman
C'est très bon de mentionner dans ce contexte que vous n'auriez jamais besoin d'utiliser le notify()et le wait()nouveau
Chaklader Asfak Arefe
7

Avez-vous jeté un œil à ce didacticiel Java ?

De plus, je vous conseillerais de ne pas jouer avec ce genre de choses dans de vrais logiciels. Il est bon de jouer avec pour savoir ce que c'est, mais la concurrence a des pièges partout. Il est préférable d'utiliser des abstractions de niveau supérieur et des collections synchronisées ou des files d'attente JMS si vous créez des logiciels pour d'autres personnes.

C'est du moins ce que je fais. Je ne suis pas un expert de la concurrence, donc je reste loin de gérer les threads à la main dans la mesure du possible.

extraneon
la source
2

Exemple

public class myThread extends Thread{
     @override
     public void run(){
        while(true){
           threadCondWait();// Circle waiting...
           //bla bla bla bla
        }
     }
     public synchronized void threadCondWait(){
        while(myCondition){
           wait();//Comminucate with notify()
        }
     }

}
public class myAnotherThread extends Thread{
     @override
     public void run(){
        //Bla Bla bla
        notify();//Trigger wait() Next Step
     }

}
Ferhat KOÇER
la source
0

Exemple pour wait () et notifyall () dans Threading.

Une liste de tableaux statiques synchronisés est utilisée comme ressource et la méthode wait () est appelée si la liste de tableaux est vide. La méthode notify () est appelée une fois qu'un élément est ajouté pour la liste de tableaux.

public class PrinterResource extends Thread{

//resource
public static List<String> arrayList = new ArrayList<String>();

public void addElement(String a){
    //System.out.println("Add element method "+this.getName());
    synchronized (arrayList) {
        arrayList.add(a);
        arrayList.notifyAll();
    }
}

public void removeElement(){
    //System.out.println("Remove element method  "+this.getName());
    synchronized (arrayList) {
        if(arrayList.size() == 0){
            try {
                arrayList.wait();
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }else{
            arrayList.remove(0);
        }
    }
}

public void run(){
    System.out.println("Thread name -- "+this.getName());
    if(!this.getName().equalsIgnoreCase("p4")){
        this.removeElement();
    }
    this.addElement("threads");

}

public static void main(String[] args) {
    PrinterResource p1 = new PrinterResource();
    p1.setName("p1");
    p1.start();

    PrinterResource p2 = new PrinterResource();
    p2.setName("p2");
    p2.start();


    PrinterResource p3 = new PrinterResource();
    p3.setName("p3");
    p3.start();


    PrinterResource p4 = new PrinterResource();
    p4.setName("p4");
    p4.start();     

    try{
        p1.join();
        p2.join();
        p3.join();
        p4.join();
    }catch(InterruptedException e){
        e.printStackTrace();
    }
    System.out.println("Final size of arraylist  "+arrayList.size());
   }
}
srinivas
la source
1
plz revérifiez cette ligne if(arrayList.size() == 0), je pense que cela pourrait être une erreur ici.
Wizmann