Non ci addentriamo nella teoria dei thread e su cosa siano in questo articolo. Lasciamo il lettore ai manuali scolastici. Ci limitiamo a notare come i thread in java abbiano una anomalia dovuta alla JVM rispetto ai thread nativi del C. Non per questo le capacità multi-threading di Java demeritano, anzi. Ha una libreria consolidata e performante che lo fa preferire, ad esempio, a Python.
In Java, la classe ad hoc che implementa i thread è la java.lang.Thread. E’ importante però comprendere due paradigmi fondamentali di problemi/esercizi:
- Due o più thread possono condividere, indipendentemente dai dati, il codice sorgente che essi eseguono. Tali thread eseguono il loro codice da una stessa classe. I singoli thead possono tornare o meno valori al programma principale.
- Due o più thread possono condividere, indipendentemente dal codice, i dati su cui eseguono delle operazioni. Questo avviene quando tali thread condividono l’accesso ad un oggetto comune.
Indice dei contenuti
Come creare dei thread semplici
Ci sono, fondamentalmente, due metodi per creare un thread in Java. Quello che preferisco consigliare, soprattutto quello basato sulla struttura Object Oriented, con una estensione di una classe.
Il secondo, che non affrontiamo in questo articolo, è basato sul concetto di Runnable, ovvero una proprietà “da aggiungere” ad una classe/metodo esistente per poterlo eseguire indipendentemente dal flusso principale. Vediamo il codice del primo tipo con un esempio semplice. Vogliamo creare due thread a cui vengono passati due parametri numerici intesi come estremi di un conteggio da mostrare a video.
package thread;
/**
*
* @author alfredo centinaro
*/
public class ContaThread extends Thread
{
int inizio;
int fine;
public ContaThread(int inizio, int fine)
{
super();
this.setName("Thread - Conta da " + inizio + " a " + fine);
this.inizio = inizio;
this.fine = fine;
}
@Override
public void run()
{
System.out.println(this.getName());
for (int i=inizio; i < fine; i++)
{
System.out.println(i);
}
try
{
ContaThread.sleep(500);
}
catch(InterruptedException e)
{
}
}
}
Il cuore del nostro thread è ovviamente il metodo run in cui viene svolta la logica. Andiamo ad utilizzare in un main la classeThread appena creata. Come possiamo vedere, al momento usiamo lo stesso codice per due esecuzioni indipendenti. Non ci sono buffer o dati gestiti in output dai thread.
package thread;
/**
*
* @author alfredo centinaro
*/
public class GestioneThread {
/**
* @param args the command line arguments
*/
public static void main(String[] args)
{
ContaThread t1 = new ContaThread(1,100);
ContaThread t2 = new ContaThread(101,200);
t1.start();
t2.start();
}
}
Far tornare un buffer da un thread
Uno dei primi aspetti che non contempla l’esercizio precedente è la comunicazione del thread col programma principale. Non c’è di fatto alcuna comunicazione tra i thread lanciati che eseguono il loro codice, e il programma main che li ha lanciati. Mettiamo caso abbiamo due thread che eseguono due operazioni algebriche incorrelate tra loro, anche semplici, e vogliamo utilizzare i risultati di tali operazioni per effettuare un terza operazione algebrica.
Es. vogliamo calcolare
S= (100 * 24) + (45 + 3)
100 * 24 sarà gestito da un thread, 45 + 3 da un altro thread, il main sfrutterà i risultati per fare l’operazione di somma centrale.
Costruiamoci allora una classe buffer: servirà per far tornare il risultato e permettere la comunicazione tra thread e programma principale. E’ in realtà davvero poco codice: un attributo col tipo di ritorno delle operazioni, intero qui, e i suoi metodi di accesso. Possiamo personalizzare questa classe inserendo attributi diversi in numero e tipo a seconda delle necessità, renderla persino generica per essere riutilizzata o inserirci tipi complessi come vettori o altri oggetti.
Classe Buffer
package thread;
/**
*
* @author alfredo centinaro
*/
public class Buffer
{
private int intBuffer;
public Buffer() {
this.intBuffer = 0;
}
public Buffer(int _b) {
this.intBuffer = _b;
}
public int getIntBuffer() {
return intBuffer;
}
public void setIntBuffer(int intBuffer) {
this.intBuffer = intBuffer;
}
}
Le classi thread somma e prodotto
A questo punto dobbiamo costruirci le due classi thread per eseguire le operazioni algebriche. L’esempio è molto semplice, sembra qusi di sprecare più codice di quello che serva realmente, ma l’esempio è volutamente didattico.
Come nell’esempio precedente, le due classi ereditano da Thread e implementano il metodo run(). Qui parametrizziamo l’operazione, passando due parametri al thread che sono i termini numerici contenuti nelle parentesi. La novità è un terzo attributo di tipo Buffer: i risultati dell’operazione verranno passati al programma main attraverso questo attributo
package thread;
/**
*
* @author alfredo centinaro
*/
public class Prodotto extends Thread
{
private int numero1;
private int numero2;
private Buffer buffer;
public Prodotto(int _numero1, int _numero2, Buffer _b)
{
this.numero1 = _numero1;
this.numero2 = _numero2;
this.buffer = _b;
}
@Override
public void run()
{
int risultato = this.numero1 * this.numero2;
buffer.setIntBuffer(risultato);
}
}
package thread;
/**
*
* @author alfione centinaro
*/
public class Somma extends Thread
{
private int numero1;
private int numero2;
private Buffer buffer;
public Somma(int _numero1, int _numero2, Buffer _b)
{
this.numero1 = _numero1;
this.numero2 = _numero2;
this.buffer = _b;
}
@Override
public void run()
{
int risultato = this.numero1 + this.numero2;
buffer.setIntBuffer(risultato);
}
}
A questo punto non ci resta che creare un main per testare tutto. La differenza con l’esercizio introduttivo precedente è che ci creiamo due istanze di buffer che poi diamo in pasto alle due istanze di thread. Avremo thread che non condividono codice ma hanno dei buffer singoli individuali non condivisi.
Lanciati i thread con start, tocca prelevare i loro risultati. Qui la piccola anomalia. I thread hanno una esecuzione non deterministica in senso di tempistiche, ordine e durata. Ovvero il sistema operativo potrebbe eseguire prima il thread somma e poi quello prodotto, o viceversa. Il problema per noi è che abbiamo bisogno ad un certo punto dei risultati delle operazioni per effettuare la somma finale. Qui entra in gioco il metodo join() che ci permette di forzare l’attesa della fine esecuzione dei due thread. Senza join, il lettore può provare, la somma potrebbe avere risultati inattesi: 0, 2400, o 48, ovvero la somma viene eseguita senza che nessuno dei due thread sia stato eseguito o viene eseguito solo il prodotto piuttosto che la somma. Il metodo join, o segnalerà anche l’IDE. richiede il costrutto try/catch.
Classe main con join
package thread;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
*
* @author alfredo centinaro
*/
public class GestioneThread {
/**
* @param args the command line arguments
*/
public static void main(String[] args)
{
Buffer bufSomma = new Buffer();
Buffer bufProdotto = new Buffer();
Thread op1 = new Prodotto(100,24,bufProdotto);
Thread op2 = new Somma(45,3,bufSomma);
op1.start();
op2.start();
try {
op1.join();
} catch (InterruptedException ex) {
Logger.getLogger(GestioneThread.class.getName()).log(Level.SEVERE, null, ex);
}
try {
op2.join();
} catch (InterruptedException ex) {
Logger.getLogger(GestioneThread.class.getName()).log(Level.SEVERE, null, ex);
}
int S = bufSomma.getIntBuffer() + bufProdotto.getIntBuffer();
System.out.println("S= (100 * 24) + (45 + 3)=> ATTESO 2448 ");
System.out.println("S= " + S);
}
}
Ultima modifica 19 Maggio 2022