311 votes

Pourquoi wait() doit-il toujours être dans un bloc synchronisé ?

Nous savons tous qu'afin d'invoquer Object.wait() cet appel doit être placé dans un bloc synchronisé, sinon un IllegalMonitorStateException est jeté. Mais quelle est la raison de cette restriction ? Je sais que wait() libère le moniteur, mais pourquoi devons-nous acquérir explicitement le moniteur en rendant un bloc particulier synchronisé et ensuite libérer le moniteur en appelant wait() ?

Quel est le dommage potentiel s'il était possible d'invoquer wait() en dehors d'un bloc synchronisé, en conservant sa sémantique - en suspendant le thread appelant ?

324voto

aioobe Points 158466

Quel est le dommage potentiel s'il était possible d'invoquer wait() en dehors d'un bloc synchronisé, en conservant sa sémantique - en suspendant le thread appelant ?

Illustrons les problèmes auxquels nous serions confrontés si wait() pourrait être appelé en dehors d'un bloc synchronisé avec un exemple concret .

Supposons que nous mettions en place une file d'attente bloquante (je sais, il y en a déjà une dans l'API :)

Une première tentative (sans synchronisation) pourrait ressembler à ce qui suit

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();
    }
}

Voilà ce qui pourrait se passer :

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

  2. Avant que le thread du consommateur n'appelle wait() un fil de producteur arrive et invoque une pleine give() c'est-à-dire, buffer.add(data); notify();

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

  4. S'il est malchanceux, le fil producteur ne produira pas plus de give() en raison du fait que le thread du 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 synchronized pour s'assurer notify n'est jamais appelé entre isEmpty y wait .

Sans entrer dans les détails : Ce problème de synchronisation est universel. Comme le fait remarquer Michael Borgwardt, wait/notify concerne la communication entre les threads, et vous finirez toujours par avoir une situation de course similaire à celle décrite ci-dessus. C'est la raison pour laquelle la règle "only wait inside synchronized" est appliquée.


Un paragraphe du lien posté par @Willie résume assez bien la situation :

Vous avez besoin d'une garantie absolue que le serveur et le notificateur sont d'accord sur l'état du prédicat. Le serveur vérifie l'état du prédicat à un moment légèrement AVANT qu'il ne s'endorme, mais il dépend pour être correct que le prédicat soit vrai QUAND il s'endort. Il y a une période de vulnérabilité entre ces deux événements, ce qui peut casser 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 le wait et le notify sont exécutés en synchronized blocs.


Ce post a été réécrit sous forme d'article ici : Java : Pourquoi wait doit être appelé dans un bloc synchronisé

0 votes

En outre, je pense que cela permet de s'assurer que les modifications apportées à la condition sont visibles immédiatement après la fin de la fonction wait(). Sinon, il y a également un blocage de l'accès puisque le notify() a déjà été appelé.

0 votes

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

0 votes

Mais si on ajoute return buffer.remove(); dans le bloc while mais après wait(); ça marche ?

262voto

Michael Borgwardt Points 181658

A wait() n'a de sens que lorsqu'il existe également un notify() Il s'agit donc toujours de la communication entre les threads, qui nécessite une synchronisation pour fonctionner correctement. On pourrait argumenter que cela devrait être implicite, mais cela ne serait pas vraiment utile, pour la raison suivante :

Sémantiquement, on ne peut jamais juste wait() . Il faut une certaine condition pour être satsifié, et si elle ne l'est pas, on attend qu'elle le soit. Donc ce que vous faites vraiment c'est

if(!condition){
    wait();
}

Mais la condition est définie par un thread séparé, donc pour que cela fonctionne correctement, il faut une synchronisation.

Il y a encore deux choses qui ne vont pas : le fait que le fil de discussion ait cessé d'attendre ne signifie pas que la condition que vous recherchez est vraie :

  • Vous pouvez obtenir des réveils intempestifs (c'est-à-dire qu'un thread peut se réveiller de l'attente sans avoir jamais reçu de notification), ou

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

Pour faire face à ces cas, ce dont vous avez réellement besoin est toujours une variation de ceci :

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

Mieux encore, ne vous occupez pas du tout des primitives de synchronisation et travaillez avec les abstractions offertes par le module java.util.concurrent paquets.

3 votes

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

1 votes

Au fait, si vous ne devez pas ignorer le drapeau interrompu, la boucle doit vérifier Thread.interrupted() également.

3 votes

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 une autre raison derrière cette restriction, peut-être due à la façon dont elle est implémentée en Java ?

17voto

@Rollerball a raison. Le site wait() est appelé, de sorte que le thread peut attendre qu'une certaine condition se produise lorsque cette fonction wait() se produit, le thread est obligé de renoncer à son verrou.
Pour renoncer à quelque chose, il faut d'abord le posséder. Le thread doit d'abord posséder le verrou. D'où la nécessité de l'appeler à l'intérieur d'un synchronized méthode/bloc.

Oui, je suis d'accord avec toutes les réponses ci-dessus concernant les dommages/incompatibilités potentiels si vous n'avez pas vérifié l'état des lieux. synchronized méthode/bloc. 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 bonne lecture

5 votes

@Popeye Expliquez "correctement" correctement. Votre commentaire n'est d'aucune utilité pour qui que ce soit.

3voto

Marcus Points 677

Il s'agit essentiellement de l'architecture matérielle (c'est-à-dire RAM y caches ).

Si vous n'utilisez pas synchronized en même temps que wait() ou notify() dans un autre fil podría entrer dans le même bloc au lieu d'attendre que le moniteur y entre. De plus, en accédant par exemple à un tableau sans bloc synchronisé, un autre thread peut ne pas voir le changement apporté à celui-ci... en fait un autre thread ne sera pas voir des changements quand il dispose déjà d'une copie du tableau dans le cache de niveau x (c'est-à-dire les caches de 1er, 2e et 3e niveau) du cœur du processeur traitant le fil.

Mais les blocs synchronisés ne sont qu'un côté de la médaille : Si vous accédez à un objet dans un contexte synchronisé à partir d'un contexte non synchronisé, l'objet ne sera toujours pas synchronisé, même au sein d'un bloc synchronisé, car celui-ci détient une copie de l'objet dans son cache. J'ai écrit sur ce problème ici : https://stackoverflow.com/a/21462631 y Lorsqu'un verrou détient un objet non final, la référence de l'objet peut-elle encore ê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 trucs de bas niveau, comme le fonctionnement des CPU ou la façon dont la hiérarchie de la mémoire affecte le fonctionnement des applications : http://en.wikipedia.org/wiki/Memory_hierarchy

La raison pour laquelle les cours de programmation ne commencent pas par la hiérarchie des mémoires et l'architecture des processeurs reste une énigme. "Hello world" n'y changera rien ;)

1 votes

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

0 votes

Hmm je ne suis pas sûr de suivre. Si la mise en cache était la seule raison de mettre wait et notify dans synchronized, pourquoi la synchronisation n'est-elle pas mise dans l'implémentation de wait / notify ?

0 votes

Bonne question, puisque wait / notify pourraient 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 coup d'oeil au lien ci-dessus, ou peut-être que ceci vous aidera aussi : docs.oracle.com/javase/specs/jls/se7/html/jls-17.html

0voto

Rollerball Points 2372

Directement de ce Tutoriel java oracle :

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

0 votes

De la question de l'auteur, ne semble pas que l'auteur de la question a une compréhension claire de ce que j'ai cité de la tutorial.And de plus, ma réponse, explique "Pourquoi".

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X