Thread Java con codice e buffer condiviso

Continuiamo a vedere come utilizzare i thread in Java. Se non hai letto la prima lezione, puoi trovarla a questo link. Questa volta ci addentriamo in un argomento più delicato. Come possiamo far interagire due o più thread sullo stesso spazio di memoria condiviso in modo concorrente?

Vediamo due modalità: utilizzo dei vector e utilizzo degli arraylist. Utilizziamo queste strutture dati e ci poniamo di realizzare una sorta di gioco del tris, anche se non è esattamente a turni come nel gioco dei bimbi. Vogliamo costruire un vettore di 9 caselle che simuli la famosa tabella di gioco ed ognuno tra due thread che piazza in un tempo casuale un simbolo X o O.

La struttura dati Vector

Il vector, qualche lettore potrebbe già stranirsi, è una struttura dati molto versatile del Java, peccato si obsoleta e dalla versione JDK 8, viene mantenuto un porting di retrocompatibilità e ne viene sconsigliato l’utilizzo in nuovi progetti di produzione. In realtà il vector è una struttura dati molto didattica e si presta molto bene a semplificare le trattazione della concorrenza tra thread java. Infatti allo studente non occorre sincronizzare letture o scritture su uno spazio di memoria comune in quanto la progettazione della struttura dati è già thread-safe e non sono necessarie diciture o metodi di sincronizzazione.

Essendo didattica, molti docenti di scuola superiore o universitari rimangono ancorati a questa trattazione, che svisceriamo quindi anche noi. Però mi sento in dovere di sconsigliare categoricamente l’uso di vector per applicazioni commerciali vere!

La classe thread Giocatore

Per il nostro thread, prevediamo un attributo per gestire il simbolo X o O ed un buffer comune che andiamo a dichiarare di tipo vector ma statico. Questo è fondamentale. Se il buffer non fosse statico, ogni thread avrebbe una sua versione/copia mentre così ce n’è uno per tutti. Il restante codice no ha nessuna particolarità. Nel costruttore facciamo inizializzare una volta per tutti i thread in gioco, il campo di gioco, la matrice 3×3 che qui simuliamo con il vettore. A questo punto, il thread non deve fa altro che cercare finché ci sono spazi vuoti o, meglio, col simbolo di default -, pescare un numero a caso da 0 a 9, provare a piazzare il proprio simbolo, e attendere in ogni caso, piazzato o meno, un tempo casuale, qui ad esempio impostato entro i 2000 millisecondi.

Abbiamo aggiunto un metodo toString() per stampare la matrice una volta che è tutta piena e i due thread sono rientrati a quello principale.


package iisteramo.tris.thread;

import java.util.Random;
import java.util.Vector;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 *
 * @author alfredo cetinaro
 */
public class Giocatore extends Thread
{
    private char simbolo;
    public static Vector<Character> vettore;
    
    public Giocatore(char _simbolo)
    {
        this.simbolo = _simbolo;
        if (this.vettore == null)
        {
            this.vettore = new Vector<>();
            for (int i=0; i < 9; i ++)
            {
                vettore.add('-');
            }              
        }    

    }
    
    @Override
    public void run()
    {
        int posizione;
        Random rand = new Random();
        while(this.vettore.contains('-'))
        {
            posizione = rand.nextInt(9) ;
            if (this.vettore.get(posizione) == '-')
            {
                this.vettore.set(posizione, simbolo);
                System.out.println("posizione:" + posizione + " simbolo: " + simbolo);
            }   

            try {
                Thread.sleep(rand.nextInt(2000));
            } catch (InterruptedException ex) {
                Logger.getLogger(Giocatore.class.getName()).log(Level.SEVERE, null, ex);
            }
        }

    }
    
    @Override
    public String toString()
    {
        String tris = this.vettore.get(0) + " " + this.vettore.get(1) + " " + this.vettore.get(2) + "\n" 
                      + this.vettore.get(3) + " " + this.vettore.get(4) + " " + this.vettore.get(5) + "\n" 
                      + this.vettore.get(6) + " " + this.vettore.get(7) + " " + this.vettore.get(8);
        return tris;
    
    }        
       
}

La classe di test/prova

Il metodo di test/main in realtà è molto semplice. Inizializzo i due thread con simboli differenti, li lancio e attendo il loro ricongiungimento al programma principale, stampo la matrice riempita. Mi attendo che non ci siano tanti X e O a metà. E’ probabile che uno dei thread dorma di meno e piazzi più simboli.

package iisteramo.tris.thread;

import java.util.Vector;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 *
 * @author alfredo centinaro
 */
public class TrisThread {

    public static void main(String[] args)
    {
        
        Giocatore g1 = new Giocatore('X');
        Giocatore g2 = new Giocatore('O');
        g1.start();
        g2.start();
        
        try {
            g1.join();
        } catch (InterruptedException ex) {
            Logger.getLogger(TrisThread.class.getName()).log(Level.SEVERE, null, ex);
        }
        try {
            g2.join();
        } catch (InterruptedException ex) {
            Logger.getLogger(TrisThread.class.getName()).log(Level.SEVERE, null, ex);
        }
        
        System.out.print(g2.toString());
    }
}

Ultima modifica 9 Novembre 2023