Muni de ce nouveau mot clé la solution est entre nos mains: nous
utiliserons simplement le mot clé synchronized pour les méthodes de
TwoCounter. L'exemple suivant est le même que le précédent, avec en plus
le nouveau mot clé:
//: c14:Sharing2.java
// Utilisant le mot clé synchronized pour éviter
// les accès multiples à une ressource particulière.
// <applet code=Sharing2 width=350 height=500>
// <param name=size value="12">
// <param name=watchers value="15">
// </applet>
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import com.bruceeckel.swing.*;
public class Sharing2 extends JApplet {
TwoCounter[] s;
private static int accessCount = 0;
private static JTextField aCount =
new JTextField("0", 7);
public static void incrementAccess() {
accessCount++;
aCount.setText(Integer.toString(accessCount));
}
private JButton
start = new JButton("Start"),
watcher = new JButton("Watch");
private boolean isApplet = true;
private int numCounters = 12;
private int numWatchers = 15;
class TwoCounter extends Thread {
private boolean started = color="#0000ff">false;
private JTextField
t1 = new JTextField(5),
t2 = new JTextField(5);
private JLabel l =
new JLabel("count1 == count2");
private int count1 = 0, count2 = 0;
public TwoCounter() {
JPanel p = new JPanel();
p.add(t1);
p.add(t2);
p.add(l);
getContentPane().add(p);
}
public void start() {
if(!started) {
started = true;
super.start();
}
}
public synchronized void run() {
while (true) {
t1.setText(Integer.toString(count1++));
t2.setText(Integer.toString(count2++));
try {
sleep(500);
} catch(InterruptedException e) {
System.err.println("Interrupted");
}
}
}
public synchronized void synchTest() {
Sharing2.incrementAccess();
if(count1 != count2)
l.setText("Unsynched");
}
}
class Watcher extends Thread {
public Watcher() { start(); }
public void run() {
while(true) {
for(int i = 0; i < s.length; i++)
s[i].synchTest();
try {
sleep(500);
} catch(InterruptedException e) {
System.err.println("Interrupted");
}
}
}
}
class StartL implements ActionListener {
public void actionPerformed(ActionEvent e) {
for(int i = 0; i < s.length; i++)
s[i].start();
}
}
class WatcherL implements ActionListener {
public void actionPerformed(ActionEvent e) {
for(int i = 0; i < numWatchers; i++)
new Watcher();
}
}
public void init() {
if(isApplet) {
String counters = getParameter("size");
if(counters != null)
numCounters = Integer.parseInt(counters);
String watchers = getParameter("watchers");
if(watchers != null)
numWatchers = Integer.parseInt(watchers);
}
s = new TwoCounter[numCounters];
Container cp = getContentPane();
cp.setLayout(new FlowLayout());
for(int i = 0; i < s.length; i++)
s[i] = new TwoCounter();
JPanel p = new JPanel();
start.addActionListener(new StartL());
p.add(start);
watcher.addActionListener(new WatcherL());
p.add(watcher);
p.add(new Label("Access Count"));
p.add(aCount);
cp.add(p);
}
public static void main(String[] args) {
Sharing2 applet = new Sharing2();
// Ce n'est pas une applet, donc place le flag et
// récupère les valeurs de paramètres depuis args:
applet.isApplet = false;
applet.numCounters =
(args.length == 0 ? 12 :
Integer.parseInt(args[0]));
applet.numWatchers =
(args.length < 2 ? 15 :
Integer.parseInt(args[1]));
Console.run(applet, 350,
applet.numCounters * 50);
}
} ///:~
Vous noterez que les deux méthodes run() et
synchTest() sont synchronized. Si vous synchronizez seulement une des
méthodes, alors l'autre est libre d'ignorer l'objet verrouillé et peut être
appelé en toute impunité. C'est un point important: chaque méthode qui
accède à une ressource partagée critique doit être synchronized
ou ça ne fonctionnera pas correctement.
Maintenant un nouveau problème apparaît. Le Watcher ne
peut jamais voir [get a peek] ce qui ce passe parce que la méthode run() est
entièrement synchronized, et comme run() tourne toujours pour chaque objet le
verrou est toujours fermé, synchTest() ne peut jamais être appelé. Vous
pouvez le voir parce que accessCount ne change jamais.
Ce que nous aurions voulu pour cet exemple est un moyen d'isoler seulement
une partie du code de run(). La section de code que vous voulez isoler de cette
manière est appelé une section critique et vous utilisez le mot clé
synchronized d'une manière différente pour créer une section critique.
Java supporte les sections critiques à l'aide d'un synchronized block; cette fois
synchronized est utilisé pour spécifier l'objet sur lequel le verrou est
utilisé pour synchronizer le code encapsulé:
synchronized(syncObject) {
// Ce code ne peut être accéder
// que par un thread à la fois
}
Avant l'entrée dans le bloc synchronisé, le verrou doit
être acquis sur syncObject. Si d'autres threads possède déjà le
verrou, l'entrée dans le bloc est impossible jusqu'à ce que le verrou soit
libéré.
L'exemple Sharing2 peut être modifié en supprimant le
mot clé synchronized de la méthode run() et de mettre à la place
un bloc synchronized autour des deux lignes critiques. Mais quel objet devrait être
utilisé comme verrou? Celui qui est déjà respecté par
synchTest(), qui est l'objet courant (this)! Ainsi la méthode run()
modifié ressemble à:
public void run() {
while (true) {
synchronized(this) {
t1.setText(Integer.toString(count1++));
t2.setText(Integer.toString(count2++));
}
try {
sleep(500);
} catch(InterruptedException e) {
System.err.println("Interrupted");
}
}
}
C'est le seul changement qui doit être fait à
Sharing2.java, et vous verrez que bien que les compteurs ne soit jamais
désynchronisés (d'après ce que Watcher est autorisé à
voir d'eux), il y a toujours un accès adéquat fourni au Watcher pendant
l'exécution de run().
Bien sûr, toutes les synchronisations dépendent de la
diligence du programmeur: chaque morceau de code qui peut accéder à une ressource
partagée doit être emballé dans un bloc synchronisé
approprié.
Efficacité de la synchronisation
Comme avoir deux méthodes écrivant dans le même morceau
de données n'apparaît jamais comme une bonne idée, il semblerait avoir
du sens que toutes les méthodes soit automatiquement synchronized et
d'éliminer le mot clé synchronized ailleurs. (Bien sûr, l'exemple avec
synchronized run() montre que ça ne fonctionnerait pas.) Mais il faut savoir
qu'acquérir un verrou n'est pas une opération légère; cela multiplie le
coût de l'appel de méthode (c'est à dire entrer et sortir de la méthode,
pas exécuter le corps de cette méthode) par au moins quatre fois, et peut être
très différent suivant votre implémentation. Donc si vous savez qu'une
méthode particulière ne posera pas de problèmes particuliers il est opportun
de ne pas utiliser le mot clé synchronized. D'un autre coté, supprimer le mot
clé synchronized parce que vous pensez que c'est un goulot d'étranglement pour
les performances en espérant qu'il n'y aura pas de collisions est une invitation au
désastre.
JavaBeans revisités
Maintenant que vous comprenez la synchronisation, vous pouvez avoir un
autre regard sur les Javabeans. Quand vous créez un Bean, vous devez assumez qu'il sera
exécuté dans un environnement multithread. Ce qui signifie que:
-
Autant que possible, toutes les méthodes public d'un Bean
devront être synchronized. Bien sûr, cela implique un désagrément
lié à l'augmentation de temps d'exécution du à synchronized. Si
c'est un problème, les méthodes qui ne poseront pas de problème de sections
critiques peuvent être laissées non-synchronized, mais gardez en mémoire
que ce n'est pas toujours aussi évident. Les méthodes qui donne l'accès aux
attributs ont tendance à être petite (comme getCircleSize() dans l'exemple
suivant) et/ou « atomique » en fait, l'appel de méthode exécute
un si petit code que l'objet ne peut pas être changé durant l'exécution. Rendre
ce type de méthodes non-synchronized ne devrait pas avoir d'effet important sur la
vitesse d'exécution de votre programme. Vous devriez de même rendre toutes les
méthodes public d'un Bean synchronized et supprimer le mot clé
synchronized seulement quand vous savez avec certitude que c'est nécessaire et que
ça fera une différence.
-
Quand vous déclenchez un multicast event à un banc de
listeners intéressé par cet événement, vous devez vous assurer que tous
les listeners seront ajoutés et supprimés durant le déplacement dans la
liste.
Le premier point est assez facile à comprendre, mais le second point
exige un petit effort. Considérez l'exemple BangBean.java presenté dans le
précédent chapitre. Il évitait la question du multithreading en ignorant le
mot clé synchronized (qui n'avait pas été encore introduit) et en
rendant l'événement unicast. Voici cet exemple modifié pour fonctionner dans
un environnement multi-tâche et utilisant le multicasting pour les
événements:
//: c14:BangBean2.java
// Vous devriez écrire vos Beans de cette façon pour qu'ils
// puissent tournés dans un environement multithread.
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import java.io.*;
import com.bruceeckel.swing.*;
public class BangBean2 extends JPanel
implements Serializable {
private int xm, ym;
private int cSize = 20; // Taille du cercle
private String text = "Bang!";
private int fontSize = 48;
private Color tColor = Color.red;
private ArrayList actionListeners =
new ArrayList();
public BangBean2() {
addMouseListener(new ML());
addMouseMotionListener(new MM());
}
public synchronized int getCircleSize() {
return cSize;
}
public synchronized void
setCircleSize(int newSize) {
cSize = newSize;
}
public synchronized String getBangText() {
return text;
}
public synchronized void
setBangText(String newText) {
text = newText;
}
public synchronized int getFontSize() {
return fontSize;
}
public synchronized void
setFontSize(int newSize) {
fontSize = newSize;
}
public synchronized Color getTextColor() {
return tColor;
}
public synchronized void
setTextColor(Color newColor) {
tColor = newColor;
}
public void paintComponent(Graphics g) {
super.paintComponent(g);
g.setColor(Color.black);
g.drawOval(xm - cSize/2, ym - cSize/2,
cSize, cSize);
}
// C'est un listener multicast, qui est
// plus typiquement utilisé que l'approche
// unicast utilisée dans BangBean.java:
public synchronized void
addActionListener(ActionListener l) {
actionListeners.add(l);
}
public synchronized void
removeActionListener(ActionListener l) {
actionListeners.remove(l);
}
// Remarquez qu'elle n'est pas synchronized:
public void notifyListeners() {
ActionEvent a =
new ActionEvent(BangBean2.this,
ActionEvent.ACTION_PERFORMED, null);
ArrayList lv = null;
// Effectue une copie profonde de la liste au cas où
// quelqu'un ajouterait un listener pendant que nous
// appelons les listeners:
synchronized(this) {
lv = (ArrayList)actionListeners.clone();
}
// Apelle toutes les méthodes listeners:
for(int i = 0; i < lv.size(); i++)
((ActionListener)lv.get(i))
.actionPerformed(a);
}
class ML extends MouseAdapter {
public void mousePressed(MouseEvent e) {
Graphics g = getGraphics();
g.setColor(tColor);
g.setFont(
new Font(
"TimesRoman", Font.BOLD, fontSize));
int width =
g.getFontMetrics().stringWidth(text);
g.drawString(text,
(getSize().width - width) /2,
getSize().height/2);
g.dispose();
notifyListeners();
}
}
class MM extends MouseMotionAdapter {
public void mouseMoved(MouseEvent e) {
xm = e.getX();
ym = e.getY();
repaint();
}
}
public static void main(String[] args) {
BangBean2 bb = new BangBean2();
bb.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e){
System.out.println("ActionEvent" + e);
}
});
bb.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e){
System.out.println("BangBean2 action");
}
});
bb.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e){
System.out.println("More action");
}
});
Console.run(bb, 300, 300);
}
} ///:~
Ajouter synchronized aux méthodes est un changement facile.
Toutefois, motez que dans addActionListener() et removeActionListener() que les
ActionListeners sont maintenant ajoutés et supprimés d'une ArrayList,
ainsi vous pouvez en avoir autant que vous voulez.