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 è:

  1. Definire una classe che eredita dalla classe Thread e che contiene il metodo run()
    class ListSorter extends Thread{
    	...
    	public void run() { //Inserire qui il codice che esegue il thread }
    }
  1. Creare un’istanza della classe
    ListSorter concSorter = new ListSorter(list1);
  1. Invocare il metodo start(), che a sua volta chiamerà il metodo run()
    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.

  1. Definire una classe che implementa l’interfaccia Runnable e implementare il metodo run().
class ListSorter implements Runnable{
	...
	public void run(){ //Inserire qui il codice che esegue il thread }
}
  1. Creare un’istanza della classe
ListSorter concSorter = new ListSorter(list1);
  1. Creare il thread
Thread myThread = new Thread(concSorter);
  1. 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:

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

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:

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