L'objectif de la communication du thread est de permettre aux threads de se envoyer des signaux. D'un autre côté, la communication du thread permet aux threads d'attendre les signaux des autres threads.
Communication via des objets partagés
Un moyen facile d'envoyer des signaux entre les threads consiste à définir la valeur du signal dans les variables de l'objet partagé. Le thread A définit la variable booléenne de Hasdatatoprocess sur true dans un bloc de synchronisation, et le thread B lit également la variable membre de Hasdatatoprocess dans le bloc de synchronisation. Cet exemple simple utilise un objet tenant un signal et fournit les méthodes d'ensemble et de vérification:
classe publique MySignal {Boolean HasdatatoProcess protégé = false; Public synchronisé booléen Hasdatatoprocess () {return this.hasdatatoprocess; } public synchronisé void sethasdatatoprocess (boolean hasdata) {this.hasdatatoprocess = Hasdata; }}Les threads A et B doivent obtenir une référence à une instance partagée MySignal pour la communication. Si les références qu'ils détiennent pointent des instances MySingal différentes, les uns les autres ne pourront pas détecter les signaux de l'autre. Les données à traiter peuvent être stockées dans une zone de cache partagée, qui est stockée séparément de l'instance MySignal.
Attendre
Le thread B qui se prépare à traiter les données attend que les données soient disponibles. En d'autres termes, il attend qu'un signal du thread A, ce qui fait que Hasdatatoprocess () revient vrai. Le thread B fonctionne dans une boucle pour attendre ce signal:
protégé MySignal SharedSignal = ...... while (! sharedsignal.hasdatatoprocess ()) {// ne rien faire ... occupé à attendre}wait (), notify () et notifyall ()
L'attente occupée n'utilise pas efficacement le CPU exécutant le fil d'attente à moins que le temps d'attente moyen ne soit très court. Sinon, il est plus sage de faire dormir le fil d'attente ou non jusqu'à ce qu'il reçoive le signal qu'il attend.
Java a un mécanisme d'attente intégré pour permettre aux threads de devenir sans courses en attendant des signaux. La classe java.lang.object définit trois méthodes, attendre (), notify () et notifyall (), pour mettre en œuvre ce mécanisme d'attente.
Une fois qu'un thread appelle la méthode wait () de tout objet, il devient un état non running jusqu'à ce qu'un autre thread appelle la méthode notify () du même objet. Afin d'appeler attendre () ou notifier (), le thread doit d'abord obtenir le verrou de cet objet. C'est-à-dire que le thread doit appeler attendre () ou notifier () dans le bloc de synchronisation. Ce qui suit est une version modifiée de MySingal - MyWaitNotify à l'aide d'attente () et de notify ():
classe publique MonitorObject {} classe publique MyWaitNotify {monitorObject MyMonitorObject = new MonitorObject (); public void dowait () {synchronisé (myMonitorObject) {try {myMonitorObject.Wait (); } catch (InterruptedException e) {...}}} public void donotify () {synchronisé (myMonitorObject) {myMonitorObject.Notify (); }}}Le fil d'attente appellera Dowait (), tandis que le fil de réveil appellera Donotify (). Lorsqu'un thread appelle la méthode Notify () d'un objet, l'un des threads en attente de l'objet sera éveillé et autorisé à exécuter (Remarque: Ce thread à éveiller est aléatoire, et il ne peut pas être spécifié quel thread se réveille). Une méthode notifyall () pour réveiller tous les threads en attente d'un objet donné.
Comme vous pouvez le voir, qu'il s'agisse du fil d'attente ou du fil de réveil, il appelle attendre () et notifier () dans le bloc de synchronisation. C'est obligatoire! Si un thread ne tient pas le verrouillage de l'objet, il ne peut pas appeler attendre (), notifier () ou notifyall (). Sinon, une exception illégalitorstateException sera lancée.
(Remarque: c'est ainsi que JVM est implémenté.
Mais comment est-ce possible? Lorsque vous attendez que le thread s'exécute dans le bloc de synchronisation, ne maintient-il pas toujours le verrou de l'objet Monitor (objet MyMonitor)? Le thread d'attente peut-il bloquer le fil de réveil entrant dans le bloc synchrone de donotify ()? La réponse est: c'est vrai. Une fois que le thread appelle la méthode wait (), il libère le verrouillage de l'objet de moniteur maintenu. Cela permettra également à d'autres threads d'appeler attendre () ou notifier ().
Une fois qu'un thread est réveillé, l'appel de méthode attendu () ne peut pas être sorti immédiatement tant que notify () est appelé.
classe publique MyWaitNotify2 {monitorObject MyMonitorObject = new MonitorObject (); booléen wassignaled = false; public void dowait () {synchronisé (myMonitorObject) {if (! wassignaled) {try {myMonitorObject.Wait (); } catch (InterruptedException e) {...}} // Clear Signal et continuez en fonctionnement. wassignaled = false; }} public void donotify () {synchronisé (myMonitorObject) {wassignaled = true; myMonitorObject.Notify (); }}}
Le thread quitte son propre bloc de synchronisation. En d'autres termes, le fil Waken doit retrouver le verrou de l'objet Monitor avant de pouvoir quitter l'appel de méthode attendu (), car l'appel de méthode d'attente s'exécute dans le bloc de synchronisation. Si plusieurs threads sont éveillés par notifyall (), alors en même temps, un seul thread peut quitter la méthode Wait (), car chaque thread doit obtenir le verrouillage de l'objet Monitor avant de quitter Wait ().
Signaux manqués
Les méthodes notify () et notifyall () ne sauvent pas la méthode qui les appelle, car lorsque ces deux méthodes sont appelées, il est possible qu'aucun thread ne soit dans l'état d'attente. Le signal de notification a été jeté. Par conséquent, si un thread appelle Notify () avant d'être averti avant d'appeler Wait (), le thread d'attente manquera ce signal. Cela peut ou non être un problème. Cependant, dans certains cas, cela peut faire attendre toujours le fil d'attente et ne plus se réveiller car le fil manque le signal de réveil.
Pour éviter de perdre des signaux, ils doivent être enregistrés dans la classe de signal. Dans l'exemple MyWaitNotify, le signal de notification doit être stocké dans une variable de membre de l'instance MyWaitNotify. Voici une version modifiée de MyWaitNotify:
classe publique MyWaitNotify2 {monitorObject MyMonitorObject = new MonitorObject (); booléen wassignaled = false; public void dowait () {synchronisé (myMonitorObject) {if (! wassignaled) {try {myMonitorObject.Wait (); } catch (InterruptedException e) {...}} // Clear Signal et continuez en fonctionnement. wassignaled = false; }} public void donotify () {synchronisé (myMonitorObject) {wassignaled = true; myMonitorObject.Notify (); }}}Notez que la méthode donotify () définit la variable Wassignalled à true avant d'appeler Notify (). Dans le même temps, notez que la méthode Dowait () vérifie la variable Wassignaled avant d'appeler Wait (). En fait, si aucun signal n'est reçu pendant la période de temps entre l'appel Dowait () précédent et cet appel Dowait (), il n'appellera qu'attendre ().
(Note de preuve: pour éviter la perte de signal, utilisez une variable pour sauver si elle a été notifiée. Avant d'informer, définissez-vous avoir été informé. Après l'attente, définissez-vous pour ne pas avoir été informé et avoir besoin d'attendre la notification.)
Faux réveil
Pour une raison quelconque, il est possible que le thread se réveille sans appeler Notify () et notifyall (). C'est ce qu'on appelle des réveils parasites. Réveillez-vous sans raison.
Si un faux réveil se produit dans la méthode Dowait () de MyWaitNotify2, le thread d'attente peut effectuer des opérations ultérieures même si elle ne reçoit pas le signal correct. Cela peut causer de graves problèmes avec votre application.
Pour éviter le faux réveil, les variables membre qui maintiennent le signal seront vérifiées dans une boucle de temps, plutôt que dans l'expression if. Une telle boucle est appelée verrouillage de spin (Remarque: Cette approche doit être prudente. Le spin d'implémentation JVM actuel consomme le processeur. Si la méthode Donotify n'est pas appelée pendant longtemps, la méthode Dowait fera tourner en continu et que le CPU consommera trop). Le fil éveillé tourne jusqu'à ce que la condition dans le verrouillage de spin (pendant la boucle) devienne fausse. La version modifiée suivante de MyWaitNotify2 montre ceci:
classe publique MyWaitNotify3 {monitorObject MyMonitorObject = new MonitorObject (); booléen wassignaled = false; public void dowait () {synchronisé (myMonitorObject) {while (! wassignaled) {try {myMonitorObject.Wait (); } catch (InterruptedException e) {...}} // Clear Signal et continuez en fonctionnement. wassignaled = false; }} public void donotify () {synchronisé (myMonitorObject) {wassignaled = true; myMonitorObject.Notify (); }}}Notez que la méthode attend () est dans la boucle while, pas dans l'expression if. Si le thread d'attente se réveille sans recevoir le signal, la variable Wassignale deviendra fausse et la boucle While sera à nouveau exécutée, incité le fil de réveil à retourner à l'état d'attente.
Plusieurs threads attendent le même signal
Si vous avez plusieurs threads en attente et que vous êtes éveillé par notifyall (), mais qu'un seul est autorisé à continuer l'exécution, l'utilisation d'une boucle de temps est également un bon moyen. Un seul thread peut obtenir le verrouillage de l'objet Monitor à chaque fois, ce qui signifie qu'un seul thread peut quitter l'appel attendu () et effacer l'indicateur Wassignaled (défini sur false). Une fois que ce thread a quitté le bloc de synchronisation Dowait (), d'autres threads quittent l'appel attendu () et vérifiez la valeur variable de la femme dans la boucle while. Cependant, ce drapeau a été éliminé par le premier fil éveillé, de sorte que le reste des fils éveillés reviendra à l'état d'attente jusqu'à la prochaine fois que le signal arrive.
N'appelez pas attendre () dans des constantes de chaîne ou des objets globaux
(Note de preuve: la constante de corde mentionnée dans ce chapitre fait référence aux variables avec des valeurs constantes)
Une version antérieure de cet article utilise des constantes de chaîne ("") comme objet de tuyau dans l'exemple MyWaitNotify. Voici l'exemple:
classe publique MyWaitNotify {String MyMonitorObject = ""; booléen wassignaled = false; public void dowait () {synchronisé (myMonitorObject) {while (! wassignaled) {try {myMonitorObject.Wait (); } catch (InterruptedException e) {...}} // Clear Signal et continuez en fonctionnement. wassignaled = false; }} public void donotify () {synchronisé (myMonitorObject) {wassignaled = true; myMonitorObject.Notify (); }}}Le problème causé par l'appel Wait () et Notify () dans le bloc de synchronisation d'une chaîne vide en tant que verrouillage (ou autre chaîne constante) est que le JVM / le compilateur convertira la chaîne constante en même objet. Cela signifie que même si vous avez 2 instances de MyWaitnotify différentes, elles se réfèrent toutes à la même instance de chaîne vide. Cela signifie également qu'il existe un risque que le fil appelant Dowait () sur la première instance MyWaitNotify soit éveillé par le fil appelant Donotify () sur la deuxième instance MyWaitNotify. Cette situation peut être tirée comme suit:
Au début, ce n'est peut-être pas un gros problème. Après tout, si Donotify () est appelé sur la deuxième instance MyWaitnotify, ce qui se passe vraiment, c'est que les fils A et B sont éveillés à tort. Le fil de réveil (A ou B) vérifiera la valeur du signal dans la boucle while, puis reviendra à l'état d'attente, car donotify () n'est pas appelé sur la première instance MyWaitNotify, et c'est l'instance qu'il attend. Cette situation équivaut à déclencher un faux réveil. Le thread A ou B se réveille sans que la valeur du signal ne soit mise à jour. Mais le code gère cette situation, donc le fil revient à l'état d'attente. N'oubliez pas que si 4 threads appellent attendre () et notify () sur la même instance de chaîne partagée, les signaux dans Dowait () et Donotify () seront enregistrés respectivement par 2 instances MyWaitnotify. Un appel donotify () sur myWaitNotify1 peut réveiller le thread de myWaitNotify2, mais la valeur du signal ne sera enregistrée que dans myWaitNotify1.
Le problème est que, puisque Donotify () appelle uniquement Notify () au lieu de notifyall (), même s'il y a 4 threads en attente sur la même instance de chaîne (chaîne vide), un seul thread est éveillé. Ainsi, si le thread A ou B est éveillé par un signal envoyé à C ou D, il vérifiera sa propre valeur de signal pour voir si un signal est reçu, puis reviendra à l'état d'attente. Ni C ni D n'ont été éveillés pour vérifier la valeur du signal qu'ils ont réellement reçue, donc le signal a été perdu. Cette situation équivaut au problème des signaux manquants mentionnés ci-dessus. C et D ont été envoyés au signal, mais aucun ne peut répondre au signal.
Si la méthode donotify () appelle notifyall () au lieu de notifier (), tous les threads d'attente seront éveillés et la valeur du signal sera vérifiée à son tour. Les threads A et B reviendront à l'état d'attente, mais un seul thread en C ou D remarque le signal et quitte l'appel Dowait (). L'autre en C ou D reviendra à l'état d'attente car le thread qui a obtenu le signal efface la valeur du signal (réglé sur False) pendant le processus de sortie de Dowait ().
Après avoir lu le paragraphe ci-dessus, vous pouvez essayer d'utiliser Notifyall () au lieu de notifier (), mais c'est une mauvaise idée basée sur les performances. Lorsqu'un seul thread peut répondre au signal, il n'y a aucune raison de réveiller tous les threads à chaque fois.
Donc: dans le mécanisme attendu () / notify (), n'utilisez pas d'objets globaux, de constantes de chaîne, etc. L'objet unique correspondant doit être utilisé. Par exemple, chaque instance de MyWaitNotify3 a son propre objet Monitor au lieu d'appeler Wait () / Notify () sur une chaîne vide.
Ce qui précède est les informations sur le multi-threading Java et la communication de thread. Nous continuerons d'ajouter des informations pertinentes à l'avenir. Merci pour votre soutien à ce site!