Threads

Start

In Java non c'è un solo filo d'esecuzione, si possono avere contemporaneamente diversi fili d'esecuzione (Threads). I threads sono oggetti e contengono un oggetto di tipo Runnable. Tale tipo è definito dall'interface:
public interface Runnable {
   void run();
}
Per attivare un thread si chiama il metodo start sul thread che a sua volta chiama il metodo run sul suo oggetto. Esempio:
class Count implements Runnable {
  String name;
  int min, max, incr;
  Count (String name, int min, int max, int incr) {
    this.name=name;
    this.min=min;
    this.max=max;
    this.incr=incr;
  }
  public void run() {
    for (int i=min; i < max; i+=incr) {
      System.out.println(name+" "+i);
    }
  }
}

class Test {
  public static void main(String[] args) {
    Thread th1 = new Thread(new Count("even",2,100,2));
    Thread th2 = new Thread(new Count("odd",1,100,2));
    th1.start();
    th2.start();
  }
}
Eseguendo Test, il programma crea due thread th1, th2, li attiva e termina. Ma poiché ci sono due thread attivi, l'esecuzione prosegue e finirà quando tutti i thread saranno finiti. I thread th1 e th2 sono eseguiti in parallelo. Questo vuole dire che sappiamo che th1 produrrà
even 2
even 4
....
e che th2 produrrà
odd 1
odd 3
....
ma non sappiamo in quale ordine ciò accadrà:
even 2
even 4
....
even 98
odd 1
ode 3
...
o
even 2
odd1
even 4
odd 3
...
o
odd 1 
odd 3
even 2
even 4
odd 5
...
I thread infatti implementano Runnable. L'implementazione di default del metodo start è dunque quella di chiamare il metodo run sul suo oggetto. Si può ridefinire il run, cosí si può evitare di creare un oggetto. Un'alternativa all'esempio precedente è:
class Count extends Thread {
  String name;
  int min, max, incr;
  Count (String name, int min, int max, int incr) {
    this.name=name;
    this.min=min;
    this.max=max;
    this.incr=incr;
  }
  public void run() {
    for (int i=min; i < max; i+=incr) {
      System.out.println(name+" "+i);
    }
  }
}

class Test {
  public static void main(String[] args) {
    Thread th1 = new Count("even",2,100,2);
    Thread th2 = new Count("odd",1,100,2);
    th1.start();
    th2.start();
  }
}

Sleep e Interrupt

Si può mettere un thread in uno stato d'attesa con il metodo statico sleep che prende il numero di millisecondi da aspettare. Un oggetto può ottenere il suo thread d'esecuzione usando il metodo Thread.currentThread().

Se un thread è in stato d'attesa, lo si può svegliare chiamando il metodo interrupt. Tale metodo genera un'eccezione InterruptedException. Per questo è necessario ogni volta che si fa uno sleep prevedere l'eccezione InterruptedException.

Usando uno sleep, l'esempio precedente diventa:

class Count implements Runnable {
  ...
  public void run() {
    for (int i=min; i < max; i+=incr) {
      System.out.println(name+" "+i);
      try {
        Thread.currentThread().sleep(10);
      }
      catch (InterruptedException e) {}
    }
  }
}
Tale modifica rallenta l'esecuzione del programma Test, ma può anche modificare la combinazione dei due thread.

Un'alternativa equivalente a Thread.currentThread().sleep è usare il metodo statico Thread.sleep.

Join

Un thread finisce quando il metodo run del suo oggetto finisce. Questo metodo può terminare normalmente oppure terminare con una eccezione non catturata.

Si può aspettare fino alla fine di un thread utilizzando il metodo join. Questo metodo aspetta la fine del thread per proseguire. Per esempio se modifichiamo il nostro esempio:

class Test {
  public static void main(String[] args) {
    Thread th1 = new Thread(new Count("even",2,100,2));
    Thread th2 = new Thread(new Count("odd",1,100,2));
    th1.start();
    th1.join();
    th2.start();
  }
}
Adesso siamo sicuri che i numeri pari sono stampati prima dei dispari.

Synchronized

Un oggetto può essere manipolato contemporaneamente in thread diversi ma forse non ha senso eseguire lo stesso metodo nello stesso momento. Per impedire tale situazione c'è il modificatore synchronized che permette di dire che un metodo può essere chiamato contemporaneamente:
class Speaker {
synchronized void say(String s) {
  ...
}
La sincronizzazione è fatta al livello dell'oggetto. Dunque se un oggetto ha più di un metodo con synchronized, siamo sicuri che in ogni istante viene eseguito al massimo un metodo synchronized di questo oggetto:
class Point {
  int x, y;
  synchronized void set(int x, int y) {
    this.x = x;
    this.y = y;
  }
  synchronized void print() {
    System.out.println("("+x+","+y+")");
  }
}
L'implementazione si fa con un lock sull'oggetto. In Java si può usare direttamente questo lock usando il commando:

synchronized (<EXPRESSION >) {
   ...
}

dove l'espressione si valuta in un oggetto. Dunque

synchronized void method() {
    ..
}
è equivalente a
void method() {
  synchronized (this) {
    ...
  }
}

Wait e Notify

In un metodo synchronized forse non siamo in uno stato dove si può fare qualcosa. Bisogna aspettare che qualche altro thread faccia qualcosa per continuare. Il metodo wait di un oggetto permette di fare una tale pausa. Il risveglio del thread deve essere esplicito. Una volta che l'altro thread ha cambiato qualcosa, utilizza il metodo notify per risvegliare un thread che era in uno stato wait.

Per esempio, definiamo un produttore che genera messaggi:

class Producer implements Runnable {
  int i;
  String name;
  String [] messages;
  Producer (String name, int i) {
    this.name = name;
    this.i = 0;
    messages = new String[i];
  }
  public void run() {
    try {
      while (true) {
        produceMessage();
        Thread.sleep(1000);
      }
    }
    catch(InterruptedException e) {}
  }
  private synchronized void produceMessage() throws InterruptedException {
    while (i >= messages.length) {
      wait();
    }
    messages[i]=new java.util.Date().toString();
    System.out.println("Produce "+messages[i]);
    i++;
    notify();
  }
  public synchronized String getMessage() throws InterruptedException {
    notify();
    while (i == 0) {
      wait();
    }
    String res=messages[i-1];
    i--;
    return res;
  }
}
Adesso il consumatore può avere messaggi del produttore usando il metodo getMessage:
class Consumer implements Runnable {
  String name;
  Producer pr;
  Consumer (String name, Producer pr) {
    this.name = name;
    this.pr = pr;
  }
  public void run() {
    try {
      while (true) {
        System.out.println(name+" got "+pr.getMessage());
        Thread.sleep(2000);
      }
    }
    catch(InterruptedException e) {}
  }
}
Si può mettere un produttore con un consumatore:
class Test {
  static public void main(String [] args) {
   Producer pr = new Producer("A",20);
   Thread th1 = new Thread(pr);
   Thread th2 = new Thread(new Consumer("B",pr));
   th1.start();
   th2.start();
  }
}
Con il programma precedente si può anche avere più di un consumatore:
class Test {
  static public void main(String [] args) {
   Producer pr = new Producer("A",20);
   Thread th1 = new Thread(pr);
   Thread th2 = new Thread(new Consumer("B",pr));
   Thread th3 = new Thread(new Consumer("C",pr));
   th1.start();
   th2.start();
   th3.start();
  }
}
In questo caso, c'è una competizione fra B e C per consumare. Questo significa che più di un oggetto può essere in uno stato wait. Con notify se ne risveglia solo uno. Il metodo notifyAll() permette di svegliare tutti. Una versione più completa dell'esempio precedente è la seguente:
class Producer implements Runnable {
  ...
  private synchronized void produceMessage() throws InterruptedException {
    while (i >= messages.length) {
      wait();
    }
    messages[i]=new java.util.Date().toString();
    System.out.println("Produce "+messages[i]);
    i++;
    notifyAll();
  }
  public synchronized String getMessage() throws InterruptedException 
    notify();
    while (i == 0) {
      wait();
    }
    String res=messages[i-1];
    i--;
    return res;
  }
}

Laurent Théry
Last modified: Mon Feb 9 11:24:05 MET 2004