JSem: 5.5
|
La programmazione multithreads può causare problemi quando più threads cercano di usare una stessa risorsa (per esempio lo schermo o anche una variabile). Supponiamo che un thread, T, vada a leggere il valore di una certa variabile, X, ma che appena letto il valore, T venga sospeso (perchè per esempio, il suo tempo è scaduto). Mentre T è sospeso un altro thread, T', cambia il valore di X; quando T riparte ad eseguire il suo flusso, potrebbe prendere una decisione importante per il nostro programma basandosi su un valore della variabile ormai scaduto, questo potrebbe avere delle conseguenze catastrofiche per il programma.
Per evitare questo tipo di problema bisogna quindi sincronizzare i vari threads quando usano una stessa risorsa in comune. Si ricorda che anche se si crea un unico thread in un'applet, l'applet avrà comunque più di un thread.
Il problema è quindi di assicurare che un thread che accedde a una risorsa comune abbia un accesso esclusivo a tale risorsa, di modo che nessun altro thread possa intromettersi. In Java questo accesso esclusivo si ottiene usando metodi o istruzioni synchronized.
Brevemente, ogni oggetto ha un lucchetto ("lock"), il lucchetto può essere aperto da un thread, ma un solo thread può aprire il lucchetto a un tempo dato. Se un thread, T', trova il lucchetto aperto da un altro thread, T, allora T' dovrà aspettare che T abbia finito. Se tutti gli accessi a una risorsa comune sono dati da metodi sincronizzati su uno stesso oggetto, allora ogni thread avrà un accesso esclusivo.
Oltre a synchronized, i metodi della classe Object, wait() e notify() aiutano la sincronizzazione.
Per potere chiamare il metodo wait() di un oggetto, il thread deve avere il lucchetto di tale oggetto. Un thread chiama wait() quando vuole aspettare che una certa condizione sia verificata, mentre aspetta rilascia il lucchetto.
Un thread chiama il metodo notify() di un oggetto per informare che una certa condizione è verificata. A questo punto tutti i threads che aspettavano questo evento si rimetterano in azione.
Il metodo wait() lancia un'eccezione che bisogna catturare.
Vediamo su un esempio come gestire questa sincronizzazione.
L'applet seguente è una variazione dell'applet ScrollingText di 5_4.
import java.awt.*;
import java.applet.*;
public class ScrollingText_c extends Applet implements Runnable{
Thread runner;
private volatile int stato;
private final static int VAI = 0, SOSPENDI = 1, TERMINA = 2;
int msg_pos = -1;
int len,x;
Font ft = new Font("Monospaced", Font.BOLD, 30);
FontMetrics fm;
int msgH, msgW, charW, w, h, s = 250;
Color bgcol = new Color(238, 238,238);
String msg ="";
Color txtcol = new Color(0,0,255);
Color txtcol2 = new Color(255,0,0);
Image miaIm;
Graphics m_g;
public void init(){
setBackground(bgcol);
w = getSize().width;
h = getSize().height;
s = Integer.parseInt(getParameter("speed"));
msg = getParameter("testo");
if(msg == null){
msg = "Buon 2001";
}
len = msg.length();
fm = getFontMetrics(ft);
msgW = fm.stringWidth(msg);
msgH = fm.getAscent();
charW = Integer.parseInt(getParameter("scan"));
}
public void update(Graphics g){
if(miaIm == null){
miaIm = createImage(w,h);
}
m_g = miaIm.getGraphics();
m_g.setColor(getBackground());
m_g.fillRect(0,0,w,h);
paint(m_g);
g.drawImage(miaIm,0,0,this);
}
synchronized public void paint(Graphics g){
if(msg_pos > 0){
g.setFont(ft);
x = w-msg_pos;
if((x >= w/2 )&&(x <= w/2+50)){
g.setColor(txtcol2);
}
else{
g.setColor(txtcol);
}
g.drawString(msg, x,h/2+msgH/2);
}
}
synchronized public void start(){
stato = VAI;
if(runner == null || ! runner.isAlive()){
//il thread non esiste ancora o, per qualche ragione, non c'è più
runner = new Thread(this);
runner.start();
}
else{
notify();
}
}
synchronized public void stop(){
stato = SOSPENDI;
notify();
}
synchronized public void destroy(){
stato = TERMINA;
notify();
}
synchronized void nextFrame(){
msg_pos += charW;
if(w - msg_pos + msgW < 0){
//il msg è scomparso, a sinistra
msg_pos = 0;
}
repaint();
}
public void run(){
while(stato != TERMINA){
synchronized(this){
while(stato == SOSPENDI){ waitDelay();}
}//fine synchronized this
if(stato == VAI){
nextFrame();
waitDelay(s);
}
}
}//fine run()
synchronized void waitDelay(){
try{
wait();
}
catch(InterruptedException e){};
}//fine metodo
synchronized void waitDelay(int milliscds){
/*il thread fa una pausa per il # specificato
di millisecondi o finchè il metodo
notify() è chiamato da un altro thread*/
try{
wait(milliscds);
}
catch(InterruptedException e){};
}//fine metodo
}//fine classe
|
Rispetto alla versione precedente, alcune impostazioni sono diverse: la creazione dell'immagine per il doppio-buffering è fatta in update(), la gestione del thread è fatta attraverso la variabile stato. Questa variabile è dichiarata volatile dove volatile è un modificatore da usare per la comunicazione tra due threads (qui runner e l'applet); questo modificatore è indicato per quelle variabili il cui valore è fissato da un thread e letto da un altro thread. Il thread è controllato con le costanti VAI, TERMINA, SOSPENDI: mentre stato = VAI il thread va, quando stato = TERMINA, il thread deve finire perchè l'applet sta per essere distrutta (il metodo destroy() è quello dell'applet), mentre se stato = SOSPENDI il thread è sospeso, perchè è stato chiamato il metodo stop() dell'applet.
I cambiamenti del valore di stato avvengono nei metodi destroy(), stop() dell'applet e vengono notificati tramite notify().
L'istruzione synchronized(this) che si trova nel metodo run, significa: sincronizza sull'oggetto che contiene il metodo (in questo caso l'applet).
I metodi (paint(), nextFrame(), ...), tranne il run, sono sincronizzati.
La sincronizzazione è un'impresa difficile, per dei programmi non troppo complicati, che non mettono in gioco tanti threads con tante risorse comuni, è forse anche inutile, in certe situazioni più complesse è invece indispensabile; è comunque bene sapere che esiste!
|