Maison  >  Article  >  Java  >  Multithreading Java - Exemples de communication de threads expliqués

Multithreading Java - Exemples de communication de threads expliqués

高洛峰
高洛峰original
2017-01-05 15:08:051087parcourir

Le but de la communication par thread est de permettre aux threads de s'envoyer des signaux les uns aux autres. La communication par thread, en revanche, permet aux threads d'attendre les signaux des autres threads.

Communiquer via un objet partagé

Occupé en attente

wait(), notify() et notifyAll()

Signal perdu

Faux wakeup

Plusieurs threads attendent le même signal

Ne pas appeler wait() sur des chaînes constantes ou des objets globaux

Communiquer via des objets partagés

Un moyen simple d'envoyer des signaux entre les threads consiste à définir la valeur du signal dans une variable d'un objet partagé. Le thread A définit la variable membre booléenne hasDataToProcess sur true dans un bloc synchronisé, et le thread B lit également la variable membre hasDataToProcess dans le bloc synchronisé. Cet exemple simple utilise un objet qui contient un signal et fournit des méthodes set et check :

public class MySignal{
 
 protected boolean hasDataToProcess = false;
 
 public synchronized boolean hasDataToProcess(){
 return this.hasDataToProcess;
 }
 
 public synchronized void setHasDataToProcess(boolean hasData){
 this.hasDataToProcess = hasData;
 }
 
}

Les threads A et B doivent obtenir une référence à une instance partagée de MySignal afin de communiquer. S'ils détiennent des références à différentes instances de MySingal, alors chacun ne pourra pas détecter le signal de l'autre. Les données à traiter peuvent être stockées dans un cache partagé, qui est stocké séparément de l'instance MySignal.

Attente occupée

Le thread B qui se prépare à traiter les données attend que les données soient disponibles. En d’autres termes, il attend un signal du thread A qui fait que hasDataToProcess() renvoie true. Le thread B s'exécute en boucle, en attendant ce signal :

protected MySignal sharedSignal = ...
 
...
 
while(!sharedSignal.hasDataToProcess()){
 //do nothing... busy waiting
}

wait(), notify() et notifyAll()

L'attente occupée ne prend pas effet sur le processeur exécutant l'attente l'utilisation des threads, sauf si le temps d'attente moyen est très court. Sinon, il est plus sage de laisser le thread en attente dormir ou rester dans un état non exécuté jusqu'à ce qu'il reçoive le signal qu'il attend.

Java dispose d'un mécanisme d'attente intégré qui permet aux threads de ne plus s'exécuter en attendant un signal. La classe java.lang.Object définit trois méthodes, wait(), notify() et notifyAll(), pour implémenter ce mécanisme d'attente.

Une fois qu'un thread appelle la méthode wait() d'un objet, il ne s'exécutera plus jusqu'à ce qu'un autre thread appelle la méthode notify() du même objet. Afin d'appeler wait() ou notify(), le thread doit d'abord acquérir le verrou sur cet objet. En d’autres termes, le thread doit appeler wait() ou notify() dans le bloc synchronisé. Ce qui suit est une version modifiée de MySingal - MyWaitNotify utilisant wait() et notify() :

public class MonitorObject{
}
 
public class MyWaitNotify{
 
 MonitorObject myMonitorObject = new MonitorObject();
 
 public void doWait(){
 synchronized(myMonitorObject){
  try{
  myMonitorObject.wait();
  } catch(InterruptedException e){...}
 }
 }
 
 public void doNotify(){
 synchronized(myMonitorObject){
  myMonitorObject.notify();
 }
 }
}

Le thread en attente appellera doWait() et le thread de réveil appellera doNotify(). Lorsqu'un thread appelle la méthode notify() d'un objet, un thread parmi tous les threads en attente de l'objet sera réveillé et autorisé à s'exécuter (remarque : le thread qui sera réveillé est aléatoire et vous ne pouvez pas spécifier quel thread réveiller) . Il fournit également une méthode notifyAll() pour réveiller tous les threads en attente d'un objet donné.

Comme vous pouvez le voir, le thread en attente et le thread de réveil appellent wait() et notify() dans le bloc synchronisé. C'est obligatoire ! Un thread ne peut pas appeler wait(), notify() ou notifyAll() s'il ne détient pas le verrou d'objet. Sinon, une exception IllegalMonitorStateException sera levée.

