3.3) Multithreading

Java supporta il multithreading in modo nativo, a livello di linguaggio. Ciò rende la programmazione di multipli thread molto più semplice che dovendo usare librerie apposite, come è il caso di altri linguaggi.

L'ambiente Java fornisce la classe Thread per gestire i thread di esecuzione. Ogni oggetto istanziato da tale classe (o da una sua derivata) costituisce un flusso separato di esecuzione (ossia un thread di esecuzione).

Si noti in proposito che il metodo main() di un qualunque oggetto attiva un thread, che termina con la terminazione del main() stesso.

3.3.1) Classe Thread

E' la rappresentazione runtime di un thread di esecuzione.

Costruttori

Ce ne sono vari, i più usati sono i tre elencati sotto. Il terzo serve nel caso si faccia uso dell'interfaccia Runnable (che vedremo più avanti).

public Thread();

public Thread(String name);

public Thread(Runnable target);

Metodi più importanti

Ci sono vari metodi statici (che vengono chiamati sul thread corrente), i più utili dei quali sono:

public static void sleep(long millis) throws InterruptedException;

Questo metodo mette a dormire il thread corrente per millis millisecondi. In questo periodo, altri thread possono avanzare nell'esecuzione. Se in questo tempo qualcun altro chiama il suo metodo interrupt(), il thread viene risvegliato da una InterruptedException.

public static void yield() throws InterruptedException;

Questo metodo fa si che il thread corrente ceda la Cpu ad altri thread in attesa. Poiché l'ambiente Java non può garantire la preemption (essa dipende dal sistema operativo) è consigliabile usarlo, quando un thread deve effettuare lunghe computazioni, a intervalli regolari.

I metodi di istanza più utili (che possono essere chiamati su qualunque thread) sono:

public synchronized void start() throws IllegalThreadStateException;

E' il metodo che si deve chiamare per far partire un thread, una volta creato; è un errore chiamarlo su un thread già avviato.

public void run();

E' il metodo che l'ambiente runtime chiama quando un thread viene avviato con il metodo start(). costituisce il corpo eseguibile del thread, e determina il suo comportamento. Quando esso finisce, il thread termina. E' dunque il metodo che ogni classe derivata da Thread deve ridefinire.

public void stop();

Termina il thread.

public void suspend();

Sospende il thread; altri possono eseguire.

public void resume();

Rende il thread nuovamente eseguibile, cioè ready (Nota: questo non significa che diventi anche running, in quanto ciò dipende anche da altri fattori).

In più, ci sono altri metodi per gestire la priorità, avere notizie sullo stato del thread, ecc.

Esempio 6

Programma che consiste di due thread, i quali scrivono un messaggio ciascuno sullo standard output con cadenze differenti.

import java.io.*;
import java.lang.*;

public class PingPong extends Thread {
    String word;
    int delay;
//--------------------------------------------------
    public PingPong (String whatToSay, int delayTime) {
        word = whatToSay;
        delay = delayTime;
    }
//--------------------------------------------------
    public void run () {
        try {
            while (true) {
                System.out.println(word);
                sleep(delay);
            }
        } catch (InterruptedException e) {
            return;
        }
    }
//--------------------------------------------------
    public static void main(String args[]) {
        new PingPong("ping", 250).start();  //1/4 sec.
        new PingPong("PONG", 100).start();  //1/10 sec.
    }
}


3.3.2) Interfaccia Runnable

Vi sono alcune situazioni nelle quali il ricorso a una estensione della classe Thread non è adatto agli scopi, poiché:

La risposta a questo problema è data dalla interfaccia Runnable, che include un solo metodo:

public abstract void run();

dall'ovvio significato: in esso si specifica il comportamento del thread da creare, come visto prima.

Basta quindi definire una classe che implementa tale interfaccia:

public MyClass implements Runnable {

...

}

ed includervi un metodo run():

public abstract void run(){

...

}

per avere la possibilità di lanciare thread multipli all'interno di un oggetto di tale classe.

Tali thread vengono creati col terzo dei costruttori visti per la classe Thread e successivamente vengono attivati invocandone il metodo start(), che a sua volta causa l'avvio del metodo run() dell'oggetto che è passato come parametro nel costruttore:

...

new Thread(theRunnableObject).start();

...

Esempio 7

Funzionalità simile all'esempio precedente ma ottenuta con l'interfaccia Runnable.

import java.io.*;
import java.lang.*;

public class RPingPong /*extends AnotherClass*/ implements Runnable {
    String word;
    int delay;
//--------------------------------------------------
    public RPingPong (String whatToSay, int delayTime) {
        word = whatToSay;
        delay = delayTime;
    }
//--------------------------------------------------
    public void run () {
        try {
            while (true) {
                System.out.println(word);
                Thread.sleep(delay);
            }
        } catch (InterruptedException e) {
            return;
        }
    }
//--------------------------------------------------
    public static void main(String args[]) {
        Runnable ping = new RPingPong("ping", 100); //1/10 sec.
        Runnable PONG = new RPingPong("PONG", 100); //1/10 sec.
        new Thread(ping).start();
        new Thread(ping).start();
        new Thread(PONG).start();
        new Thread(PONG).start();
        new Thread(PONG).start();
        new Thread(PONG).start();
        new Thread(PONG).start();
    }
}


Esempio 8

Applicazione che apre una connessione di rete, come nell'esempio 4. La differenza è che un thread separato rimane in ascolto delle risposte del server: questo permette di ricevere risposte costituite da più linee di testo senza alcun problema.

Il codice è costituito da due classi. La prima, BaseAppE8, è basata su quella dell'esempio 4 e provvede ad istanziare la seconda che ha il compito di attivare la connessione e di gestire la comunicazione. Se ne mostrano qui solo i frammenti di codice rilevanti, sottolineando il fatto che non c'è più bisogno del bottone "Receive", visto che un thread separato rimane in costante ascolto delle risposte.

import java.awt.*;

public class BaseAppE8 extends Frame    {
...ecc.
//--------------------------------------------------
    void button1_Clicked(Event event) {
        baseTConn = new BaseTConn(textField1.getText(),
                                textField2.getText(),
                                textArea1, textArea2);
    }
//--------------------------------------------------
    void button2_Clicked(Event event) {
        baseTConn.send();
    }
//--------------------------------------------------
    void button3_Clicked(Event event) {
        baseTConn.close();
    }
//--------------------------------------------------


La seconda classe si occupa della comunicazione, ed inolte attiva tramite il metodo run() un thread separato costituito da un ciclo infinito in cui si ricevono le risposte del server e si provvede a mostrarle sullo schermo.

import java.awt.*;
import java.lang.*;
import java.io.*;
import java.net.*;

public class BaseTConn implements Runnable {
    TextArea commandArea, responseArea;
    Socket socket = null;
    PrintStream os = null;
    DataInputStream is = null;
//--------------------------------------------------
    public BaseTConn(String host, String port,
                     TextArea commandArea, TextArea responseArea) {
        this.commandArea = commandArea;
        this.responseArea = responseArea;
        try {
            socket = new Socket(host, Integer.parseInt(port));
            os = new PrintStream(socket.getOutputStream());
            is = new DataInputStream(socket.getInputStream());
            responseArea.appendText("***Connection established" + "\n");
            new Thread(this).start();
        } catch (Exception e)   {
            responseArea.appendText("Exception" + "\n");
        }
    }
//--------------------------------------------------
    public void run() {
        String inputLine;
        try {
            while ((inputLine = is.readLine()) != null) {
                responseArea.appendText(inputLine + "\n");
            }
        } catch (IOException e) {
            responseArea.appendText("IO Exception" + "\n");
        }
    }   
//--------------------------------------------------
    public void send() {
            os.println(commandArea.getText());
    }   
//--------------------------------------------------
    public void close() {
        try {
            is.close();
            os.close();
            socket.close();
            responseArea.appendText("***Connection closed" + "\n");
        } catch (IOException e) {
            responseArea.appendText("IO Exception" + "\n");
        }
    }   
}



3.4 Sincronizzazione

Come in tutti i regimi di concorrenza, anche in Java possono sorgere problemi di consistenza dei dati condivisi se i thread sono cooperanti.

I dati condivisi fra thread differenti possono essere:

Per risolvere i problemi legati alla concorrenza, in Java è stata incorporata nella classe Object (e quindi in tutte le altre, che derivano da essa) e nelle sue istanze la capacità potenziale di funzionare come un monitor.

3.4.1) Metodi sincronizzati

In particolare, tale funzionalità si attiva ricorrendo ai metodi sincronizzati, definiti come:

public void synchronized aMethod(...) {

...

}

Quando un thread chiama un metodo sincronizzato di un oggetto, acquisisce un lucchetto su quell'oggetto. Nessun altro thread può chiamare un qualunque metodo sincronizzato dello stesso oggetto finché il lucchetto non viene rilasciato. Se lo fa, verrà messo in attesa che ciò avvenga.

Si noti che eventuali metodi non sincronizzati di quell'oggetto possono comunque essere eseguiti da qualunque thread, in concorrenza col thread che possiede il lucchetto.

Dunque:

Ad esempio, consideriamo una classe che rappresenta un acconto bancario, sul quale possono potenzialmente essere fatte molte operazioni contemporaneamente:

public class Account    {
    private double balance;
//--------------------------------------------------
    public Account(double initialDeposit) {
        balance = initialDeposit;
    }
//--------------------------------------------------
    public synchronized double getBalance() {
        return balance;
    }
//--------------------------------------------------
    public synchronized void deposit(double amount) {
        balance += amount;
    }
}


Non può succedere che un thread legga il valore del conto mentre un altro thread lo aggiorna, o che due thread lo aggiornino in concorrenza.

Si noti che il costruttore non ha bisogno di essere sincronizzato, perché è eseguito solo per creare un oggetto, il che avviene una sola volta per ogni nuovo oggetto.

Anche i metodi statici possono essere sincronizzati. In questo caso il lucchetto è relativo alla classe e non alle sue istanze. In altre parole, sia una classe che le sue istanze possono funzionare come monitor. Va notato come tali monitor siano indipendenti, cioè il lucchetto sulla classe non ha alcun effetto sui metodi sincronizzati delle sue istanze e viceversa.

3.4.2) Istruzioni sincronizzate

E' possibile eseguire del codice sincronizzato, che quindi attiva il lucchetto di un oggetto, anche senza invocare un metodo sincronizzato di tale oggetto. Ciò si ottiene con le istruzioni sincronizzate, che hanno questa forma:

...

synchronized (anObject) {

... // istruzioni sincronizzate

}

...

Questo modo di ottenere la sincrinizzazione fra thread cooperanti richiede maggior attenzione da parte del programmatore, che deve inserire blocchi di istruzioni sincronizzate in tutte le parti di codice interessate.


Esempio 9

Applicazione che realizza un server multithreaded, il quale:

Costituisce, dunque, un server per una semplice chatline. Può essere usato con una semplice variazione del client visto nell'esempio 8: il client, appena si connette, deve inviare una linea di testo che il server usa come identificativo dell'utente connesso.

In tal modo si realizza di fatto una minimale architettura client-server.

Il codice è costituito da due classi. La prima, ChatServer, accetta richieste di connessione sul port 5000 e, ogni volta che ne arriva una, istanzia un oggetto della classe ChatHandler che si occupa di gestirla.

import java.net.*;
import java.io.*;
import java.util.*;

    public class ChatServer {
//--------------------------------------------------
    public ChatServer() throws IOException {
        ServerSocket server = new ServerSocket(5000);
        System.out.println ("Accepting connections...");
        while(true) {
            Socket client = server.accept();
            System.out.println ("Accepted from " + client.getInetAddress());
            new ChatHandler(client).start();
        }
    }
//--------------------------------------------------
    public static void main(String args[]) throws IOException {
        new ChatServer();
    }
}


La seconda si occupa della gestione di una singola connessione e dell'invio a tutte le altre, in broadcast, dei dati provenienti da tale connessione.

import java.net.*;
import java.io.*;
import java.util.*;

public class ChatHandler extends Thread {
  protected static Vector handlers = new Vector();
  protected String userName;
  protected Socket socket;
  protected DataInputStream is;
  protected PrintStream os;
//--------------------------------------------------
  public ChatHandler(Socket socket) throws IOException {
    this.socket = socket;
    is = new DataInputStream(new BufferedInputStream(socket.getInputStream()));
    os = new PrintStream(new BufferedOutputStream(socket.getOutputStream()));
  }
//--------------------------------------------------
  public void run() {
    try {
      userName = is.readLine();
      os.println("Salve " + userName + ", benvenuto nella chatline di Reti II");
      os.flush();
      broadcast(this, "Salve a tutti! Ci sono anch'io.");
      handlers.addElement(this); //e' un metodo sincronizzato
      while (true) {
        String msg = is.readLine();
        if (msg != null) broadcast(this, msg);
        else break;
      }
    } catch(IOException ex) {
      ex.printStackTrace();
    } finally {
      handlers.removeElement(this); //e' un metodo sincronizzato
      broadcast(this, "Arrivederci a presto.");
      try {
        socket.close();
      } catch(Exception ex) {
        ex.printStackTrace();
      }
    }
  }
//--------------------------------------------------
  protected static void broadcast(ChatHandler sender, String message) {
    synchronized (handlers) { //ora nessuno puo' aggiungersi o abbandonare 
      Enumeration e = handlers.elements();
      while (e.hasMoreElements()) {
        ChatHandler c = (ChatHandler) e.nextElement();
        try {
          c.os.print("Da " + sender.userName);
          c.os.print("@" + sender.socket.getInetAddress().getHostName() + ": ");
          c.os.println(message);
          c.os.flush();
        } catch(Exception ex) {
          c.stop();
        }
      }
    }
  }
}


3.4.3) Wait() e Notify()

In Java esiste anche un meccanismo per sospendere e risvegliare i thread che si trovano all'interno di un monitor, analogo a quello offerto dalle variabili di condizione.

Esistono tre metodi della classe Object che servono a questo:

public final void wait() throws InterruptedException;

Un thread che chiama questo metodo di un oggetto viene sospeso finché un altro thread non chiama notify() o notifyAll() su quello stesso oggetto. Il lucchetto su quell'oggetto viene temporaneamente e atomicamente rilasciato, così altri thread possono entrare.

public final void notify() throws IllegalMonitorStateException;

Questo metodo, chiamato su un oggetto, risveglia un thread (quello in attesa da più tempo) fra quelli sospesi dentro quell'oggetto. In particolare, tale thread riprenderà l'esecuzione solo quando il thread che ha chiamato notify() rilascia il lucchetto sull'oggetto.

public final void notifyAll() throws IllegalMonitorStateException;

Questo metodo, chiamato su un oggetto, risveglia tutti i thread sospesi su quell'oggetto. Naturalmente, quando il chiamante rilascerà il lucchetto, solo uno dei risvegliati riuscirà a conquistarlo e gli altri verranno nuovamente sospesi.

E' importante ricordare che questi metodi possono essere usati solo all'interno di codice sincronizzato sullo stesso oggetto per il quale si invocano, e cioè:


Esempio 10

Esempio di produttore-consumatore che fa uso di wait() e notify(). La classe Consumer rappresenta un consumatore, che cerca di consumare un oggetto (se c'è) da un Vector.

Usa un thread separato per questo, e sfrutta wait() per rimanere in attesa di un oggetto da consumare.

Il produttore è costituito dall'utente, che attraverso il thread principale (quello del main()) aggiunge oggetti e invia notify() per risvegliare il consumatore.

import java.util.*;
import java.io.*;

public class Consumer extends Thread {
    protected Vector objects;
//--------------------------------------------------
    public Consumer () {
        objects = new Vector();     
    }
//--------------------------------------------------
    public void run () {
        while (true) {
            Object object = extract ();
            System.out.println (object);
        }
    }
//--------------------------------------------------
    protected Object extract () {
        synchronized (objects) {
            while (objects.isEmpty ()) {
                try {
                    objects.wait ();
                } catch (InterruptedException ex) {
                    ex.printStackTrace ();
                }
            }
            Object o = objects.firstElement ();
            objects.removeElement (o);
            return o;
        }
    }
//--------------------------------------------------
    public void insert (Object o) {
        synchronized (objects) {
            objects.addElement (o);
            objects.notify ();
        }
    }
//--------------------------------------------------
    public static void main (String args[]) throws IOException,
                                                   InterruptedException {
        Consumer c = new Consumer ();
        c.start ();
        DataInputStream i = new DataInputStream (System.in);
        String s;
        while ((s = i.readLine ()) != null) {
            c.insert (s);
            Thread.sleep (1000);
        }
    }
}


Torna al sommario | Vai avanti