Pourquoi doit attendre () toujours être dans un bloc synchronisé


257

Nous savons tous que pour appeler Object.wait(), cet appel doit être placé dans un bloc synchronisé, sinon un IllegalMonitorStateExceptionest lancé. Mais quelle est la raison de cette restriction? Je sais que cela wait()libère le moniteur, mais pourquoi devons-nous acquérir explicitement le moniteur en synchronisant un bloc particulier, puis libérer le moniteur en appelant wait()?

Quels sont les dommages potentiels s'il était possible d'invoquer en wait()dehors d'un bloc synchronisé, en conservant sa sémantique - en suspendant le thread appelant?

Réponses:


232

A wait()n'a de sens que lorsqu'il y en a aussi notify(), il s'agit donc toujours de communication entre les threads, et qui nécessite une synchronisation pour fonctionner correctement. On pourrait faire valoir que cela devrait être implicite, mais cela n'aiderait pas vraiment, pour la raison suivante:

Sémantiquement, vous n'êtes jamais juste wait(). Vous avez besoin d'une condition pour être certifié, et si ce n'est pas le cas, vous attendez qu'elle le soit. Donc ce que vous faites vraiment

if(!condition){
    wait();
}

Mais la condition est définie par un thread distinct, donc pour que cela fonctionne correctement, vous avez besoin d'une synchronisation.

Quelques autres choses qui ne vont pas avec cela, où le fait que votre thread quitte l'attente ne signifie pas que la condition que vous recherchez est vraie:

  • Vous pouvez obtenir de faux réveils (ce qui signifie qu'un thread peut se réveiller sans avoir reçu de notification), ou

  • La condition peut être définie, mais un troisième thread rend la condition fausse à nouveau au moment où le thread en attente se réveille (et rachète le moniteur).

Pour traiter ces cas, ce dont vous avez vraiment besoin est toujours une variante de ceci:

synchronized(lock){
    while(!condition){
        lock.wait();
    }
}

Mieux encore, ne vous souciez pas du tout des primitives de synchronisation et travaillez avec les abstractions proposées dans les java.util.concurrentpackages.


3
Il y a aussi une discussion détaillée ici, disant essentiellement la même chose. coding.derkeiler.com/Archive/Java/comp.lang.java.programmer/…

1
btw, si vous ne devez pas ignorer le drapeau interrompu, la boucle doit Thread.interrupted()également être vérifiée.
bestsss

2
Je peux toujours faire quelque chose comme: while (! Condition) {synchronized (this) {wait ();}} ce qui signifie qu'il y a toujours une course entre la vérification de la condition et l'attente même si wait () est correctement appelé dans un bloc synchronisé. Y a-t-il donc une autre raison derrière cette restriction, peut-être en raison de la façon dont elle est implémentée en Java?
shrini1000

9
Un autre scénario désagréable: la condition est fausse, nous sommes sur le point d'entrer dans wait (), puis un autre thread modifie la condition et appelle notify (). Parce que nous ne sommes pas encore en attente (), nous manquerons cette notification (). En d'autres termes, tester et attendre, ainsi que modifier et notifier doivent être atomiques .

1
@Nullpointer: si c'est un type qui peut être écrit de manière atomique (comme le booléen implicite en l'utilisant directement dans une clause if) et qu'il n'y a pas d'interdépendance avec d'autres données partagées, vous pourriez vous en sortir en le déclarant volatile. Mais vous avez besoin de cela ou d'une synchronisation pour vous assurer que la mise à jour sera rapidement visible pour les autres threads.
Michael Borgwardt

283

Quels sont les dommages potentiels s'il était possible d'invoquer en wait()dehors d'un bloc synchronisé, en conservant sa sémantique - en suspendant le thread appelant?

Illustrons les problèmes que nous rencontrerions si nous wait()pouvions les appeler en dehors d'un bloc synchronisé avec un exemple concret .

Supposons que nous devions implémenter une file d'attente de blocage (je sais, il y en a déjà une dans l'API :)

Une première tentative (sans synchronisation) pourrait ressembler à quelque chose comme ci-dessous

class BlockingQueue {
    Queue<String> buffer = new LinkedList<String>();

    public void give(String data) {
        buffer.add(data);
        notify();                   // Since someone may be waiting in take!
    }