(Remarque : voici comment la JVM est implémentée. Lorsque vous appelez wait, elle vérifie d'abord si le thread actuel est le propriétaire du verrou. Sinon, elle lance un IllegalMonitorStateExcept.)

Mais comment est-ce possible ? Lorsque le thread en attente est exécuté dans le bloc synchronisé, ne détient-il pas toujours le verrou de l'objet moniteur (objet myMonitor) ? Le thread en attente ne peut-il pas empêcher le thread de réveil d'entrer dans le bloc synchronisé de doNotify() ? La réponse est : effectivement non. Une fois qu'un thread appelle la méthode wait(), il libère le verrou qu'il détient sur l'objet moniteur. Cela permettra à d'autres threads d'appeler également wait() ou notify().

Une fois qu'un thread est réveillé, il ne peut pas quitter immédiatement l'appel de la méthode wait() jusqu'à ce que le thread

public class MyWaitNotify2{
 
 MonitorObject myMonitorObject = new MonitorObject();
 boolean wasSignalled = false;
 
 public void doWait(){
 synchronized(myMonitorObject){
  if(!wasSignalled){
  try{
   myMonitorObject.wait();
   } catch(InterruptedException e){...}
  }
  //clear signal and continue running.
  wasSignalled = false;
 }
 }
 
 public void doNotify(){
 synchronized(myMonitorObject){
  wasSignalled = true;
  myMonitorObject.notify();
 }
 }
}
 
<br>

qui appelle notify() quitte son propre bloc de synchronisation. En d'autres termes : le thread réveillé doit réacquérir le verrou de l'objet moniteur avant de pouvoir quitter l'appel de la méthode wait(), car l'appel de la méthode wait s'exécute dans un bloc synchronisé. Si plusieurs threads sont réveillés par notifyAll(), alors un seul thread peut quitter la méthode wait() en même temps, car chaque thread doit obtenir le verrou de l'objet moniteur avant de quitter wait().

Signaux manqués

Les méthodes notify() et notifyAll() ne sauvegardent pas la méthode qui les a appelées, car lorsque ces deux méthodes sont appelées, il est possible qu'aucun thread ne soit en état d'attente . Le signal de notification est rejeté après son expiration. Par conséquent, si un thread appelle notify() avant que le thread notifié n'appelle wait(), le thread en attente manquera le signal. Cela peut ou non être un problème. Cependant, dans certains cas, cela peut amener le thread en attente à attendre indéfiniment et à ne jamais se réveiller car il a manqué le signal de réveil.

Afin d'é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 membre de l'instance MyWaitNotify. Voici une version modifiée de MyWaitNotify :

public class MyWaitNotify2{
 
 MonitorObject myMonitorObject = new MonitorObject();
 boolean wasSignalled = false;
 
 public void doWait(){
 synchronized(myMonitorObject){
  if(!wasSignalled){
  try{
   myMonitorObject.wait();
   } catch(InterruptedException e){...}
  }
  //clear signal and continue running.
  wasSignalled = false;
 }
 }
 
 public void doNotify(){
 synchronized(myMonitorObject){
  wasSignalled = true;
  myMonitorObject.notify();
 }
 }
}

留意 doNotify()方法在调用 notify()前把 wasSignalled 变量设为 true。同时,留意 doWait()方法在调用 wait()前会检查 wasSignalled 变量。事实上,如果没有信号在前一次 doWait()调用和这次 doWait()调用之间的时间段里被接收到,它将只调用 wait()。

(校注:为了避免信号丢失, 用一个变量来保存是否被通知过。在 notify 前,设置自己已经被通知过。在 wait 后,设置自己没有被通知过,需要等待通知。)

假唤醒

由于莫名其妙的原因,线程有可能在没有调用过 notify()和 notifyAll()的情况下醒来。这就是所谓的假唤醒(spurious wakeups)。无端端地醒过来了。

如果在 MyWaitNotify2 的 doWait()方法里发生了假唤醒,等待线程即使没有收到正确的信号,也能够执行后续的操作。这可能导致你的应用程序出现严重问题。

为了防止假唤醒,保存信号的成员变量将在一个 while 循环里接受检查,而不是在 if 表达式里。这样的一个 while 循环叫做自旋锁(校注:这种做法要慎重,目前的 JVM 实现自旋会消耗 CPU,如果长时间不调用 doNotify 方法,doWait 方法会一直自旋,CPU 会消耗太大)。被唤醒的线程会自旋直到自旋锁(while 循环)里的条件变为 false。以下 MyWaitNotify2 的修改版本展示了这点:

public class MyWaitNotify3{
 
 MonitorObject myMonitorObject = new MonitorObject();
 boolean wasSignalled = false;
 
 public void doWait(){
 synchronized(myMonitorObject){
  while(!wasSignalled){
  try{
   myMonitorObject.wait();
   } catch(InterruptedException e){...}
  }
  //clear signal and continue running.
  wasSignalled = false;
 }
 }
 
 public void doNotify(){
 synchronized(myMonitorObject){
  wasSignalled = true;
  myMonitorObject.notify();
 }
 }
}

留意 wait()方法是在 while 循环里,而不在 if 表达式里。如果等待线程没有收到信号就唤醒,wasSignalled 变量将变为 false,while 循环会再执行一次,促使醒来的线程回到等待状态。

多个线程等待相同信号

如果你有多个线程在等待,被 notifyAll()唤醒,但只有一个被允许继续执行,使用 while 循环也是个好方法。每次只有一个线程可以获得监视器对象锁,意味着只有一个线程可以退出 wait()调用并清除 wasSignalled 标志(设为 false)。一旦这个线程退出 doWait()的同步块,其他线程退出 wait()调用,并在 while 循环里检查 wasSignalled 变量值。但是,这个标志已经被第一个唤醒的线程清除了,所以其余醒来的线程将回到等待状态,直到下次信号到来。

不要在字符串常量或全局对象中调用 wait()

(校注:本章说的字符串常量指的是值为常量的变量)

本文早期的一个版本在 MyWaitNotify 例子里使用字符串常量(””)作为管程对象。以下是那个例子:

public class MyWaitNotify{
 
 String myMonitorObject = "";
 boolean wasSignalled = false;
 
 public void doWait(){
 synchronized(myMonitorObject){
  while(!wasSignalled){
  try{
   myMonitorObject.wait();
   } catch(InterruptedException e){...}
  }
  //clear signal and continue running.
  wasSignalled = false;
 }
 }
 
 public void doNotify(){
 synchronized(myMonitorObject){
  wasSignalled = true;
  myMonitorObject.notify();
 }
 }
}

在空字符串作为锁的同步块(或者其他常量字符串)里调用 wait()和 notify()产生的问题是,JVM/编译器内部会把常量字符串转换成同一个对象。这意味着,即使你有 2 个不同的 MyWaitNotify 实例,它们都引用了相同的空字符串实例。同时也意味着存在这样的风险:在第一个 MyWaitNotify 实例上调用 doWait()的线程会被在第二个 MyWaitNotify 实例上调用 doNotify()的线程唤醒。这种情况可以画成以下这张图:

java 多线程-线程通信实例讲解

起初这可能不像个大问题。毕竟,如果 doNotify()在第二个 MyWaitNotify 实例上被调用,真正发生的事不外乎线程 A 和 B 被错误的唤醒了 。这个被唤醒的线程(A 或者 B)将在 while 循环里检查信号值,然后回到等待状态,因为 doNotify()并没有在第一个 MyWaitNotify 实例上调用,而这个正是它要等待的实例。这种情况相当于引发了一次假唤醒。线程 A 或者 B 在信号值没有更新的情况下唤醒。但是代码处理了这种情况,所以线程回到了等待状态。记住,即使 4 个线程在相同的共享字符串实例上调用 wait()和 notify(),doWait()和 doNotify()里的信号还会被 2 个 MyWaitNotify 实例分别保存。在 MyWaitNotify1 上的一次 doNotify()调用可能唤醒 MyWaitNotify2 的线程,但是信号值只会保存在 MyWaitNotify1 里。

问题在于,由于 doNotify()仅调用了 notify()而不是 notifyAll(),即使有 4 个线程在相同的字符串(空字符串)实例上等待,只能有一个线程被唤醒。所以,如果线程 A 或 B 被发给 C 或 D 的信号唤醒,它会检查自己的信号值,看看有没有信号被接收到,然后回到等待状态。而 C 和 D 都没被唤醒来检查它们实际上接收到的信号值,这样信号便丢失了。这种情况相当于前面所说的丢失信号的问题。C 和 D 被发送过信号,只是都不能对信号作出回应。

Si la méthode doNotify() appelle notifyAll() au lieu de notify(), tous les threads en attente seront réveillés et vérifieront tour à tour la valeur du signal. Les threads A et B reviendront à l'état d'attente, mais un seul thread, C ou D, remarquera le signal et quittera l'appel de la méthode doWait(). L'autre en C ou D reviendra à l'état d'attente car le thread qui a reçu le signal a effacé la valeur du signal (la définit sur false) à la sortie de doWait().

Après avoir lu le paragraphe ci-dessus, vous pourriez essayer d'utiliser notifyAll() au lieu de notify(), mais c'est une mauvaise idée en termes de performances. Il n'y a aucune raison de réveiller tous les threads à chaque fois lorsqu'un seul thread peut répondre au signal.

Donc : dans le mécanisme wait()/notify(), n'utilisez pas d'objets globaux, de constantes de chaîne, etc. Les objets uniques correspondants doivent être utilisés. Par exemple, au lieu d'appeler wait()/notify() sur une chaîne vide, chaque instance de MyWaitNotify3 possède son propre objet moniteur.

Ce qui précède est la collecte d'informations sur le multi-threading Java et la communication par thread. Nous continuerons à ajouter des informations pertinentes à l'avenir. Merci pour votre soutien à ce site !

Pour plus d'exemples de communication multi-threading Java et d'articles connexes, veuillez faire attention au site Web PHP chinois !


Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn