Ingegneria del software
Ing. Jody Marca – jody.marca@polimi.it
Cosa faremo oggi
Concorrenza
Sincronizzazione
Concorrenza
Nei programmi Java fino ad ora sviluppati è presente solo un flusso di esecuzione sequenziale.
La concorrenza ci permette di gestire nel medesimo programma più flussi di esecuzione
La concorrenza è fondamentale per creare applicazioni interattive o client server multiutente
I Thread
La concorrenza si può ottenere attraverso una gestione multi-processo o multi-thread
Solitamente si usano i thread poiché sono più leggeri e utilizzano una comunicazione a memoria condivisa
In Java un thread può essere creato utilizzando uno dei seguenti metodi
Estensione della classe java.lang.Thread
Implementazione dell’intefaccia java.lang.Runnable
Thread – Estensione della classe Thread
Definire una classe che estende la classe Thread e contiene un metodo run()
public class SimpleThreadMode1 extends Thread {
public void run() { //Contiene il codice che eseguirà il thread } }
Istanziare la classe
SimpleThreadMode1 thread = new SimpleThreadMode1();
Avviare il thread chiamando il metodo start() il quale richiamerà il metodo run() definito nel thread
thread.start();
Thread – Implementazione dell’interfaccia Runnable
Definire una classe che implementa l’interfaccia Runnable e contiene un metodo run()
public class SimpleThreadMode2 implements Runnable {
public void run() { //Contiene il codice che eseguirà il thread } }
Instanziare la classe
SimpleThreadMode2 runTask = new SimpleThreadMode2();
Instanziare un thread
Thread thread = new Thread(runTask);
Avviare il thread chiamando il metodo start() il quale richiamerà il metodo run() definito nel thread
thread.start();
Aspettare il termine di un thread
Alcune azioni necessitano una certa serializzazione nell’avvio dei thread.
public class SimpleThreadJoin implements Runnable {
public void run() {
System.out.println("Ciao sono il thread" );
for(int i=0; i<10; i++){
System.out.println("thread - stampa numero: "+(i+1));
try { Thread.sleep(500); }
catch (InterruptedException e) {}
System.out.println("thread - Termino");
}
Aspettare il termine di un thread / 2
public static void main(String[] args) {
SimpleThreadJoin threadTask = new SimpleThreadJoin();
Thread thread = new Thread(threadTask);
thread.start();
try {
thread.join(); //Attendo che termini il thread } catch (InterruptedException e) {}
System.out.println("Creazione dei threads terminata");
} }
Interrupts
Gli Interrupts sono utilizzati per bloccare alcune azioni che sta eseguendo un thread.
public class SimpleThreadInterrupt implements Runnable {
public void run() {
System.out.println("Ciao sono il thread” );
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
System.out.println("thread - Ricevuto un interrupt");
}
System.out.println("thread - Termino");
}
Interrupts \ 2
public static void main(String[] args) {
SimpleThreadInterrupt threadTask = new SimpleThreadInterrupt();
Thread thread = new Thread(threadTask);
thread.start();
System.out.println("Creazione del thread terminata");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {}
System.out.println("Lancio interrupt al thread");
thread.interrupt();
}
}
Sincronizzazione
Sappiamo che l’esecuzione di più threads concorrenti non è deterministica.
Cosa succede se dobbiamo gestire dei dati condivisi tra più thread???
È necessario introdurre un meccanismo di Lock che ci
permetta di essere sicuri che l’accesso alla modifica dei dati condivisi sia effettuato in modo mutuamente
esclusivo
Sincronizzazione / 2
Dobbiamo implementare un accesso esclusivo ai dati.
Java ci fornisce la possibilità di sincronizzare l’accesso dei thread a dei metodi o a determinate porzioni di codice
Immaginiamo di implementare la cassa del progetto
magazzino implementato nella prima lezione e di gestire i diversi utenti del magazzino utilizzando dei Thread
Dobbiamo assicurare che le operazioni siano di modifica del saldo siano sincronizzate
Synchronized
Utilizziamo la parola chiave synchronized per sincronizzare l’accesso ai metodi
public class Cassa {
private Double saldo = null;
public Cassa(Double saldo) { this.saldo = saldo; }
public synchronized void vendi(Double daVersare){
saldo = saldo + daVersare;}
public synchronized void acquista(Double daPrelevare){
saldo = saldo – daPrelevare;}
}
Synchronized /2
La parola chiave synchronized permette un accesso
sincronizzato a tutti e solo i metodi definiti synchronized
Ci garantisce quindi che se un thread sta accedendo a un metodo synchronized nessun altro thread possa
accedere a qualsiasi metodo synchronized della classe stessa. Non ci garantisce pero che non possa accedere in modo concorrente ad altri metodi della classe non definiti synchronized.
I costruttori non possono essere synchronized
Synchronized Statements
Un altro modo di gestire la sincronizzazione in Java è utilizzare dei Synchronized Statements
In questo caso è necessario dichiarare l’oggetto su si vuole porre il lock.
I Synchronized Statements sono utili per implementare
una gestione della sincronizzazione a granularità più fine Usate i Synchronized Statements con cura dovete essere
assolutamente sicuri di ciò che state facendo
Synchronized Statements - Esempio 1
Eliminiamo synchronized dai metodi della classe cassa sostituendoli con i Synchronized Statements
public void vendi(Double daVersare){
Double saldoPre = null, saldoPost = null;
synchronized(this) {
saldoPre = saldo;
saldo = saldo + daVersare;
}
saldoPost = saldo;
System.out.println("Venduto "+daVersare+" saldo precedente
"+saldoPre+" attuale "+saldoPost);
}
Synchronized Statements - Esempio 2
Immaginiamo ora di dover gestire due variabili che
permettano di memorizzare il totale delle vendite e degli acquisti.
Non è necessario porre il lock su tutta la classe ma è
sufficiente sincronizzare le letture e le scritture relative ad ogni singola variabile
Synchronized Statements - Esempio 2 /2
public class CassaSynchronizedStatements2 {
private Double vendite = null, acquisti = null;
//Definisco i lock da utilizzare per l’aggiornamento delle variabili private Object lockVendite = null, lockAcquisti = null;
public CassaSynchronizedStatements2(Double vendite, Double acquisti) { this.vendite = vendite;
this.acquisti = acquisti;
//Inizializzo gli oggetti da utilizzare come lock
lockVendite = new Object();
lockAcquisti = new Object();
}
Synchronized Statements - Esempio 2 /3
public void vendi(Double daVersare){
//Sincronizzo le operazioni sulle vendite
synchronized(lockVendite) {
vendite = vendite + daVersare;
} }
public void acquista(Double daPrelevare){
//Sincronizzo le operazioni sugli acquisti
synchronized(lockAcquisti) {
acquisti = acquisti + daPrelevare;
} }
}
Bloccare e risvegliare una esecuzione
Vogliamo inserire un vincolo sull’acquisto: è possibile acquistare solo se il saldo è positivo
Dobbiamo fare in modo che, qualora il saldo sia negativo, l’azione di acquisto venga temporaneamente sospesa
Per questo scopo si possono utilizzare i metodi:
wait() – interrompe temporaneamente l’esecuzione notify() (o notifyAll() )– risveglia l’esecuzione
Esempio di Wait – Notify
public class Cassa {
private Double saldo =0.0;
public synchronized void vendi(Double daVersare){
saldo = saldo + daVersare;
notifyAll();
}
public synchronized void acquista(Double daPrelevare) throws Exception{
while((saldo – daPrelevare )<=0.0) wait();
saldo = saldo – daPrelevare;
} }
© 2015 – Jody Marca – Threads
Problemi di sincronizzazione - Deadlock
Il Deadlock si verifica quando due o più threads restano bloccati per sempre poiché si aspettano a vicenda
Thread 1
public class ObjA{
private ObjB objB = null;
synchronized void call(){
……
objB.call();
……
} }
Thread 2
public class ObjB{
private ObjA objA = null;
synchronized void call(){
……
objA.call();
……
} }
Locking
Il locking mettere a disposizione uno strumento avanzato e potente che permette di sincronizzare
“l’accesso” in modo esclusivo a porzioni di codice Java
A differenza dei comuni Synchronized statement attraverso l’uso del locking è possibe “tentare” di acquisire il lock in modalità non bloccante attraverso l’operazione tryLock()
Esempio di Locking
public class SharedClass {
private Lock lock = null; //dichiaro il Lock
public SharedClass(){
this.lock = new ReentrantLock(); //inizializzo il Lock }
public void usaRisorsa() throws InterruptedException {
//provo ad acquisire il lock, se entro 5 secondi non lo ottengo // la funzione tryLock ritorna false e continuo
if(lock.tryLock(5, TimeUnit.SECONDS)){
… frammento di codice da eseguire in mutua esclusione … }
} }
Riassumendo: ciclo di vita di un thread
born
dead ready
blocked running
waiting wait notify
notifyAll
start
I/O lock
fine I/O fine lock