Sincronizzazione dei Thread in Java Daniela Tibaldi: [email protected] 1) Sottoclasse della classe Thread Thread possiede un metodo run() che la sottoclasse deve ridefinire • si crea un’istanza della sottoclasse tramite new • si esegue un thread chiamando il metodo start() che a sua volta richiama il metodo run() Esempio di classe SimpleThread che è sottoclasse di Thread: public class SimpleThread extends Thread { public SimpleThread(String str){super(str);} public void run() { for (int i = 0; i < 10; i++) { System.out.println(i +" "+ getName()); try{sleep((int)(Math.random()*1000)); }catch (InterruptedException e){} } System.out.println("DONE! " + getName()); } } public class TwoThreadsTest { public static void main (String[] args) { new SimpleThread("Jamaica").start(); new SimpleThread("Fiji").start(); } } E se occorre definire thread che non siano necessariamente sottoclassi di Thread? Thread come classe che implementa l’interfaccia runnable • implementare il metodo run() nella classe che implementa l’interfaccia Runnable • creare un’istanza della classe tramite new • creare un’istanza della classe Thread con un’altra new, passando come parametro l’istanza della classe che si è creata • invocare il metodo start() sul thread creato, producendo la chiamata al suo metodo run() Interfaccia Runnable: maggiore flessibilità Æ thread come sottoclasse di qualsiasi altra classe Esempio di classe EsempioRunnable che implementa l’interfaccia Runnable ed è sottoclasse di MiaClasse class EsempioRunnable extends MiaClasse implements Runnable { // non e’ sottoclasse di Thread public void run() { for (int i=1; i<=10; i++) System.out.println(i + “ ” + i*i); } } public class Esempio { public static void main(String args[]) { EsempioRunnable e = new EsempioRunnable(); Thread t = new Thread(e); t.start(); } } Metodi per il controllo di thread • start() fa partire l’esecuzione di un thread. La macchina virtuale Java invoca il metodo run() del thread appena creato • stop() forza la terminazione dell’esecuzione di un thread. Tutte le risorse utilizzate dal thread vengono immediatamente liberate (lock inclusi), come effetto della propagazione dell’eccezione ThreadDeath • suspend() blocca l'esecuzione di un thread in attesa di una successiva operazione di resume(). Non libera le risorse impegnate dal thread (lock inclusi) • resume() riprende l'esecuzione di un thread precedentemente sospeso. Se il thread riattivato ha una priorità maggiore di quello correntemente in esecuzione, avrà subito accesso alla CPU, altrimenti andrà in coda d'attesa • sleep(long t) blocca per un tempo specificato (time) l'esecuzione di un thread. Nessun lock in possesso del thread viene rilasciato. • join() blocca il thread chiamante in attesa della terminazione del thread di cui si invoca il metodo. Anche con timeout • yield() sospende l'esecuzione del thread invocante, lasciando il controllo della CPU agli altri thread in coda d'attesa I metodi precedenti interagiscono con il gestore della sicurezza della macchina virtuale Java (SecurityManager, checkPermission()) Il problema di stop()e suspend() stop() e suspend() rappresentano azioni “brutali” sul ciclo di vita di un thread Æ rischio di determinare situazioni inconsistenti o di blocco critico (deadlock) • se il thread sospeso aveva acquisito una risorsa in maniera esclusiva, tale risorsa rimane bloccata e non è utilizzabile da altri, perché il thread sospeso non ha avuto modo di rilasciare il lock su di essa • se il thread interrotto stava compiendo un insieme di operazioni su risorse comuni, da eseguirsi idealmente in maniera atomica, l’interruzione può condurre ad uno stato inconsistente del sistema JDK 1.4, pur supportandoli ancora per ragioni di backcompatibility, sconsiglia l’utilizzo dei metodi stop(), suspend() e resume() (metodi deprecated) Si consiglia invece di realizzare tutte le azioni di controllo e sincronizzazione fra thread tramite i metodi wait(), notify() e notifyAll() (astrazione di monitor) Sincronizzazione di Threads Quando due o più thread eseguono concorrentemente, è (in generale) impossibile prevedere l'ordine in cui le loro istruzioni saranno eseguite. Nel caso in cui i thread invochino metodi sullo stesso oggetto SONO POSSIBILI INCONSISTENZE! Mutua Esclusione Differenti thread che fanno parte della stessa applicazione Java condividono lo stesso spazio di memoria Æ è possibile che più thread accedano contemporaneamente allo stesso metodo o alla stessa sezione di codice di un oggetto Servono meccanismi di mutua esclusione sull’accesso al metodo e alla sezione di codice condivisa JVM supporta la mutua esclusione nell’accesso a risorse condivise tramite la keyword synchronized synchronized su: • singolo metodo • blocco di istruzioni In pratica: • a ogni oggetto Java è automaticamente associato un lock • per accedere a un metodo o una sezione synchronized, un thread deve prima acquisire il lock dell’oggetto • il lock è automaticamente rilasciato quando il thread esce dalla sezione synchronized, o se viene interrotto da un’eccezione • un thread che non riesce ad acquisire un lock rimane sospeso sulla richiesta della risorsa fino a che il lock non è disponibile Quando un thread in esecuzione tenta di accedere ad una sezione synchronized di un oggetto il cui lock è già stato acquisito da un altro thread, che esegue concorrentemente, esso si mette in attesa di poter acquisire il lock. Il thread rimane in stato runnable Æ entry-set Appena il lock è rilasciato, lo scheduler può potenzialmente mettere in esecuzione il thread. NOTA: ad ogni oggetto contenente metodi o blocchi synchronized viene assegnata una sola coda di attesa (Æ principale differenza con i monitor: mancanza di variabili condizione) Æ Due thread non possono accedere contemporaneamente a due sezioni synchronized diverse di uno stesso oggetto Sincronizzazione Mutua esclusione uso di lock (tramite keyword synchronized) Politiche di sincronizzazione nell’accesso ad una risorsa condivisa: uso di costrutti synchronized e primitive wait(), notify() e notifyAll() Ogni oggetto Java (istanza di una sottoclasse qualsiasi della classe java.lang.Object) fornisce i metodi di sincronizzazione wait(), notify() e notifyAll() • wait() blocca l’esecuzione del thread invocante in attesa che un altro thread invochi i metodi notify() o notifyAll() per quell’oggetto. Il lock viene rilasciato. Il thread invocante deve essere in possesso del lock sull’oggetto (ossia la wait() deve essere eseguita all’interno di un metodo o sezione synchronized) altrimenti viene lanciata un’eccezione. Il blocco del thread invocante avviene in maniera atomica dopo aver rilasciato il lock. Il thread riacquisisce il lock prima di ritornare dal metodo wait() per evitare situazioni di corsa critica. Anche varianti con specifica di timeout. Quando un thread esegue una wait() si porta in stato Not-Runnable non può essere messo in esecuzione dallo scheduler. Æ wait-set • notify() risveglia un unico thread in attesa sul monitor dell’oggetto in questione. Ciò significa che il thread risvegliato viene portato in stato Runnable e può quindi essere messo in esecuzione dallo scheduler. Se più thread sono in attesa, la scelta avviene in maniera arbitraria, dipendente dall’implementazione della macchina virtuale Java. Il thread risvegliato compete con ogni altro thread, come di norma, per ottenere la risorsa protetta. Anche notify() deve essere chiamata all’interno di un metodo synchronized. • notifyAll() si comporta come notify(), ma risveglia tutti i thread in attesa per l’oggetto in questione. È necessario tutte le volte in cui più thread possono essere sospesi su differenti sezioni critiche dello stesso oggetto oppure per diverse cause (unica coda d’attesa). Per ogni oggetto sincronizzato esiste un’unica coda di attesa (wait-set). Esempio (Controllore per un parcheggio di auto) Progettare un sistema di controllo per l’ingresso ad un parcheggio per auto. Il sistema permette alle auto di entrare solo quando il parcheggio non è pieno e non permette alle auto di uscire quando non ci sono auto nel parcheggio. Simulare l’arrivo e l’uscita di auto tramite threads. Threads: Arrivals, Departures (implementano Runnable). CarParkControl è la classe che fornisce il controllo nell’accesso al parcheggio. class CarParkControl { protected int spaces; protected int capacity; CarParkControl(int capacity) {capacity = spaces = n;} synchronized void arrive() { … --spaces; … } synchronized void depart() { … ++spaces; … } } class Arrivals implements Runnable { CarParkControl carpark; Arrivals(CarParkControl c) {carpark = c;} public void run() { try { while(true) { carpark.arrive(); } } catch (InterruptedException e){} } } e le condizioni di sospensione? class CarParkControl { protected int spaces; protected int capacity; CarParkControl(int capacity){capacity = spaces = n;} synchronized void arrive() throws InterruptedException { while (spaces==0) wait(); --spaces; notifyAll(); } synchronized void depart() throws InterruptedException { while (spaces==capacity) wait(); ++spaces; notifyAll(); } } Ricapitolando… Il parcheggio è l’oggetto sincronizzato ed esiste un’unica coda di attesa sia per i thread rappresentativi delle auto in ingresso sia per quelle in uscita al parcheggio. Æ condizioni di sospensione diverse. • E se per testare la condizione di sospensione avessimo utilizzato if(condizione)wait()? • E se avessi utilizzato la notify() invece che la notifyAll()? • c1, c2, c3 e c4 eseguono il metodo depart() quando il parcheggio è vuoto parcheggio entry-set wait-set p2 p3 p4 • p1 running esegue il metodo arrive() c1 c2 c3 c4 • p2, p3 e p4 cercano di eseguire il metodo arrive Æ si sospendono in attesa dell’acquisizione del lock • p1 termina: esegue la notify() e rilascia il • p2 acquisisce il lock. Esegue il metodo lock arrive() entry-set wait-set parcheggio • p2 termina: esegue la notify() e rilascia il lock p2 c2 p3 p4 c1 c3 c4 parcheggio Il parcheggio è pieno. entry-set p3 p4 c1 c2 wait-set c3 c4 parcheggio entry-set p3 p4 c1 c2 wait-set c3 c4 • p3 acquisisce il lock. Esegue il metodo arrive() Æ parcheggio pieno Æ wait() parcheggio entry-set p4 c1 c2 wait-set • p4 acquisisce il lock. Esegue il metodo arrive() Æ parcheggio pieno Æ wait() parcheggio p3 c3 c4 • c1 acquisisce il lock. Esegue il metodo depart() • c1 termina: esegue la notify() e rilascia il lock parcheggio entry-set c2 c3 wait-set p4 p3 c4 entry-set c1 c2 wait-set p4 p3 c3 c4 parcheggio entry-set c2 c3 wait-set p4 p3 c4 • c2 acquisisce il lock. Esegue il metodo depart() • c2 termina: esegue la notify() e rilascia il lock parcheggio entry-set c3 c4 wait-set p4 p3 • c3 acquisisce il lock. Esegue il metodo depart() Æ parcheggio vuoto Æ wait() • c4 acquisisce il lock. Esegue il metodo depart() Æ parcheggio vuoto Æ wait() Æ Deadlock!!! public class Mailbox { private int[]contenuto; private int contatore, testa, coda; Esempio: Buffer circolare public mailbox(){ contenuto = new int[N]; contatore = 0; testa = 0; coda = 0; } public synchronized int preleva (){ int elemento; while (contatore == 0) wait(); elemento = contenuto[testa]; testa = (testa + 1)%N; --contatore; notifyAll(); return elemento; } public synchronized void deposita (int valore){ while (contatore == N) wait(); contenuto[coda] = valore; coda = (coda + 1)%N; ++contatore; notifyAll(); } } public class GuardedBoundedBuffer implements Buffer { private List data; private final int capacity; public GuardedBoundedBuffer(int capacity) { data = new ArrayList(capacity); this.capacity = capacity; } public synchronized Object take() throws Failure { while (data.size() == 0) try { wait(); } catch(InterruptedException ex) {throw new Failure();} Object temp = data.get(0); data.remove(0); notifyAll(); return temp; } public synchronized void put(Object obj) throws Failure { while (data.size() == capacity) try { wait(); } catch(InterruptedException ex) {throw new Failure();} data.add(obj); notifyAll(); } public synchronized int size() { return data.size(); } public int capacity() { return capacity; } } Altro Esempio Principale limitazione dei meccanismi Java: unica wait-set per un oggetto sincronizzato Æ difficoltà di sospendere thread su differenti code, mancanza dei semafori privati Produttori e Consumatori Concorrenti Possibili Politiche di Accesso • se ci sono dei lettori già attivi sulla risorsa, può un altro thread lettore accedere anche se c’è già uno scrittore sospeso? • se sulla risorsa è attivo uno scrittore e in attesa ci sono lettori e scrittori, a chi dare la precedenza? Ai lettori? Agli scrittori? Al primo che ha fatto la richiesta? Random? In maniera alternata? Esempio: class ReadersWriters extends Rwbasic { private int nr=0; private synchronized void startRead() { nr++; } public synchronized void endRead() { nr--; if (nr==0) notify(); // sveglia i Writers in attesa } public void read() { // il metodo read non è più synchronized startRead(); System.out.println(“wrote “ +data); endRead(); } public synchronized void write() { while (nr>0) //finché ci sono dei readers try { wait();} catch (InterruptedException ex) {return;} data++; System.out.println(“wrote “ +data); notify; //necessaria per gli altri writers in attesa } } class Reader extends Thread { int rounds; ReadersWriters rw; public Reader(int rounds, ReadersWriters rw) { this.rounds=rounds; this.rw=rw;} public void run() { for (int i=0; i< rounds; i++) {rw.read();} } } class Writer extends Thread { int rounds; ReadersWriters rw; public Writer(int rounds, ReadersWriters rw) { this.rounds=rounds; this.rw=rw;} public void run() { for (int i=0; i<rounds; i++) {rw.write();} } } class Main { static ReadersWriters rw; rw=new ReadersWriters (); public static void main(String[] args) { int rounds=Integer.parseInt(args[0], 10); new Reader(rounds, rw).start(); new Writer(rounds, rw).start(); } } A chi viene data la priorità nell’accesso alla risorsa condivisa (variabile data)? Variante: public abstract class RW { protected int activeReaders = 0; protected int activeWriters = 0; protected int waitingReaders = 0; protected int waitingWriters = 0; public void read() { beforeRead(); try { doRead(); } finally { afterRead(); } } public void write() { beforeWrite(); try {doWrite(); } finally { afterWrite();} } protected boolean allowReader() { return waitingWriters == 0 && activeWriters == 0; } protected boolean allowWriter() { return activeReaders == 0 && activeWriters == 0; } protected synchronized void beforeRead() { ++waitingReaders; while (!allowReader()) try { wait(); } catch (InterruptedException ex){ ... } --waitingReaders; ++activeReaders; } protected abstract void doRead(); protected synchronized void afterRead() { --activeReaders; notifyAll(); } protected synchronized void beforeWrite() { ++waitingWriters; while (!allowWriter()) try { wait(); } catch (InterruptedException ex){ ... } --waitingWriters; ++activeWriters; } protected abstract void doWrite(); protected synchronized void afterWrite() { --activeWriters; notifyAll(); } } Semafori in Java Utilizzando i costrutti primitivi di Java si possono realizzare meccanismi più sofisticati (semafori e anche monitor). public class Semaphore { private int value; public Semaphore (int initial){ value = initial; } synchronized public void V() ++value; notify(); } synchronized public void P() throws InterruptedException{ while (value == 0) wait(); --value; } } Produttore/Consumatore con Semafori Java implements Buffer { class SemaBuffer protected Object[] buf; protected int in = 0; protected int out = 0; protected int count = 0; protected int size = 0; Semaphore full; //numero dei messaggi Semaphore empty; //numero degli spazi SemaBuffer(int size) { this.size=size; buf=new Object[size]; full = new Semaphore(0); empty = new Semaphore(size); } } synchronized public void put(Object o) throws InterruptedException { empty.P(); buf[in] = o; ++count; in=(in+1)%size; full.V(); } synchronized public Object get() throws InterruptedException{ full.P(); Object o =buf[out]; buf[out]=null; --count; out=(out+1)%size; empty.V(); return (o); } Funziona tutto bene??? L’esempio sopra non garantisce liveness, perché ci può essere deadlock: • se il buffer è vuoto • il consumatore fa una get • il consumatore si blocca e rilascia il lock sul semaforo • il produttore invoca put ma viene sospeso sul lock del buffer, che non è stato rilasciato dal consumatore. In questo caso il deadlock è causato da chiamate innestate (nested monitor). Esempio corretto public void put(Object o) throws InterruptedException { empty.P(); synchronized(this){ buf[in] = o; ++count; in=(in+1)%size; } full.V(); } Il deadlock può essere evitato solo attraverso un progetto attento, sincronizzando la regione critica di accesso al buffer solo dopo il passaggio del semaforo. Quindi la realizzazione di costrutti di sincronizzazione di alto livello in Java richiede particolare attenzione.
© Copyright 2024 Paperzz