1. Programmazione concorrente
Un processo è un programma eseguibile caricato in memoria, con il suo spazio di indirizzamento. Ogni processo esegue un programma diverso e può contenere più threads.
Programma con dei threads
La procedura da seguire è:
- Definire una classe che eredita dalla classe
Thread
e che contiene il metodorun()
class ListSorter extends Thread{ ... public void run() { //Inserire qui il codice che esegue il thread } }
- Creare un’istanza della classe
ListSorter concSorter = new ListSorter(list1);
- Invocare il metodo
start()
, che a sua volta chiamerà il metodorun()
concSorter.start(); // Il metodo start() non viene definito dal programmatore
Interfaccia Runnable
La classe Thread
implementa l’interfaccia chiamata Runnable
, che definisce un singolo metodo run()
, che contiene il codice eseguito dal thread. Questo offre un’altra possibilità di creare un programma con dei threads.
- Definire una classe che implementa l’interfaccia
Runnable
e implementare il metodorun()
.
class ListSorter implements Runnable{
...
public void run(){ //Inserire qui il codice che esegue il thread }
}
- Creare un’istanza della classe
ListSorter concSorter = new ListSorter(list1);
- Creare il thread
Thread myThread = new Thread(concSorter);
- Attivare il metodo
start()
myThread.start();
Nozioni varie
La classe Thread
offre varie funzioni di controllo, come sleep(x)
, interrupt()
, join
, yield
, isalive
...
Ogni thread ha una priorità, compresa tra Thread.Min_Priority
e Thread.Max_Priority
, impostata di default a Thread.Norm_Priority
.
Dati condivisi e interferenza
Quando thread diversi eseguono operazioni in mutua esclusione su dati condivisi si potrebbero generare interferenze sui dati.
Per questo motivo, in certi casi si vorrebbe avere la possibilità di eseguire serie di istruzioni, come se fossero atomiche: facendo così le istruzioni vengono eseguite tutte insieme, prima che il processore cambi il contesto a favore di un altro thread.
In Java nessuna istruzione è di per sé atomica: occorre specificare se una serie di istruzioni deve essere trattata come se fosse atomica: per fare ciò si usa la parola chiave synchronized
.
Metodi synchronized
public class SynchronizedCounter{
private int c = 0;
public synchronized void increment() { c++;}
public synchronized void decrement() { c--;}
public synchronized int value() {return c;}
}
Il Java associa un lock intrinseco a ciascun oggetto.
Quando un metodo synchronnized
viene invocato:
- Se nessun metodo
synchronized
è in esecuzione, l’oggetto viene bloccato e viene eseguito il metodo
- Se c’è già un metodo
synchronized
in esecuzione, il thread chiamante viene sospeso fino a quando il lock dell’oggetto non viene liberato, e in seguito esegue il metodo.
Metodi static synchronized
Quando un metodo static
e synchronized
viene invocato il thread acquisisce il lock intrinseco per l’oggetto associato alla classe: per questo motivo l’accesso ad attributi statici è controllato da un lock speciale, diverso da quello associato ad ogni istanza della classe.
Note su metodi synchronized
- I costruttori NON possono essere
synchronized
.
- Inoltre tutti i dati dichiarati come
final
possono essere letti da metodi NONsynchronized
.
- La parola chiave
synchronized
è un dettaglio di implementazione di un metodo e non viene ereditata: in una sottoclasse abbiamo quindi la possibilità di scegliere se il metodo re-implementato (caso in cui faccio l’override del metodo della super-classe) siasynchronized
o no.
Operazioni di read e parola chiave volatile
Anche se eseguiamo operazioni di read su variabili, è necessario metterle in blocchi synchronized
, poiché la JVM tende a modificare l’ordine di esecuzione di certe operazioni, per motivi di ottimizzazione.
Nel caso in cui non si volesse, ad esempio, creare un metodo synchronized
per fare operazioni di lettura su una variabile, è possibile dichiarare la variabile come volatile
, ad esempio public volatile int x = 0;
.
Questo è utile quando ci sono threads che leggono e basta la variabile, senza scriverla, poiché se vengono eseguite operazioni di scrittura su variabili volatile
, queste non verranno riorganizzate come vuole la JVM, incorrendo quindi in possibili peggioramenti delle prestazioni.
Blocchi di codice synchronized
È possibile dichiarare blocchi di codice di un metodo come synchronized
.
public void AddName(String name){
synchronized(this){
lastName = name;
nameCount++;
}
nameList.add(name);
}
Per fare ciò, è necessario specificare l’oggetto del quale si intende acquisire il lock (in questo esempio è l’oggetto stesso).
Situazioni pericolose
Deadlock: È una situazione nella quale duo o più thread sono bloccati per sempre, perché sono in attesa di lock bloccati da altri thread.
Starvation: È una situazione dove un thread ha difficoltà ad accedere ad una risorsa condivisa.
Livelock: Errore di design che genera una sequenza ciclica di operazioni inutili per il vero scopo computazionale.
Wait
Di seguito viene riportato un pezzo di codice che mostra come scrivere bene il codice per fare andare il thread in wait.
class Account{
private float balance;
...
synchronized public void withdraw(float money){
while (balance-money<0){
wait();
}
balance -= money;
}
}
Ciclo di vita di un thread
Un thread che è in stato di wait può essere svegliato solo da una notify()
o da una notifyAll()
.
La notify()
sveglia solo un thread e in modo casuale, quindi di solito si usa la notifyAll()
per svegliare un thread.
Quando in un thread si esegue la chiamata di una wait()
è necessario avere la possibilità di catturare InterruptedException
, che può essere lanciata da un altro thread che esegue una interrupt()
, che termina il processo.
Il metodo più semplice per fare ciò è aggiungere la clausola throws InterruptedException
al metodo che chiama la wait()
, cosicché se qualcuno invoca una interrupt()
, il metodo in wait non si deve preoccupare di gestire l’eccezione, visto che la passa in alto.
Oggetti di tipo lock
I metodi synchronized
offrono dei lock impliciti negli oggetti. Tuttavia, il package java.util.concurrent.locks
offre la possibilità di usare dei lock espliciti.
Un lock definito con l’interfaccia Lock
può essere acquisito solo da un thread, come nel caso dell’acquisizione di un lock intrinseco di un oggetto.
Però è anche possibile usare dei metodi riguardanti i lock:
trylock
: il thread prova ad acquisire il lock e, nel caso in cui non fosse possibile, il thread non va in attesa della liberazione del lock
lockInterruptibly
: permette di ritirarsi dall’acquisizione del lock, se un altri thread manda un interrupt prima che il lock venga acquisito
Esecutori
Introducono un nuovo metodo per creare un programma con threads.
Se r
è un Runnable ed e
è un Executor, invece di scrivere (new Thread(r)).start();
possiamo scrivere e.execute(r);
.
Le implementazioni dell’interfaccia Executors
usano le thread pools, che sono insiemi di Thread che esistono fuori da Runnable.
Collezioni concorrenti
java.util.concurrent
offre delle versioni concorrenti delle collezioni, come BlockingQueue
, che è una coda FIFO che non permette di inserire se la coda è piena e di rimuovere se la coda è vuota, e ConcurrentMap
.
Variabili e array atomici
java.util.atomic
definisce classi che supportano operazioni atomiche su variabili singole: ognuna di esse possiede getter e setter, insieme ad un metodo compareAndSet
.
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicCounter {
private AtomicInteger c = new AtomicInteger(0);
public void increment() {
c.incrementAndGet();
}
public void decrement() {
c.decrementAndGet();
}
public int value() {
return c.get();
}
}
Note varie
- Se un attributo può essere scritto solo dal costruttore, non occorre necessariamente mettere le operazioni di lettura in un blocco synchronized.
- L’assegnamento non è di per sé un’operazione atomica