I THREAD IN JAVA
ESEMPIO DI CONCORRENZA: PRODUCER CONSUMER
APPLICAZIONE DEL MULTITHREADING E DELLA CONCORRENZA IN JAVA
Il problema del producer-consumer (noto anche come il problema del buffer limitato) è un esempio di sincronizzazione tra processi. Ci sono due processi, uno producer e l’altro consumer, che condividono un buffer di dimensione fissa.
Il producer genera dati e li deposita nel buffer.
Il consumer contemporaneamente utilizza i dati prodotti dal producer, rimuovendoli dal buffer. Il problema consiste nell’assicurarsi che:
- il producer non elabori nuovi dati quando il buffer è pieno
- il consumer non cerchi di leggere dati quando il buffer è vuoto
La soluzione consiste nel:
- sospendere l’esecuzione del producer se il buffer è pieno; quando il consumer preleva un elemento dal buffer, esso provvederà a svegliare il producer, che riprenderà a riempire il buffer
- sospendere l’esecuzione del consumer se il buffer è vuoto; quando il producer avrà inserito i dati nel buffer, esso provvederà a svegliare il consumer, che riprenderà a leggere e svuotare il buffer
La soluzione può essere implementata mediante l’utilizzo di strategie di comunicazione tra processi (tipicamente con i semafori). Se la soluzione non venisse implementata correttamente, potremmo avere una situazione di deadlock, in cui tutti e due i processi restano in attesa di essere risvegliati.
import java.util.List; public class Producer implements Runnable { private final List<Integer> bufferCondiviso; private final int SIZE; private int i = 1; public Producer(List<Integer> bufferCondiviso, int size) { this.bufferCondiviso = bufferCondiviso; this.SIZE = size; } @Override public void run() { while(true) { try { produce(); i++; Thread.sleep(1000); } catch (InterruptedException ex) { ex.printStackTrace(); } } } private void produce() throws InterruptedException { // il thread resta in stato wait se il buffer è pieno while (bufferCondiviso.size() == SIZE) { synchronized (bufferCondiviso) { System.out.println("Il buffer è pieno, il thread Producer resta in attesa... la dimensione del buffer adesso è: " + bufferCondiviso.size()); bufferCondiviso.wait(); } } // il buffer non èpieno, quindi il thread può aggiungere un nuovo elemento e notificarlo al consumer synchronized (bufferCondiviso) { bufferCondiviso.add(i); bufferCondiviso.notifyAll(); System.out.println("Il thread Producer ha aggiunto al buffer l'elemento: " + i + " la dimensione del buffer adesso è: " + bufferCondiviso.size()); } } }
import java.util.List; public class Consumer implements Runnable { private final List<Integer> bufferCondiviso; public Consumer(List<Integer> bufferCondiviso, int size) { this.bufferCondiviso = bufferCondiviso; } @Override public void run() { while (true) { try { System.out.println("Il thread Consumer sta leggendo il buffer... "); consume(); Thread.sleep(1000); } catch (InterruptedException ex) { ex.printStackTrace(); } } } private void consume() throws InterruptedException { // il thread resta in stato wait se il buffer è vuoto while (bufferCondiviso.isEmpty()) { synchronized (bufferCondiviso) { System.out.println("Il buffer è vuoto, il thread Consumer resta in attesa... la dimensione del buffer adesso è: " + bufferCondiviso.size()); bufferCondiviso.wait(); } } // il buffer contiene elementi, quindi il thread può eliminarne uno e notificarlo al producer synchronized (bufferCondiviso) { System.out.println("Il thread Consumer sta leggendo il buffer ed eliminando il seguente elemento: " + bufferCondiviso.remove(0) + " la dimensione del buffer adesso è: " + bufferCondiviso.size()); bufferCondiviso.notifyAll(); } } }
import java.util.LinkedList; import java.util.List; public class TestClass { public static void main(String args[]) { List<Integer> bufferCondiviso = new LinkedList<Integer>(); int size = 4; Thread prodThread = new Thread(new Producer(bufferCondiviso, size), "Producer"); Thread consThread = new Thread(new Consumer(bufferCondiviso, size), "Consumer"); prodThread.start(); consThread.start(); } }
I METODI WAIT NOTIFY E NOTIFYALL
I metodi wait(), notify() e notifyAll() sono definiti all’interno della classe Object.
wait()
- Questo metodo mette in attesa un thread.
- È possibile invocare il metodo wait() solo su oggetti sul quale ha il «lock»
- Il metodo wait() può essere invocato solo in un metodo o blocco di codice synchronized, altrimenti avremo l’eccezione IllegalMonitorStateException
Quando viene invocato il metodo wait() su un oggetto si hanno i seguenti effetti:
- sull’oggetto viene rilasciato il lock
- il thread viene posto in stato «blocked»
Analogamente al metodo wait(), anche sleep() mette in attesa il thread invocante.
Tra i due metodi, però c’è una differenza:
- quando viene invocato il metodo sleep() il lock sull’oggetto non viene rilasciato; quindi, nessun thread può utilizzarlo;
- quando viene invocato il metodo wait(), invece, viene rilasciato il lock sull’oggetto, il quale diventa accessibile agli altri thread
Esistono le seguenti definizioni del metodo wait:
- wait(), causa l’interruzione di un thread finché un altro thread non invoca il metodo notify() o notifyAll()
- wait(long timeout), causa l’interruzione di un thread finché un altro thread non invoca il metodo notify() o notifyAll() o se è stato raggiunto il timeout, espresso in millisecondi, impostato. Se timeout è 0 il comportamento è lo stesso del metodo wait()
- wait(long timeout, int nanos), è analogo a wait(long timeout), solo che al timeout in millisecondi è possibile aggiungere anche i nanosecondi.
notify() e notifyAll()
Il metodo notify() risveglia un thread in attesa su un oggetto che si trovava in stato «lock». Il metodo notifyAll() risveglia tutti i thread in attesa su un oggetto che si trovava in stato «lock». Quando vengono invocati questi metodi, i thread che ricevono la notifica passano in stato Runnable. I thread risvegliati, devono acquisire il «lock» dell’oggetto che era utilizzato dal thread che ha invocato il metodo notify() o notifyAll(). Un thread può invocare questi metodi solo se ha un «lock» sull’oggetto per il quale richiede la notifica. Se viene invocato il metodo notify() su un oggetto su cui nessun thread è in stato «wait», la notifica viene persa (va a vuoto…) Il metodo notifyAll() va utilizzato quando ci sono più thread in attesa. Quando viene invocato il metodo notifyAll() si ha questo effetto:
- Tutti i thread in attesa vengono risvegliati
- Tutti i thread risvegliati si mettono in coda per acquisire il lock sull’oggetto rilasciato
- Solo un thread prenderà il lock, gli altri attenderanno che l’oggetto venga nuovamente rilasciato
Il metodo notify() è più efficiente del metodo notifyAll().
LA SINCRONIZZAZIONE AVANZATA CON L’INTERFACCIA LOCK E LA CLASSE REENTRANTLOCK
LA SINCRONIZZAZIONE MEDIANTE LA KEYWORD SYNCHRONIZED
L’utilizzo della keyword synchronized:
- permette di accedere in maniera esclusiva ad una porzione di codice condivisa
- rende atomico l’accesso ad un blocco di codice, consentendo di evitare i seguenti problemi:
▪ race condition, ovvero il fenomeno secondo cui un output generato in ambiente multi-thread dipende dalla temporizzazione o dalla sequenza con cui sono eseguiti i thread
▪ interleaving, cioè l’accesso ad una risorsa da parte di processi concorrenti
L’utilizzo della keyword synchronized, tuttavia ha alcune limitazioni, ovvero:
- hand over hand (o multi-locking): si ha quando abbiamo più di una risorsa su cui effettuare il locking. L’obiettivo è sincronizzare una risorsa per volta, permettendo ad altri thread di accedere alle risorse ancora libere
- timeout: un thread che accede ad un blocco synchronized non può uscirne finché il blocco non viene liberato. Questo vuol dire che un altro thread deve attendere un tempo non specificato prima di accedere al blocco occupato
- interruptibility: si ha quando vogliamo interrompere l’esecuzione di un blocco sincronizzato
L’utilizzo dei Lock permette di superare tali limitazioni!
LOCK
L’interfaccia Lock è stata pensata per mettere a disposizione degli sviluppatori uno strumento più potente della classica sincronizzazione (mediante synchronized). Questa interfaccia si trova nel package java.util.concurrent.locks. L’interfaccia Lock fornisce tutte le funzionalità della keyword synchronized oltre a nuovi strumenti per effettuare il locking, impostare il timeout per gestire il locking etc.
Alcuni metodi importanti definiti dall’interfaccia sono:
- lock(), utilizzato per effettuare il lock di una risorsa
- unlock(), utilizzato per liberare una risorsa
- tryLock(), utilizzato per attendere un certo periodo di tempo prima di effettuare il lock
LA CLASSE REENETRANTLOCK
La classe ReentrantLock implementa l’interfaccia Lock ed è disponibile dalla versione 1.5 di Java. Questa classe si trova nel package java.util.concurrent.locks. Questa classe, oltre ad implementare i metodi dell’interfaccia Lock, contiene alcuni metodi di utilità che consentono ai thread di effettuare il lock, attendere un certo periodo di tempo prima di effettuare il lock etc. Quando utilizziamo questa classe, il lock è rientrante. Questo vuol dire che un thread può acquisire un lock che già possiede più volte. Il lock si ottiene utilizzando il metodo lock(). Per rilasciare il lock è necessario utilizzare il metodo unlock().
import java.util.concurrent.locks.ReentrantLock; public class ReentrantLockEsempio { private ReentrantLock istanzaLock = new ReentrantLock(); private int contatore = 0; private int somma = 0; public int conta() { System.out.println("Il thread " + Thread.currentThread().getName() + " ha richiesto di incrementare il contatore"); istanzaLock.lock(); try { System.out.println(Thread.currentThread().getName() + " contatore = " + contatore); contatore++; return contatore; } finally { istanzaLock.unlock(); } } public void somma() { System.out.println("Il thread " + Thread.currentThread().getName() + " ha richiesto di visualizzare la somma dei contatori"); if(istanzaLock.tryLock()) { try { somma += contatore; System.out.println(Thread.currentThread().getName() + " la somma vale = " + somma); } finally { istanzaLock.unlock(); } } else { System.out.println("************************ Il thread che ha il lock sull'oggetto è: " + Thread.currentThread().getName()); } } }
public class Contatore extends Thread { private ReentrantLockEsempio counter; private int limite; private int sleep; public Contatore(ReentrantLockEsempio counter, int limite, int sleep) { super(); this.counter = counter; this.limite = limite; this.sleep = sleep; } @Override public void run() { while (counter.conta() < limite) { try { counter.somma(); Thread.sleep(sleep); } catch (InterruptedException ex) { ex.printStackTrace(); } } } }
public class TestLock { public static void main(String[] args) { ReentrantLockEsempio counter = new ReentrantLockEsempio(); Contatore c1 = new Contatore(counter, 20, 500); Contatore c2 = new Contatore(counter, 20, 500); c1.start(); c2.start(); } }
LINK AI POST PRECEDENTI
LINK AL CODICE SU GITHUB
ESECUZIONE DEL CODICE DI ESEMPIO
- Scaricare il codice da GITHUB, lanciare il file JAR con il seguente comando in Visual Studio Code, posizionandosi nella directory contenente il JAR.
java -jar –enable-preview CorsoJava.jar
- Oppure mettere in esecuzione il main che si trova nel file CorsoJava.java.
Scrivi un commento