3.3) Multithreading |
Java supporta il multithreading in modo nativo,
a livello di linguaggio. Ciò rende la programmazione di
multipli thread molto più semplice che dovendo usare librerie
apposite, come è il caso di altri linguaggi.
L'ambiente Java fornisce la classe Thread
per gestire
i thread di esecuzione. Ogni oggetto istanziato da tale classe
(o da una sua derivata) costituisce un flusso separato di esecuzione
(ossia un thread di esecuzione).
Si noti in proposito che il metodo main()
di un qualunque
oggetto attiva un thread, che termina con la terminazione del
main()
stesso.
3.3.1) Classe Thread |
E' la rappresentazione runtime di un thread
di esecuzione.
Costruttori
Ce ne sono vari, i più usati sono i tre elencati sotto.
Il terzo serve nel caso si faccia uso dell'interfaccia
Runnable
(che vedremo più avanti).
public Thread();
public Thread(String name);
public Thread(Runnable target);
Metodi più importanti
Ci sono vari metodi statici (che vengono chiamati sul thread corrente),
i più utili dei quali sono:
public static void sleep(long millis) throws InterruptedException;
Questo metodo mette a dormire il thread corrente permillis
millisecondi. In questo periodo, altri thread possono avanzare nell'esecuzione. Se in questo tempo qualcun altro chiama il suo metodointerrupt()
, il thread viene risvegliato da unaInterruptedException
.
public static void yield() throws InterruptedException;
Questo metodo fa si che il thread corrente ceda la Cpu ad altri thread in attesa. Poiché l'ambiente Java non può garantire la preemption (essa dipende dal sistema operativo) è consigliabile usarlo, quando un thread deve effettuare lunghe computazioni, a intervalli regolari.
I metodi di istanza più utili (che possono essere chiamati
su qualunque thread) sono:
public synchronized void start() throws IllegalThreadStateException;
E' il metodo che si deve chiamare per far partire un thread, una volta creato; è un errore chiamarlo su un thread già avviato.
public void run();
E' il metodo che l'ambiente runtime chiama quando un thread viene avviato con il metodostart()
. costituisce il corpo eseguibile del thread, e determina il suo comportamento. Quando esso finisce, il thread termina. E' dunque il metodo che ogni classe derivata daThread
deve ridefinire.
public void stop();
Termina il thread.
public void suspend();
Sospende il thread; altri possono eseguire.
public void resume();
Rende il thread nuovamente eseguibile, cioè ready (Nota: questo non significa che diventi anche running, in quanto ciò dipende anche da altri fattori).
In più, ci sono altri metodi per gestire la priorità,
avere notizie sullo stato del thread, ecc.
Esempio 6 |
Programma che consiste di due thread, i quali scrivono un messaggio
ciascuno sullo standard output con cadenze differenti.
import java.io.*; import java.lang.*; public class PingPong extends Thread { String word; int delay; //-------------------------------------------------- public PingPong (String whatToSay, int delayTime) { word = whatToSay; delay = delayTime; } //-------------------------------------------------- public void run () { try { while (true) { System.out.println(word); sleep(delay); } } catch (InterruptedException e) { return; } } //-------------------------------------------------- public static void main(String args[]) { new PingPong("ping", 250).start(); //1/4 sec. new PingPong("PONG", 100).start(); //1/10 sec. } } |
3.3.2) Interfaccia Runnable |
Vi sono alcune situazioni nelle quali il ricorso
a una estensione della classe Thread
non è
adatto agli scopi, poiché:
Thread
);
Thread
.
La risposta a questo problema è data dalla interfaccia
Runnable
, che include un solo metodo:
public abstract void run();
dall'ovvio significato: in esso si specifica il comportamento
del thread da creare, come visto prima.
Basta quindi definire una classe che implementa tale interfaccia:
public MyClass implements Runnable {
...
}
ed includervi un metodo run()
:
public abstract void run(){
...
}
per avere la possibilità di lanciare thread multipli all'interno
di un oggetto di tale classe.
Tali thread vengono creati col terzo dei costruttori visti per
la classe Thread
e successivamente vengono attivati
invocandone il metodo start()
, che a sua volta causa
l'avvio del metodo run()
dell'oggetto che è
passato come parametro nel costruttore:
...
new Thread(theRunnableObject).start();
...
Esempio 7 |
Funzionalità simile all'esempio precedente ma ottenuta
con l'interfaccia Runnable
.
import java.io.*; import java.lang.*; public class RPingPong /*extends AnotherClass*/ implements Runnable { String word; int delay; //-------------------------------------------------- public RPingPong (String whatToSay, int delayTime) { word = whatToSay; delay = delayTime; } //-------------------------------------------------- public void run () { try { while (true) { System.out.println(word); Thread.sleep(delay); } } catch (InterruptedException e) { return; } } //-------------------------------------------------- public static void main(String args[]) { Runnable ping = new RPingPong("ping", 100); //1/10 sec. Runnable PONG = new RPingPong("PONG", 100); //1/10 sec. new Thread(ping).start(); new Thread(ping).start(); new Thread(PONG).start(); new Thread(PONG).start(); new Thread(PONG).start(); new Thread(PONG).start(); new Thread(PONG).start(); } } |
Esempio 8 |
Applicazione che apre una connessione di rete, come nell'esempio
4. La differenza è che un thread separato rimane in ascolto
delle risposte del server: questo permette di ricevere risposte
costituite da più linee di testo senza alcun problema.
Il codice è costituito da due classi. La prima, BaseAppE8
,
è basata su quella dell'esempio 4 e provvede ad istanziare
la seconda che ha il compito di attivare la connessione e di gestire
la comunicazione. Se ne mostrano qui solo i frammenti di codice
rilevanti, sottolineando il fatto che non c'è più
bisogno del bottone "Receive", visto che un thread separato
rimane in costante ascolto delle risposte.
import java.awt.*; public class BaseAppE8 extends Frame { ...ecc. //-------------------------------------------------- void button1_Clicked(Event event) { baseTConn = new BaseTConn(textField1.getText(), textField2.getText(), textArea1, textArea2); } //-------------------------------------------------- void button2_Clicked(Event event) { baseTConn.send(); } //-------------------------------------------------- void button3_Clicked(Event event) { baseTConn.close(); } //-------------------------------------------------- |
La seconda classe si occupa della comunicazione, ed inolte attiva
tramite il metodo run()
un thread separato costituito
da un ciclo infinito in cui si ricevono le risposte del server
e si provvede a mostrarle sullo schermo.
import java.awt.*; import java.lang.*; import java.io.*; import java.net.*; public class BaseTConn implements Runnable { TextArea commandArea, responseArea; Socket socket = null; PrintStream os = null; DataInputStream is = null; //-------------------------------------------------- public BaseTConn(String host, String port, TextArea commandArea, TextArea responseArea) { this.commandArea = commandArea; this.responseArea = responseArea; try { socket = new Socket(host, Integer.parseInt(port)); os = new PrintStream(socket.getOutputStream()); is = new DataInputStream(socket.getInputStream()); responseArea.appendText("***Connection established" + "\n"); new Thread(this).start(); } catch (Exception e) { responseArea.appendText("Exception" + "\n"); } } //-------------------------------------------------- public void run() { String inputLine; try { while ((inputLine = is.readLine()) != null) { responseArea.appendText(inputLine + "\n"); } } catch (IOException e) { responseArea.appendText("IO Exception" + "\n"); } } //-------------------------------------------------- public void send() { os.println(commandArea.getText()); } //-------------------------------------------------- public void close() { try { is.close(); os.close(); socket.close(); responseArea.appendText("***Connection closed" + "\n"); } catch (IOException e) { responseArea.appendText("IO Exception" + "\n"); } } } |
3.4 Sincronizzazione |
Come in tutti i regimi di concorrenza, anche
in Java possono sorgere problemi di consistenza dei dati condivisi
se i thread sono cooperanti.
I dati condivisi fra thread differenti possono essere:
Thread
,
che sono quindi condivise da tutte le sue istanze;
Runnable
,
che sono condivise da tutti i thread attivati dentro tale oggetto.
Per risolvere i problemi legati alla concorrenza, in Java è
stata incorporata nella classe Object
(e quindi in
tutte le altre, che derivano da essa) e nelle sue istanze la capacità
potenziale di funzionare come un monitor.
3.4.1) Metodi sincronizzati |
In particolare, tale funzionalità si
attiva ricorrendo ai metodi sincronizzati,
definiti come:
public void synchronized aMethod(...) {
...
}
Quando un thread chiama un metodo sincronizzato di un oggetto,
acquisisce un lucchetto
su quell'oggetto. Nessun altro thread può chiamare un qualunque
metodo sincronizzato dello stesso oggetto finché il lucchetto
non viene rilasciato. Se lo fa, verrà messo in attesa che
ciò avvenga.
Si noti che eventuali metodi non sincronizzati di quell'oggetto
possono comunque essere eseguiti da qualunque thread, in concorrenza
col thread che possiede il lucchetto.
Dunque:
Ad esempio, consideriamo una classe che rappresenta un acconto
bancario, sul quale possono potenzialmente essere fatte molte
operazioni contemporaneamente:
public class Account { private double balance; //-------------------------------------------------- public Account(double initialDeposit) { balance = initialDeposit; } //-------------------------------------------------- public synchronized double getBalance() { return balance; } //-------------------------------------------------- public synchronized void deposit(double amount) { balance += amount; } } |
Non può succedere che un thread legga il valore del conto
mentre un altro thread lo aggiorna, o che due thread lo aggiornino
in concorrenza.
Si noti che il costruttore non ha bisogno di essere sincronizzato,
perché è eseguito solo per creare un oggetto, il
che avviene una sola volta per ogni nuovo oggetto.
Anche i metodi statici possono essere sincronizzati. In questo
caso il lucchetto è relativo alla classe e non alle sue
istanze. In altre parole, sia una classe che le sue istanze possono
funzionare come monitor. Va notato come tali monitor siano indipendenti,
cioè il lucchetto sulla classe non ha alcun effetto sui
metodi sincronizzati delle sue istanze e viceversa.
3.4.2) Istruzioni sincronizzate |
E' possibile eseguire del codice sincronizzato,
che quindi attiva il lucchetto di un oggetto, anche senza invocare
un metodo sincronizzato di tale oggetto. Ciò si ottiene
con le istruzioni sincronizzate,
che hanno questa forma:
...
synchronized (anObject)
{
... // istruzioni sincronizzate
}
...
Questo modo di ottenere la sincrinizzazione fra thread cooperanti
richiede maggior attenzione da parte del programmatore, che deve
inserire blocchi di istruzioni sincronizzate in tutte le parti
di codice interessate.
Esempio 9 |
Applicazione che realizza un server multithreaded, il quale:
Costituisce, dunque, un server per una semplice chatline.
Può essere usato con una semplice variazione del client
visto nell'esempio 8: il client, appena si connette, deve inviare
una linea di testo che il server usa come identificativo dell'utente
connesso.
In tal modo si realizza di fatto una minimale architettura client-server.
Il codice è costituito da due classi. La prima, ChatServer
,
accetta richieste di connessione sul port 5000 e, ogni volta che
ne arriva una, istanzia un oggetto della classe ChatHandler
che si occupa di gestirla.
import java.net.*; import java.io.*; import java.util.*; public class ChatServer { //-------------------------------------------------- public ChatServer() throws IOException { ServerSocket server = new ServerSocket(5000); System.out.println ("Accepting connections..."); while(true) { Socket client = server.accept(); System.out.println ("Accepted from " + client.getInetAddress()); new ChatHandler(client).start(); } } //-------------------------------------------------- public static void main(String args[]) throws IOException { new ChatServer(); } } |
La seconda si occupa della gestione di una singola connessione
e dell'invio a tutte le altre, in broadcast, dei dati provenienti
da tale connessione.
import java.net.*; import java.io.*; import java.util.*; public class ChatHandler extends Thread { protected static Vector handlers = new Vector(); protected String userName; protected Socket socket; protected DataInputStream is; protected PrintStream os; //-------------------------------------------------- public ChatHandler(Socket socket) throws IOException { this.socket = socket; is = new DataInputStream(new BufferedInputStream(socket.getInputStream())); os = new PrintStream(new BufferedOutputStream(socket.getOutputStream())); } //-------------------------------------------------- public void run() { try { userName = is.readLine(); os.println("Salve " + userName + ", benvenuto nella chatline di Reti II"); os.flush(); broadcast(this, "Salve a tutti! Ci sono anch'io."); handlers.addElement(this); //e' un metodo sincronizzato while (true) { String msg = is.readLine(); if (msg != null) broadcast(this, msg); else break; } } catch(IOException ex) { ex.printStackTrace(); } finally { handlers.removeElement(this); //e' un metodo sincronizzato broadcast(this, "Arrivederci a presto."); try { socket.close(); } catch(Exception ex) { ex.printStackTrace(); } } } //-------------------------------------------------- protected static void broadcast(ChatHandler sender, String message) { synchronized (handlers) { //ora nessuno puo' aggiungersi o abbandonare Enumeration e = handlers.elements(); while (e.hasMoreElements()) { ChatHandler c = (ChatHandler) e.nextElement(); try { c.os.print("Da " + sender.userName); c.os.print("@" + sender.socket.getInetAddress().getHostName() + ": "); c.os.println(message); c.os.flush(); } catch(Exception ex) { c.stop(); } } } } } |
3.4.3) Wait() e Notify() |
In Java esiste anche un meccanismo per sospendere
e risvegliare i thread che si trovano all'interno di un monitor,
analogo a quello offerto dalle variabili di condizione.
Esistono tre metodi della classe Object che servono a questo:
public final void wait() throws InterruptedException;
Un thread che chiama questo metodo di un oggetto viene sospeso finché un altro thread non chiamanotify()
onotifyAll()
su quello stesso oggetto. Il lucchetto su quell'oggetto viene temporaneamente e atomicamente rilasciato, così altri thread possono entrare.
public final void notify() throws IllegalMonitorStateException;
Questo metodo, chiamato su un oggetto, risveglia un thread (quello in attesa da più tempo) fra quelli sospesi dentro quell'oggetto. In particolare, tale thread riprenderà l'esecuzione solo quando il thread che ha chiamatonotify()
rilascia il lucchetto sull'oggetto.
public final void notifyAll() throws IllegalMonitorStateException;
Questo metodo, chiamato su un oggetto, risveglia tutti i thread sospesi su quell'oggetto. Naturalmente, quando il chiamante rilascerà il lucchetto, solo uno dei risvegliati riuscirà a conquistarlo e gli altri verranno nuovamente sospesi.
E' importante ricordare che questi metodi possono essere usati solo all'interno di codice sincronizzato sullo stesso oggetto per il quale si invocano, e cioè:
Esempio 10 |
Esempio di produttore-consumatore che fa uso di wait()
e notify()
. La classe Consumer
rappresenta
un consumatore, che cerca di consumare un oggetto (se c'è)
da un Vector
.
Usa un thread separato per questo, e sfrutta wait()
per rimanere in attesa di un oggetto da consumare.
Il produttore è costituito dall'utente, che attraverso
il thread principale (quello del main()
) aggiunge
oggetti e invia notify()
per risvegliare il consumatore.
import java.util.*; import java.io.*; public class Consumer extends Thread { protected Vector objects; //-------------------------------------------------- public Consumer () { objects = new Vector(); } //-------------------------------------------------- public void run () { while (true) { Object object = extract (); System.out.println (object); } } //-------------------------------------------------- protected Object extract () { synchronized (objects) { while (objects.isEmpty ()) { try { objects.wait (); } catch (InterruptedException ex) { ex.printStackTrace (); } } Object o = objects.firstElement (); objects.removeElement (o); return o; } } //-------------------------------------------------- public void insert (Object o) { synchronized (objects) { objects.addElement (o); objects.notify (); } } //-------------------------------------------------- public static void main (String args[]) throws IOException, InterruptedException { Consumer c = new Consumer (); c.start (); DataInputStream i = new DataInputStream (System.in); String s; while ((s = i.readLine ()) != null) { c.insert (s); Thread.sleep (1000); } } } |