web-dev-qa-db-fra.com

Pourquoi faut-il attendre () toujours dans le bloc synchronisé

Nous savons tous que pour appeler Object.wait() , cet appel doit être placé dans un bloc synchronisé, sinon une IllegalMonitorStateException est émise. Mais quelle est la raison de cette restriction? Je sais que wait() libère le moniteur, mais pourquoi avons-nous besoin d'acquérir explicitement le moniteur en rendant un bloc particulier synchronisé? puis relâchez le moniteur en appelant wait()?

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

247
diy

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

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

if(!condition){
    wait();
}

Mais la condition est définie par un thread séparé. Par conséquent, pour que cela fonctionne correctement, vous avez besoin de la synchronisation.

Un couple de plus de choses qui ne vont pas avec cela, simplement parce que votre fil d'attente a cessé d'attendre ne signifie pas que la condition que vous recherchez est vraie:

  • Vous pouvez obtenir des réveils parasites (ce qui signifie qu’un fil peut sortir d’une attente 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 réacquiert le moniteur).

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

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

Mieux encore, ne jouez pas avec les primitives de synchronisation et travaillez avec les abstractions proposées dans les packages Java.util.concurrent.

223

Quel est le dommage potentiel s'il était possible d'appeler wait() en dehors d'un bloc synchronisé, en conservant sa sémantique - suspendre le fil de l'appelant?

Illustrons les problèmes que nous rencontrerions si wait() pouvait être appelé 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 le long des lignes 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 buffer.isEmpty().

  2. Avant que le thread consommateur appelle wait(), un thread producteur arrive et appelle une give() complète, c'est-à-dire buffer.add(data); notify();.

  3. Le thread consommateur appellera maintenant wait() (et manquera la notify() qui vient d'être appelée).

  4. Si ce n'est pas le cas, le thread producteur ne produira plus de give() car le thread consommateur ne se réveille jamais et nous nous retrouvons dans une impasse.

Une fois que vous avez compris le problème, la solution est évidente: utilisez synchronized pour vous assurer que notify ne soit jamais appelé entre isEmpty et wait.

Sans entrer dans les détails: ce problème de synchronisation est universel. Comme Michael Borgwardt le fait remarquer, attendre/notifier concerne avant tout la communication entre les threads. Vous aurez donc toujours une situation de concurrence critique semblable à celle décrite ci-dessus. C'est la raison pour laquelle la règle "attendre uniquement à l'intérieur synchronisé" est appliquée.


Un paragraphe de la lien posté 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 AVANT de s'endormir, mais sa correction dépend de l'exactitude du prédicat QUAND il s'endormit. 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 s'accorder est dans l'exemple ci-dessus buffer.isEmpty(). Et l'accord est résolu en veillant à ce que l'attente et la notification soient effectuées dans des blocs synchronized.


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

272
aioobe

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

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 méthode synchronized. Cependant, comme @ shrini1000 l'a fait remarquer, le simple fait d'appeler wait() dans un bloc synchronisé n'empêchera pas cette incohérence.

Voici une belle lecture ..

11
ashoka.devanampriya

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

  1. Si le 1er thread entre dans makeChangeOnX() et vérifie la condition while, et qu'il soit true (x.metCondition() renvoie false, signifie x.condition est false ) afin qu’il entre à l’intérieur. Ensuite, juste avant la méthode wait(), un autre thread passe à setConditionToTrue() et définit le x.condition sur true et notifyAll().
  2. Alors seulement après cela, le 1er thread entrera dans sa méthode wait() (non affectée par la notifyAll() qui s'est produite quelques instants auparavant). Dans ce cas, le premier thread restera dans l'attente d'un autre thread pour exécuter setConditionToTrue(), mais cela risque de ne pas se reproduire.

Mais si vous mettez synchronized avant les méthodes qui modifient 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;
    }
}
4
Shay.G

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

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

Ensuite, notifier les ensembles de threads à la variable wasNotified à true et notifier.

Chaque thread ayant son cache local, toutes les modifications y sont écrites puis transférées 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 ni 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é par notification du thread.

Pour résoudre ces types de problèmes, ces méthodes sont toujours appelées dans un bloc synchronisé, ce qui garantit que lorsque le bloc synchronisé commence, 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 ça clarifie.

2
Aavesh Yadav

directement depuis this Java tutoriel Oracle:

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

0
Rollerball

Lorsque vous appelez notify () depuis un objet t, Java notifie une méthode t.wait () particulière. Mais comment Java recherche-t-il une méthode d'attente particulière?.

Java ne regarde que le bloc de code synchronisé qui était verrouillé par l'objet t. Java ne peut pas rechercher l'intégralité du code pour notifier un t.wait () particulier.

0
lakshman reddy

Cela concerne essentiellement l’architecture matérielle (c-à-d MÉM et caches ).

Si vous n'utilisez pas synchronized avec wait() ou notify(), un autre thread pourrait entrer le même bloc au lieu d'attendre le moniteur. pour y entrer. De plus, lorsque, par exemple, En accédant à un tableau sans bloc synchronisé, un autre thread ne verra peut-être pas son changement ... en fait, un autre thread ne verra pas les modifications apportées quand il possède déjà une copie du tableau dans le cache de niveau x (caches de premier/deuxième/troisième niveaux) du noyau de traitement du processeur.

Mais les blocs synchronisés ne représentent 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 conserve sa propre copie du fichier. objet dans son cache. J'ai écrit sur 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. En effet, les développeurs n'apprennent généralement pas les éléments de bas niveau, tels que le fonctionnement du processeur ou l'impact de la hiérarchie de la mémoire sur l'exécution des applications: http://en.wikipedia.org/wiki/Memory_hierarchy

Reste à comprendre pourquoi les classes de programmation ne commencent pas par la hiérarchie de la mémoire et l'architecture du processeur. "Bonjour le monde" ne va pas aider ici. ;)

0
Marcus

selon la documentation

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

La méthode wait() signifie simplement qu'elle libère le verrou sur l'objet. Ainsi, l'objet ne sera verrouillé que dans le bloc/la méthode synchronisé. Si thread est en dehors du bloc de synchronisation, cela signifie qu'il n'est pas verrouillé. Si ce n'est pas le cas, que voulez-vous libérer sur l'objet?

0
Arun Raaj