Programmazione in rete e laboratorio JAVA - Thread Alberto Martelli THREAD Thread: flusso sequenziale di controllo (esecuzione di istruzioni) in un programma. Nello stesso programma si possono far partire più thread che sono eseguiti concorrentemente. Tutti i thread condividono le stesse variabili del programma, a differenza dai processi che hanno ciascuno il proprio contesto (lightweight process). Nei computer a singola CPU la concorrenza viene simulata con una politica di scheduling che alterna l'esecuzione dei singoli thread. Thread 2 Una applicazione Java che usa i thread può eseguire più attività contemporaneamente. Esempio: aggiornare l'informazione grafica sullo schermo e accedere alla rete. Ad esempio, quando un main crea una finestra (JFrame), viene attivato un thread di interfaccia utente, diverso da quello del main. Il costruttore del JFrame è eseguito dal thread del main, mentre il codice dei listener viene eseguito dal nuovo thread. In alcuni casi i thread possono procedere in modo indipendente uno dall'altro (comportamento asincrono), in altri devono essere sincronizzati fra loro (es. produttore - consumatore). Per sapere in quale thread ci si trova si può usare il metodo: Thread.currentThread().getName() (v. BeeperThread) Thread 3 Thread 4 Come si crea un thread Si definisce una classe che eredita da Thread e che implementa un metodo run(). Quando si crea un oggetto di questa classe si esegue il suo metodo predefinito start() per far partire il thread. class MiaClasse extends Thread { System.out.println("Sono il thread " + getname()); MiaClasse t1 = new MiaClasse(); MiaClasse t2 = new MiaClasse(); t1.start(); t2.start(); Thread 5 Per avviare un thread si deve eseguire il metodo start(), che si occupa di inizializzare un nuovo thread e poi chiama run(). Lancia una eccezione se viene chiamato più di una volta. Non è possibile chiamare direttamente il metodo run() perché questo non creerebbe un nuovo thread. L ereditarietà è usata in modo poco naturale. Non ha molto senso dire che MiaClasse è un Thread. Quello che si intende è che è possibile attivare un nuovo thread facendogli eseguire il metodo run() della MiaClasse. (v. ProvaThread e ProvaThread2) Thread 6
Altra formulazione: start viene chiamato direttamente dal costruttore del thread. Ciclo di vita di un thread class MiaClasse extends Thread { public MiaClasse(String s) { super(s); start(); public void run() {System.out.println("Sono il thread " + getname()); new MiaClasse("primo"); new MiaClasse("secondo"); new thread start ready interrupt waiting running terminated run termina NOTA Anche se l'oggetto MiaClasse non viene assegnato ad una variabile non ci sono problemi col garbage collector: l'oggetto rimane finché il thread non termina. Thread 7 Thread 8 Un thread ready può essere eseguito. L'effettiva esecuzione dipende dalla politica dello scheduler. Dopo essere stato attivato, un thread può diventare waiting se chiama il metodo sleep esegue una wait (riparte con notify) sta aspettando una operazione di I/O Il thread si ferma quando la run termina. Esistono anche dei metodo suspend, resume e stop, ma sono deprecati. sleep(long millis) è un metodo di Thread che blocca l esecuzione del thread per il numero specificato di millisecondi. Può generare una eccezione InterruptedException. sleep(500); catch(interruptedexception e) {... sleep è un metodo statico e quindi può essere usato con Thread.sleep(...) anche in una classe che non deriva da Thread. (v. TwoThreadsTest) Thread 9 Thread 10 E' possibile assegnare ai thread una priorità da 1 a 10. Il thread eseguibile con la priorità maggiore rimane in esecuzione fino a quando: cede il controllo chiamando il metodo yield; smette di essere eseguibile; un thread di priorità superiore diventa eseguibile. Se ci sono più thread con la stessa priorità, Java non garantisce la politica con cui i thread saranno gestiti (dipende dal sistema operativo sottostante: es. time slicing). Se si vuole dare la possibilità a tutti i thread di essere attivati, è conveniente far eseguire ogni tanto una sleep ad ogni thread. (v. RaceTest) L'esecuzione di un programma termina quando sono terminati tutti i thread attivati. Un thread può essere dichiarato come "daemon". Un daemon è un thread normale, che però non influenza la terminazione del programma. Un programma termina quando sono terminati tutti i thread non-daemon. Se ci sono dei daemon attivi, la loro esecuzione viene bloccata. E' possibile definire gruppi di thread mediante la classe ThreadGroup (poco utilizzata). Thread 11 Thread 12
I thread sono indispensabili per realizzare interfacce grafiche che rispondano prontamente ai comandi dell'utente, anche se il programma sta eseguendo altre computazioni. Vediamo un esempio di una interfaccia grafica Contatore1 che contiene un listener che esegue un ciclo infinito. Quando il thread dell interfaccia comincia ad eseguire il ciclo del listener, non è più in grado di servire altri eventi. Thread 13 public class Contatore1 extends JFrame { private int count = 0; private Button onoff = new Button("ON-OFF"); private Button start = new Button("START"); private TextField t = new TextField(4); private boolean runflag = true; public Contatore1() { class OnOffL implements ActionListener { runflag =!runflag; class StartL implements ActionListener { go(); private void go() { while (true) { Thread.sleep(500); catch(interruptedexception e) { if (runflag) t.settext(integer.tostring(count++)); Thread 14 Per riuscire a gestire gli altri eventi occorre eseguire il ciclo del listener in un nuovo thread. In questo modo il thread dell interfaccia è libero di gestire altri eventi. (v. ContConThread) Thread 15 public class ContConThread extends JFrame { private int count = 0; private Button onoff = new Button("ON-OFF"); private Button start = new Button("START"); private TextField t = new TextField(4); private boolean runflag = true; private ThreadCont tc = null; private class ThreadCont extends Thread { while (true) { Thread.sleep(100); catch(interruptedexception e) { if (runflag) t.settext(integer.tostring(count++)); class StartL implements ActionListener { if (tc == null) tc = new ThreadCont(); tc.start(); Thread 16 Altro modo di creare un thread Si definisce una classe che implementa l'interfaccia Runnable che possiede il metodo run. class Esempio implements Runnable {... Per attivare un thread è necessario creare un Thread passandogli come parametro al costruttore un oggetto Runnable. Quando si fa partire il thread con start, inizia l'esecuzione del metodo run nel nuovo thread. Esempio es = new Esempio(); Thread t = new Thread(es), t.start(); class MiaClasse implements Runnable { System.out.println("Sono il thread " + Thread.currentThread().getName()); MiaClasse mt = new MiaClasse(); new Thread(mt).start(); new Thread(mt).start(); (ProvaRunnable) Questo secondo modo di creare i thread deve essere usato quando la classe contenente il metodo run è già sottoclasse di un'altra classe (ereditarietà singola). Thread 17 Thread 18
class MiaClasse implements Runnable { int i = 0; i++; System.out.println(i); MiaClasse mt = new MiaClasse(); ProvaRunnable2 Thread t1 = new Thread(mt); Thread t2 = new Thread(mt); t1.start(); t2.start(); Viene stampato prima 1 e poi 2. Infatti esiste un unico oggetto della MiaClasse, legato alla variabile mt, che ha una variabile locale i. I thread t1 e t2 eseguono entrambi il metodo run di questo oggetto, incrementando la stessa variabile i. mt t1 t2 Thread Thread MiaClasse i Thread 19 Thread 20 Sincronizzazione Il meccanismo di sincronizzazione di Java si basa sulla nozione di monitor (Brinch Hansen 1973). Per ogni classe in Java è possibile definire dei metodi synchronized. Java associa un lock ad ogni oggetto della classe (+ un lock alla classe per sincronizzare i metodi statici). Quando un thread chiama un metodo synchronized, l'oggetto diventa bloccato (locked). Altri thread che tentino di accedere allo stesso oggetto chiamando metodi sincronizzati rimangono in coda fino a quando il thread precedente non rilascia l'oggetto, terminando l'esecuzione del metodo. A questo punto uno dei thread in coda può passare, bloccando di nuovo l oggetto. Es: i metodi della classe ArrayList non sono sincronizzati. Possono esserci dei problemi ad eseguire concorrentemente da più thread metodi che modificano un ArrayList. In questo caso conviene racchiudere l ArrayList in un oggetto che fornisce metodi sincronizzati. class ListaSincr { ArrayList l = new ArrayList(); synchronized void add(int index, Object elem) { l.add(index, elem); synchronized Object remove(int index) { return l.remove(index);.. Thread 21 Thread 22 E' possibile bloccare un oggetto senza usare un metodo sincronizzato, mediante un blocco sincronizzato. synchronized (obj) {... codice del blocco sincronizzato... blocca il lock dell'oggetto obj per tutta l'esecuzione del blocco. Cooperazione fra thread Spesso un thread non può eseguire un metodo sincronizzato, anche se ha ottenuto il possesso del lock, perché deve aspettare che si verifichi una qualche condizione che non dipende da lui. In questo caso il thread deve rilasciare il lock, in modo che qualche altro thread possa entrare, mettendosi in attesa, wait, della condizione. Quando un thread realizza la condizione, avvisa, notify, i thread in attesa, in modo che questi possano riprendere l esecuzione. Realizza una sezione critica. l oggetto obj viene usato come un semaforo. Thread 23 Thread 24
Da un metodo sincronizzato si possono chiamare i metodi: wait() - sblocca l'oggetto e mette il thread che lo ha eseguito in una coda di attesa associata all'oggetto; notify() - risveglia un thread a caso fra quelli in attesa, dandogli la possibilità di competere per il lock e di riprendere l'esecuzione dal punto in cui si era messo in wait; notifyall() - risveglia tutti i thread in attesa mettendoli nella coda del lock (ne entra solo uno per volta). Sono metodi di Object e quindi vengono ereditati da qualunque classe. Se si tenta di chiamarli da un metodo non sincronizzato, si ha un errore a runtime. I thread risvegliati dalla notify o notifyall vengono inseriti nella coda del lock dell oggetto ed entrano in competizione per accedere all oggetto. Non c è nessuna garanzia che un thread risvegliato dalla notify passi prima di un thread che era già nella coda del lock. NOTA Nei monitor si possono definire molte condizioni, ciascuna con la propria coda di attesa. In Java invece esiste una sola coda di attesa. Thread 25 Thread 26 Esempio: come realizzare un semaforo binario. class Semaforo { private boolean locked = false; public synchronized void p() { while (locked) wait(); catch(interruptedexception e){ locked = true; public synchronized void v() { if(locked) notify(); locked = false; Il while è indispensabile perché quando un thread risvegliato dalla notify riprende l esecuzione del metodo p, il semaforo potrebbe già essere stato bloccato di nuovo da un altro thread in attesa del metodo p. Thread 27