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! Nei codici moderni si preferiscono CopyOnWriteArrayList o la ConcurrentHashMap.

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 8 Agosto 2025