IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Penser en Java 2nde édition - Sommaire |  Préface |  Avant-propos | Chapitre : 1  2  3  4  5  6  7  8  9  10  11  12  13  14  15 |  Annexe : A B C D  | Tables des matières - Thinking in Java

  Chapitre 14 - Les Threads multiples

pages : 1 2 3 4 5 6 7 8 9 

La méthode paintComponent() n'est pas non plus synchronized. Décider de synchroniser les méthodes surchargées n'est pas aussi clair que quand vous ajoutez vos propres méthodes. Dans cet exemple, il semblerait que paint() fonctionne correctement qu'il soit synchronized ou non. Mais les points que vous devez considérés sont:

  1. Cette méthode modifie-t-elle l'état des variables « critiques » de l'objet. Pour découvrir si les variables sont « critiques » vous devez déterminer si elles seront lues ou modifiées par d'autres threads dans le programme. (Dans ce cas, la lecture ou la modification sont pratiquement toujours réalisées par des méthodes synchronized, ainsi vous pouvez examiner juste celle-là.) Dans le cas de paint(), aucune modification n'apparaît.
  2. Cette méthode dépend-elle de l'état de ces variables « critiques »? Si une méthode synchronized modifie une variable que votre méthode utilise, alors vous devriez vouloir mettre aussi votre méthode synchronized. En vous basant la dessus, vous devriez observer que cSize est changé par des méthodes synchronized et en conséquence paint() devrait être synchronized. Ici, pourtant, vous pouvez vous demander « Quelle serait la pire chose qui arriverai si cSize changeait durant paint()? » Quand vous voyez que ce n'est rien de trop grave, et un effet transitoire en plus, vous pouvez décider de laisser paint() un-synchronized pour éviter le temps supplémentaire de l'appel de méthode synchronized.
  3. Un troisième indice est de noter si la version de la classe de base de paint() est synchronized, ce qui n'est pas le cas. Ce n'est pas un argument hermétique, juste un indice. Dans ce cas, par exemple, un champ qui est changé via des méthodes synchronizes (il s'agit de cSize) a été mélangé dans la formule de paint() et devrait avoir changé la situation. Notez, cependant, que synchronized n'est pas hérité —  c'est à dire que si une méthode est synchronized dans une classe de base, alors il n'est pas automatiquement synchronized dans la version redéfinie de la classe dérivée .

Le code de test dans TestBangBean2 a été modifié depuis celui du chapitre précédent pour démontrer la possibilité de multicast de BangBean2 en ajoutant des listeners supplémentaires.

Blocage [Blocking]

Un thread peut être dans un des quatre états suivants:

  1. Nouveau (New): L'objet thread a été créé mais il n'a pas encore été démarré donc il ne peut pas tourné.
  2. Runnable: Cela signifie qu'un thread peut tourner quand le mécanisme de découpage du temps a des cycles CPU disponibles pour le thread. Ainsi, le thread pourra ou non tourner, mais il n'y a rien pour l'empêcher de tourner si le scheduler peut l'organiser; il n'est ni mort ni bloqué.
  3. Mort (Dead): La façon normale pour un thread de mourir est de terminer sa méthode run(). Vous pouvez également appelé stop(), mais cela déclenche une exception qui est une sous-classe de Error (ce qui signifie que vous n'êtes pas obligé de placer l'appel dans un bloc try). Souvenez vous que déclencher une exception devrait être un événement spécial et non une partie de l'exécution normale du programme; ainsi l'utilisation de stop() est déprécier dans Java 2. Il y a aussi une méthode destroy() (qui n'a jamais été implémentée) qui vous ne devriez jamais utiliser si vous pouvez l'éviter puisqu'elle est radicale et ne libère pas les verrous de l'objet.
  4. Bloqué (Blocked): Le thread pourrait tourner mais quelque chose l'en empêche. Tant qu'un thread est dans un état bloqué le scheduler le sautera et ne lui donnera pas de temps CPU. Jusqu'à ce que le thread repasse dans un état runnable il ne réalisera aucune opérations.

Passer à l'état bloqué

L'état bloqué est le plus intéressant, et nécessite un examen. Un thread peut devenir bloqué pour cinq raisons:

  1. Vous avez mis le thread à dormir en appelant sleep(milliseconds), auquel cas il ne tournera pas pendant le temps spécifé.
  2. Vous avez suspendu l'exécution du thread avec suspend(). Il ne redeviendra pas runnable avant que le thread ne reçoive le message resume() (ces possibilités sont dépréciés dans Java2, et seront examinés plus tard).
  3. Vous avez suspendu l'exécution du thread avec wait(). Il ne redeviendra pas runnable tant qu'il n'aura pas reçu l'un des messages notify() ou notifyAll(). (Oui, c'est comme le point 2, mais il y a une distinction qui sera vu plus tard.)
  4. Le thread attend la fin d'une I/O.
  5. Le thread essaye d'appelé une méthode synchronized sur un autre objet, et le verrou de cet objet n'est pas libre.

Vous pouvez aussi appelé yield() (une méthode de la classe Thread) pour volontairement donner le CPU afin que d'autres threads puisse tournées. Toutefois, il se passe la même chose si le scheduler décide que votre thread a eu assez de temps et saute à un autre thread. En fait, rien n'empêche le scheduler de partir de votre thread et de donner du temps à un autre thread. Quand un thread est bloqué, c'est qu'il y a une raison pour qu'il ne puisse continuer à tourner.

L'exemple suivant montre les cinq façons de devenir bloqué. Il existe en intégralité dans un seul fichier appelé Blocking.java, mais il sera examiné ici par morceau. (Vous remarquerez les tags « Continued » et « Continuing » qui permette à l'outil d'extraction de recoller les morceaux.)

Étant donné que cet exemple montre des méthodes dépréciées, vous obtiendrez des messages de depreciation lors de la compilation.

D'abord le programme de base:


//: c14:Blocking.java
// Démontre les différentes façon de
// bloqué un thread.
// <applet code=Blocking width=350 height=550>
// </applet>
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import com.bruceeckel.swing.*;

//////////// Le framework de base ///////////
class Blockable extends Thread {
  private Peeker peeker;
  protected JTextField state = new JTextField(30);
  protected int i;
  public Blockable(Container c) {
    c.add(state);
    peeker = new Peeker(this, c);
  }
  public synchronized int read() { return i; }
  protected synchronized void update() {
    state.setText(getClass().getName()
      + " state: i = " + i);
  }
  public void stopPeeker() {
    // peeker.stop(); Déprécié en Java 1.2
    peeker.terminate(); // L'approche préférée
  }
}

class Peeker extends Thread {
  private Blockable b;
  private int session;
  private JTextField status = new JTextField(30);
  private boolean stop = false;
  public Peeker(Blockable b, Container c) {
    c.add(status);
    this.b = b;
    start();
  }
  public void terminate() { stop = color="#0000ff">true; }
  public void run() {
    while (!stop) {
      status.setText(b.getClass().getName()
        + " Peeker " + (++session)
        + "; value = " + b.read());
       try {
        sleep(100);
      } catch(InterruptedException e) {
        System.err.println("Interrupted");
      }
    }
  }
} ///:Continued

La classe Blockable est destinée à être la classe de base pour toutes les classes de cet exemple qui démontre le blocking. Un objet Blockable contient un JTextField appelé state qui est utilisé pour afficher l'information sur l'objet. La méthode qui affiche cette information est update(). Vous pouvez voir qu'elle utilise getClass().getName() pour produire le nom de la classe plutôt que de l'afficher directement; c'est parce que update() ne peut pas connaître le nom exact de la classe pour laquelle elle est appelée, puisque ce sera une classe dérivée de Blockable.

L'indicateur de changement dans Blockable est un int i, qui sera incrémenter par la méthode run() de la classe dérivé.

Il y a un thread de la classe Peeker qui est démarré pour chaque objet Blockable, et le travail de Peeker est de regarder son objet Blockable associé pour voir les changements de i en appelant read() et en les reportant dans son status JTextField. C'est important: Notez que read() et update() sont toutes les deux synchronized, ce qui signifie qu'elles nécessite que le verrou de l'objet soit libre.

Dormant (Sleeping)

Le premier test de ce programme est avec sleep():


///:Continuing
///////////// Bloqué via sleep() ///////////
class Sleeper1 extends Blockable {
  public Sleeper1(Container c) { super(c); }
  public synchronized void run() {
    while(true) {
      i++;
      update();
       try {
        sleep(1000);
      } catch(InterruptedException e) {
        System.err.println("Interrupted");
      }
    }
  }
}
  
class Sleeper2 extends Blockable {
  public Sleeper2(Container c) { super(c); }
  public void run() {
    while(true) {
      change();
       try {
        sleep(1000);
      } catch(InterruptedException e) {
        System.err.println("Interrupted");
      }
    }
  }
  public synchronized void change() {
      i++;
      update();
  }
} ///:Continued

Dans Sleeper1 la méthode run() est entièrement synchronized. Vous verrez que le Peeker associé avec cet objet tournera tranquillement jusqu'à vous démarriez le thread, ensuite le Peeker sera gelé. C'est une forme de blocage: puisque Sleeper1.run() est synchronized, et une fois que le thread démarre dans run(), la méthode ne redonne jamais le verrou de l'objet et le Peeker est bloqué.

Sleeper2 fournit une solution en rendant run() un-synchronized. Seule la méthode change() est synchronized, ce qui signifie que tant que run() est dans sleep(), le Peeker peut accéder à la méthode synchronized dont il a besoin, nommée read(). Ici vous verrez que Peeker continue à tourner quand vous démarrez le thread Sleeper2.

Suspension et reprise

La partie suivante de l'exemple introduit le concept de suspension. La classe Thread a une méthode suspend() pour arrêter temporairement le thread et resume() qui le redémarre au point ou il était arrêté. resume() doit être appelé par un thread extérieur à celui suspendu, et dans ce cas il y a une classe séparée appelé Resumer qui fait juste ça. Chacune des classes démontrant suspend/resume a un resumer associé:


///:Continuing
/////////// Bloqué via suspend() ///////////
class SuspendResume extends Blockable {
  public SuspendResume(Container c) {
    super(c);    
    new Resumer(this);
  }
}

class SuspendResume1 extends SuspendResume {
  public SuspendResume1(Container c) { super(c);}
  public synchronized void run() {
    while(true) {
      i++;
      update();
      suspend(); // Déprecié en Java 1.2
    }
  }
}

class SuspendResume2 extends SuspendResume {
  public SuspendResume2(Container c) { super(c);}
  public void run() {
    while(true) {
      change();
      suspend(); // Déprecié en Java 1.2
    }
  }
  public synchronized void change() {
      i++;
      update();
  }
}

class Resumer extends Thread {
  private SuspendResume sr;
  public Resumer(SuspendResume sr) {
    this.sr = sr;
    start();
  }
  public void run() {
    while(true) {
       try {
        sleep(1000);
      } catch(InterruptedException e) {
        System.err.println("Interrupted");
      }
      sr.resume(); // Déprecié en Java 1.2
    }
  }
} ///:Continued

SuspendResume1 a aussi une méthode synchronized run(). Une nouvelle fois, lorsque vous démarrerez ce thread vous verrez que son Peeker associé se bloque en attendant que le verrou devienne disponible, ce qui n'arrive jamais. Ce problème est corrigé comme précédemment dans SuspendResume2, qui ne place pas la méthode run() entièrement synchronize mais utilise une méthode synchronized change() séparée.

Vous devez être au courant du fait que Java 2 déprécie l'utilisation de suspend() et resume(), parce que suspend() prend le verrou et est sujet aux deadlock. En fait, vous pouvez facilement avoir un certain nombre d'objets verrouillés s'attendant les uns les autres, et cela entraînera le gel du programme. Alors que vous pouvez voir leurs utilisations dans des programmes plus vieux, vous ne devriez pas utiliser suspend() et resume(). La solution adéquate est décrite plus tard dans ce chapitre.

Attendre et notifier

Dans les deux premiers exemples, il est important de comprendre que ni sleep() ni suspend() ne libère le verrou lorsqu'ils sont appelés. Vous devez faire attention à cela en travaillant avec les verrous. D'un autre coté, la méthode wait() libère le verrou quand elle est appelée, ce qui signifie que les autres méthodes synchronized de l'objet thread peuvent être appelée pendant un wait(). Dans les deux classes suivantes, vous verrez que la méthode run() est totalement synchronized dans les deux cas, toutefois, le Peeker a encore un accès complet aux méthodes synchronized pendant un wait(). C'est parce que wait() libère le verrou sur l'objet quand il suspend la méthode dans laquelle il a été appelé.

Vous verrez également qu'il y a deux formes de wait(). La première prend un argument en milli-secondes qui a la même signification que dans sleep(): pause pour cette période de temps. La différence est que dans wait(), le verrou de l'objet est libéré et vous pouvez sortir du wait() à cause d'un notify() aussi bien que parce que le temps est écoulé.

La seconde forme ne prend pas d'arguments, et signifie que le wait() continuera jusqu'à ce qu'un notify() arrive et ne sera pas automatiquement terminé après un temps donné.

Le seul point commun entre wait() et notify() est que ce sont toutes les deux des méthodes de la classe de base Object et non une partie de Thread comme le sont sleep(), suspend(), et resume(). Alors que cela parait un peu étrange au début — avoir quelque chose exclusivement pour le threading comme partie de la classe de base universelle — c'est essentiel puisqu'elles manipulent le verrou qui fait aussi partie de chaque objet. Le résultat est que vous pouvez mettre un wait() dans n'importe quelle méthode synchronized, du fait qu'il y a du threading intervenant dans cette classe particulière. En fait, la seule place où vous pouvez appeler wait() ou notify() est dans une méthode ou un bloc synchronized. Si vous appelez wait() ou notify() dans une méthode qui n'est pas synchronized, le programme se compilera, mais quand vous l'exécuterez vous obtiendrez une IllegalMonitorStateException avec le message peu intuitif « current thread not owner. » Notez que sleep(), suspend(), et resume() peuvent toutes être appelées dans des méthodes non-synchronized puisqu'elle ne manipulent pas le verrou.

Vous pouvez appelez wait() ou notify() seulement pour votre propre verrou. De nouveau, vous pouvez compiler un code qui essaye d'utiliser le mauvais verrou, mais il se produira le même message IllegalMonitorStateException que précédemment. Vous ne pouvez pas tricher avec le verrou de quelqu'un d'autre, mais vous pouvez demander à un autre objet de réaliser une opération qui manipule son verrou. Une approche possible est de créer une méthode synchronized qui appelle notify() pour son propre objet. Toutefois, dans Notifier vous verrez que notify() est appelé dans un bloc synchronized:


synchronized(wn2) {
  wn2.notify();
}

wn2 est l'objet de type WaitNotify2. Cette méthode, qui ne fait pas partie de WaitNotify2, acquiert le verrou sur l'objet wn2, à ce point il lui est possible d'appeler notify() pour wn2 et vous n'obtiendrez pas d'IllegalMonitorStateException.

Ce livre a été écrit par Bruce Eckel ( télécharger la version anglaise : Thinking in java )
Ce chapitre a été traduit par Cédric Babault ( groupe de traduction )
télécharger la version francaise (PDF) | Commandez le livre en version anglaise (amazon) | télécharger la version anglaise
pages : 1 2 3 4 5 6 7 8 9 
Penser en Java 2nde édition - Sommaire |  Préface |  Avant-propos | Chapitre : 1  2  3  4  5  6  7  8  9  10  11  12  13  14  15 |  Annexe : A B C D  | Tables des matières - Thinking in Java