    public String take() throws InterruptedException {
        while (buffer.isEmpty())    // don't use "if" due to spurious wakeups.
            wait();
        return buffer.remove();
    }
}

C'est ce qui pourrait potentiellement arriver:

  1. Un thread consommateur appelle take()et voit que le buffer.isEmpty().

  2. Avant que le thread consommateur continue à appeler wait(), un thread producteur arrive et appelle un complet give(), c'est-à-direbuffer.add(data); notify();

  3. Le thread consommateur va maintenant appeler wait()(et rater le notify()qui vient d'être appelé).

  4. En cas de malchance, le thread producteur ne produira pas plus give()du fait que le thread consommateur ne se réveille jamais, et nous avons un blocage.

Une fois que vous avez compris le problème, la solution est évidente: utilisez synchronizedpour vous assurer qu'il notifyn'est jamais appelé entre isEmptyet wait.

Sans entrer dans les détails: ce problème de synchronisation est universel. Comme le souligne Michael Borgwardt, attendre / notifier concerne la communication entre les threads, vous vous retrouverez donc toujours avec une condition de concurrence similaire à celle décrite ci-dessus. C'est pourquoi la règle "n'attendre qu'à l'intérieur synchronisée" est appliquée.


Un paragraphe du lien publié par @Willie le résume assez bien:

Vous avez besoin d'une garantie absolue que le serveur et le notifiant s'accordent sur l'état du prédicat. Le serveur vérifie légèrement l'état du prédicat à un moment donné AVANT qu'il ne se mette en veille, mais cela dépend pour l'exactitude du fait que le prédicat est vrai QUAND il se met en veille. Il y a une période de vulnérabilité entre ces deux événements, ce qui peut interrompre le programme.

Le prédicat sur lequel le producteur et le consommateur doivent se mettre d'accord est dans l'exemple ci-dessus buffer.isEmpty(). Et l'accord est résolu en s'assurant que l'attente et la notification sont effectuées en synchronizedblocs.


Ce message a été réécrit en tant qu'article ici: Java: Pourquoi attendre doit être appelé dans un bloc synchronisé


De plus, pour vous assurer que les modifications apportées à la condition sont visibles immédiatement après la fin de wait (), je suppose. Sinon, également un verrou mortel puisque le notify () a déjà été appelé.
Surya Wijaya Madjid

Intéressant, mais notez que le simple fait d'appeler synchronisé ne résoudra pas toujours ces problèmes en raison de la nature "peu fiable" de wait () et notify (). En savoir plus ici: stackoverflow.com/questions/21439355/… . La raison pour laquelle la synchronisation est nécessaire réside dans l'architecture matérielle (voir ma réponse ci-dessous).
Marcus

mais si ajouter return buffer.remove();dans le bloc mais après wait();, ça marche?
BobJiang

@BobJiang, non, le fil peut être réveillé pour des raisons autres que quelqu'un appelant give. En d'autres termes, le tampon peut être vide même après les waitretours.
aioobe

Je n'ai que Thread.currentThread().wait();dans la mainfonction entourée de try-catch pour InterruptedException. Sans synchronizedbloc, cela me donne la même exception IllegalMonitorStateException. Qu'est-ce qui le rend maintenant illégal? Cela fonctionne à l'intérieur du synchronizedbloc.
Shashwat

12

@Rollerball a raison. Le wait()est appelé, afin que le thread puisse attendre qu'une condition se produise lorsque cet wait()appel se produit, le thread est obligé de renoncer à son verrou.
Pour abandonner quelque chose, vous devez d'abord le posséder. Le thread doit d'abord posséder le verrou. D'où la nécessité de l'appeler dans unsynchronized méthode / un bloc.

Oui, je suis d'accord avec toutes les réponses ci-dessus concernant les dommages / incohérences potentiels si vous n'avez pas vérifié la condition dans la synchronizedméthode / le blocage. Cependant, comme l'a souligné @ shrini1000, le simple fait d'appeler wait()dans un bloc synchronisé n'empêchera pas cette incohérence de se produire.

Voici une belle lecture ..


5
@Popeye Expliquez «correctement» correctement. Votre commentaire ne sert à personne.
Marquis de Lorne

4

Le problème que cela peut causer si vous le faites synchronisez pas avant wait()est le suivant:

  1. Si le 1er thread entre makeChangeOnX()et vérifie la condition while, et c'est true( x.metCondition()retourne false, signifie x.conditionest false) donc il y entrera. Puis juste avant la wait()méthode, un autre thread va àsetConditionToTrue() et définit le x.conditionà trueet notifyAll().
  2. Ensuite seulement après cela, le 1er thread entrera dans sa wait()méthode (non affecté par lenotifyAll() qui s'est passé quelques instants auparavant). Dans ce cas, le 1er thread attendra qu'un autre thread s'exécute setConditionToTrue(), mais cela pourrait ne pas se reproduire.

Mais si vous mettez synchronizedavant les méthodes qui changent l'état de l'objet, cela ne se produira pas.

class A {

    private Object X;

    makeChangeOnX(){
        while (! x.getCondition()){
            wait();
            }
        // Do the change
    }

    setConditionToTrue(){
        x.condition = true; 
        notifyAll();

    }
    setConditionToFalse(){
        x.condition = false;
        notifyAll();
    }
    bool getCondition(){
        return x.condition;
    }
}

2

Nous savons tous que les méthodes wait (), notify () et notifyAll () sont utilisées pour les communications entre threads. Pour se débarrasser des signaux manqués et des problèmes de réveil parasites, le fil d'attente attend toujours sous certaines conditions. par exemple-

boolean wasNotified = false;
while(!wasNotified) {
    wait();
}

Ensuite, la notification de la variable wasNotified des ensembles de threads à true et notifie.

Chaque thread a son cache local, donc toutes les modifications y sont d'abord écrites puis promues progressivement dans la mémoire principale.

Si ces méthodes n'avaient pas été invoquées dans le bloc synchronisé, la variable wasNotified ne serait pas vidée dans la mémoire principale et serait là dans le cache local du thread, de sorte que le thread en attente continuera d'attendre le signal bien qu'il ait été réinitialisé en notifiant le thread.

Pour résoudre ces types de problèmes, ces méthodes sont toujours invoquées à l'intérieur du bloc synchronisé, ce qui garantit qu'au démarrage du bloc synchronisé, tout sera lu dans la mémoire principale et vidé dans la mémoire principale avant de quitter le bloc synchronisé.

synchronized(monitor) {
    boolean wasNotified = false;
    while(!wasNotified) {
        wait();
    }
}

Merci, j'espère que cela clarifiera.


1

Cela a essentiellement à voir avec l'architecture matérielle (c'est-à-dire la RAM et les caches ).

Si vous n'utilisez pas synchronizedavec wait()ou notify(), un autre thread pourrait entrer dans le même bloc au lieu d'attendre que le moniteur y entre. De plus, lorsque, par exemple, l'accès à un tableau sans bloc synchronisé, un autre thread peut ne pas y voir le changement ... en fait, un autre thread ne verra aucun changement lorsqu'il a déjà une copie du tableau dans le cache de niveau x ( alias caches de 1er / 2e / 3e niveau) du noyau CPU de gestion des threads.

Mais les blocs synchronisés ne sont qu'un côté de la médaille: si vous accédez réellement à un objet dans un contexte synchronisé à partir d'un contexte non synchronisé, l'objet ne sera toujours pas synchronisé même dans un bloc synchronisé, car il contient une propre copie du objet dans son cache. J'ai écrit à propos de ces problèmes ici: https://stackoverflow.com/a/21462631 et lorsqu'un verrou contient un objet non final, la référence de l'objet peut-elle toujours être modifiée par un autre thread?

De plus, je suis convaincu que les caches de niveau x sont responsables de la plupart des erreurs d'exécution non reproductibles. C'est parce que les développeurs n'apprennent généralement pas les choses de bas niveau, comme le fonctionnement du processeur ou la façon dont la hiérarchie de la mémoire affecte le fonctionnement des applications: http://en.wikipedia.org/wiki/Memory_hierarchy

Cela reste une énigme pour laquelle les classes de programmation ne commencent pas par la hiérarchie de la mémoire et l'architecture du CPU en premier. "Bonjour tout le monde" n'aidera pas ici. ;)


1
Je viens de découvrir un site Web qui l'explique parfaitement et en profondeur: javamex.com/tutorials/…
Marcus

Hmm .. je ne suis pas sûr de suivre. Si la mise en cache était la seule raison de mettre en attente et notifier à l'intérieur synchronisé, pourquoi la synchronisation n'est-elle pas placée dans l'implémentation de wait / notify?
aioobe du

Bonne question, car attendre / notifier pourrait très bien être des méthodes synchronisées ... peut-être que les anciens développeurs Java de Sun connaissent la réponse? Jetez un œil au lien ci-dessus, ou peut-être que cela vous aidera également: docs.oracle.com/javase/specs/jls/se7/html/jls-17.html
Marcus

Une raison pourrait être: dans les premiers jours de Java, il n'y avait aucune erreur de compilation lors de l'appel non synchronisé avant d'effectuer ces opérations multithreading. Au lieu de cela, il n'y avait que des erreurs d'exécution (par exemple coderanch.com/t/239491/java-programmer-SCJP/certification/… ). Peut-être qu'ils pensaient vraiment à @SUN que lorsque les programmeurs reçoivent ces erreurs, ils sont contactés, ce qui leur a peut-être donné l'occasion de vendre plus de leurs serveurs. Quand a-t-il changé? Peut-être Java 5.0 ou 6.0, mais en fait je ne me souviens pas d'être honnête ...
Marcus

TBH Je vois quelques problèmes avec votre réponse 1) Votre deuxième phrase n'a pas de sens: peu importe l' objet sur lequel un thread a un verrou. Quel que soit l'objet sur lequel deux threads se synchronisent, toutes les modifications sont rendues visibles. 2) Vous dites qu'un autre thread "ne verra " aucun changement. Cela devrait être "peut-être pas" . 3) Je ne sais pas pourquoi vous montez des caches de 1er / 2e / 3e niveau ... Ce qui importe ici, c'est ce que dit le modèle de mémoire Java et qui est spécifié dans JLS. Bien que l'architecture matérielle puisse aider à comprendre pourquoi JLS dit ce qu'elle fait, elle n'est strictement pas pertinente dans ce contexte.
aioobe

0

directement à partir de ce tutoriel java oracle:

Lorsqu'un thread appelle d.wait, il doit posséder le verrou intrinsèque pour d - sinon une erreur est levée. L'appel d'attente dans une méthode synchronisée est un moyen simple d'acquérir le verrou intrinsèque.


D'après la question posée par l'auteur, il ne semble pas que l'auteur de la question ait une compréhension claire de ce que j'ai cité dans le tutoriel. Et en plus, ma réponse explique "Pourquoi".
Rollerball

0

Lorsque vous appelez notify () à partir d'un objet t, java notifie une méthode t.wait () particulière. Mais, comment java recherche et notifie une méthode d'attente particulière.

java ne regarde que le bloc de code synchronisé qui a été verrouillé par l'objet t. java ne peut pas rechercher le code entier pour notifier un t.wait () particulier.


0

selon les documents:

Le thread actuel doit posséder le moniteur de cet objet. Le thread libère la propriété de ce moniteur.

wait()signifie simplement qu'elle libère le verrou sur l'objet. Ainsi, l'objet sera verrouillé uniquement dans le bloc / méthode synchronisé. Si le thread est en dehors du bloc de synchronisation signifie qu'il n'est pas verrouillé, s'il n'est pas verrouillé, que feriez-vous sur l'objet?


0

Attente du thread sur l' objet de surveillance (objet utilisé par le bloc de synchronisation), Il peut y avoir n nombre d'objets de surveillance dans tout le trajet d'un seul thread. Si le thread attend en dehors du bloc de synchronisation, il n'y a aucun objet de surveillance et aucun autre thread ne notifie l'accès à l'objet de surveillance, alors comment le thread en dehors du bloc de synchronisation saura-t-il qu'il a été notifié. C'est également l'une des raisons pour lesquelles wait (), notify () et notifyAll () sont dans la classe objet plutôt que dans la classe thread.

Fondamentalement, l'objet de surveillance est une ressource commune ici pour tous les threads, et les objets de surveillance ne peuvent être disponibles que dans le bloc de synchronisation.

class A {
   int a = 0;
  //something......
  public void add() {
   synchronization(this) {
      //this is your monitoring object and thread has to wait to gain lock on **this**
       }
  }
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.