Thread in java
Le classi Timer e TimerTask. .
il timer è un componente hardware del sistema di elaborazione.
Contiene alcuni registri utilizzati come contatori a decremento, che vengono inizializzati da programma, e possiedono un proprio ingresso di clock, la cui frequenza determina la velocità di decremento del conteggio.

Alcuni contatori sono riservati a operazioni di sistema, altri sono disponibili per i programmatori.
Un programma può accedere al contatore del timer per leggere il conteggio, e quindi misurare un intervallo di tempo.
Comunque, quando il contatore raggiunge il valore 0 il componente avverte la CPU attivando un opportuno segnale di time-out.

La classe seguente, derivata dal gestore del timer, ridefinisce il metodo run ereditato dalla classe TimerTask.

il metodo run, che verrà eseguito quando si verifica il time-out, specifica le operazioni che si dovranno svolgere:
import java.util.TimerTask;
  class Scadenza extends TimerTask {
    public void run() {
      System.out.println("Periodo trascorso!");
      this.cancel();
      System.exit(0);
    }
  }
Commenti:
quando si verifica il time out viene richiamato il metodo run.
il processo richiamato mostra un messaggio
Poi si auto elimina
infine termina anche il processo che lo ha generato.

Per essere usata, la classe precedente deve essere ospitata in un processo che, dopo averla schedulata, ne consentirà l’esecuzione nel proprio spazio quando se ne presenterà la necessità.

La classe seguente, Promemoria, definisce due campi membro:
timer - il riferimento (non ancora inizializzato) a un oggetto di libreria util e
Avvertimento - il riferimento a un oggetto di classe Scadenza.
il costruttore della classe Promemoria riceve un parametro che rappresenta il numero con cui avviare il contatore a decremento, poi inizializza i riferimenti agli oggetti e infine schedula un processo, cioè specifica quale processo eseguire quando si verifica il time out.
import java.util.Timer;
public class Promemoria {
  Timer timer;
  Scadenza Avvertimento;
  public Promemoria(int secondi) {
    Avvertimento = new Scadenza();
    timer = new Timer();
    timer.schedule(Avvertimento, secondi*1000);
  }
  public static void main(String args[]) {
    int i=0;
    System.out.println("il processo sta per essere schedulato.");
    new Promemoria(5);
    System.out.println("Processo schedulato. intanto faccio altre operazioni");
    do {
      System.out.print(i+"\r");
      if (i<100) i++; else i=0;
    } while (true);
  }
}
Commenti:
la variabile timer dovrà contenere un riferimento ad un oggetto di classe Timer
la variabile viene inizializzata con il riferimento all’oggetto
il metodo run comunica il messaggio di promemoria, elimina il processo e termina il programma.

in questo semplice programma sono contenute le parti per implementare e schedulare un processo che deve essere eseguito dal gestore del timer.

Un programma resta in esecuzione fintanto che tutti i suoi processi timer sono in esecuzione. Ci sono quattro modi per terminare un thread del timer.
L’esempio Promemoria richiama il metodo cancel all’interno del metodo run del processo del timer.

il metodo schedule esiste in varie versioni che, oltre a specificare il processo da schedulare, prevedono la possibilità di ripetere periodicamente la schedulazione:

Secondo esercizio.


Thread.
Ci sono due metodi per fornire il metodo run a un thread:
  1. Creare una sottoclasse della classe Thread e ridefinire il metodo run.
  2. Fornire una classe che implementa l’interfaccia Runnable e implementare il metodo run.
Per provare l’esecuzione parallela dei processi l’esempio seguente crea un Thread che genera numeri in un assegnato intervallo, poi avvia due istanze della classe e, mediante la stampa, mostra che i risultati di un Thread sono intercalati nei risultati dell’altro Thread.
public class ThreadContatore extends Thread {
  private int inizio;
  private int fine;
  public ThreadContatore(int da, int a) {
    this.inizio=da;
    this.fine=a;
  }
  public void run(){
    System.out.println(this.getName() + " avviato ... ");
    for (int i=inizio; i<=fine; i++) {
      System.out.print(i + " ");
    }
  System.out.println(this.getName() + " Finito.");
  }
}
public class ThreadTest {
  static public void main(String[] args) {
    ThreadContatore Th1 = new ThreadContatore(1,100);
    ThreadContatore Th2 = new ThreadContatore(200,300);
    Th1.start();
    Th2.start();
  }
}
La classe ThreadTest ospita i Thread

Esercizio. I nomi assegnati per default ai Thread sono Thread-0 e Thread-1. Modificare i nomi dei Thread.

Terzo Esercizio


Processi Concorrenti..

Nel seguente esempio si simulano le operazioni in un super mercato: due cassiere vendono contemporaneamente 5 articoli dello stesso prodotto. Ci si aspetta che la quantità di articoli rimasti in deposito sia diminuita di 10 unità.

L'operazione di aggiornamento è volutamente ampliata:

  1. Un processo legge la quantità di scorta, che è una variabile condivisa tra i processi;
  2. Sottrae la quantità venduta ed ottiene la nuova quantità di scorta;
  3. La nuova quantità di scorta viene salvata nella variabile condivisa.

C'è comunque da considerare l'eventualità che l'operazione di aggiornamento della quantità di scorta venga interrotta. Ad esempio, se dopo l'operazione numero 1 arriva un'interruzione Hardware, i due processi vengono interrotti e mantengono nella loro variabile temporanea lo stesso valore di quantità di scorta. Quando si sarà completato il servizio dell'interruzione, entrambi i processi riprendono l'esecuzione, ognuno sottrae una quantità di articoli venduti e poi, ne salva il risultato.

Poichè la variabile è condivisa verrà aggiornata prima da un processo e poi da un altro processo, che quindi sovrascriverà il risultato del primo processo.

Per evitare il problema, le tre operazioni devono essere eseguite in forma indivisibile, cioè se un processo ha iniziato l'operazione di aggiornamento, un altro processo che intende eseguire la stessa operazione deve aspettare il termine dell'altro processo.

Linea 2: la proprietà totale mantiene la quantità di articoli disponibili in magazzino;

Linee 3÷5: il costruttore assegna il valore iniziale alla proprietà della classe;

Linea 6: il metodo togli riceve come parametro il numero di articoli venduti che si devono sottrarre dal numero totale di articoli disponibili;

Linea 7: all'interno del metodo togli si legge, in una variabile temporanea, il numero degli articoli disponibili;

Linea 8: si simula un'interruzione delle operazioni;

Linea 9: si calcola il nuovo totale e lo si memorizza.

La parola synchronized e la linea 8 devono restare racchiuse tra i segni di commento, allo scopo di verificare che l'aggiornamento appare corretto, poi, come secondo esperimento si toglie il commento alla linea 8 per simulare un'interruzione che porta in stato di attesa due processi, iniziati contemporaneamente. Infine, per correggere questo caso, si toglie il commento alla parola synchronized, che ha lo scopo di impedire ad un processo di richiamare il metodo togli se è in esecuzione per un altro processo.

Linea 1: la classe è derivata dalla classe Thread;

Linee 2 e 3: il nome del cassiere e il riferimento al deposito sono proprietà della classe;

Linee 4 e 5: vengono definite due costanti;

Linee 6÷9: il costruttore inizializza i campi membro della classe;

Linee 10÷15: il metodo run verrà richiamato quando il thread verrà avviato.

Linea 12: tra le operazioni di stampa del metodo run viene richiamato il metodo togli dell'oggetto scorta.

Linea 4: la classe contiene il riferimento ad un oggetto di classe Deposito, inizializzato con 10 articoli di scorta.

Linee 5 e 6: la classe contiene anche i riferimenti a due oggetti di classe Cassiere, inizializzati con il nome dell'impiegato e con il riferimento all'oggetto di classe Deposito, che quindi è condiviso.

Linee 7 e 8: i thread associati ai due cassieri vengono avviati, cioè viene richiamato il metodo run.

La classe Simulazione contiene solo il metodo interruptHW che, con probabilità 50%, introduce una pausa di 200 ms nell'esecuzione.


Modalità di verifica del programma:

  1. Dopo aver compilato le 4 classi che compongono il programma, si esegua la classe Magazzino.
  2. Dopo aver osservato che i risultati sono corretti, si tolgano i caratteri di commento intorno all'istruzione di chiamata del metodo interruptHW della classe Simulazione. Si deve notare che l'aggiornamento del numero di articoli di scorta risulta sbagliato.
  3. Togliere il commento alla parola synchronized ed osservare che i risultati sono corretti.

Quarto Esercizio


Il problema del Produttore e del Consumatore.

Comunicazione tra Processi

In Java i thread possono comunicare tra loro in modo da indicare quando un certo evento o una certa condizione si è verificata.

Nella programmazione tradizionale, il controllo che una certa condizione sia vera o falsa viene effettuato con la tecnica del polling: per esempio un programma resta impegnato a controllare ripetutamente il valore di una certa variabile booleana fino a che questa assume il valore true, e solo a quel punto prosegue nel proprio lavoro.

Un tipico esempio è rappresentato dalle applicazioni basate sul modello Produttore Consumatore, dove un programma genera dei dati, e un altro legge e utilizza i dati prodotti dal primo: per esempio un thread riceve pacchetti di dati dalla rete e un altro li visualizza all'interno della finestra di un browser.

Con la tecnica del polling, il consumatore spenderebbe il proprio tempo con un ciclo while in attesa che il produttore metta a disposizione nuovi dati da elaborare. Il produttore, una volta consegnati i dati al consumatore, a sua volta potrebbe essere costretto a rimanere in attesa che il consumatore finisca di elaborare i dati ricevuti. Tutto ciò provoca un inutile spreco di tempo di CPU.

Un metodo più efficiente consiste nel permettere a un thread di segnalare ad un altro quando un certo evento si è verificato o una certa condizione è diventata vera. In questo modo, il thread in attesa può sospendersi, senza sprecare tempo di CPU.

Java realizza un meccanismo di inter-process communication basato su tre metodi: wait, notify e notifyAll.

Questi metodi possono essere chiamati solo dall'interno di metodi sincronizzati, e possono essere invocati su qualunque oggetto. I tre metodi svolgono le seguenti funzioni:

Molti problemi di programmazione di sistema possono essere affrontati facendo riferimento al modello Produttore-Consumatore per esempio, un programma legge i dati di un conto corrente dal disco affinchè un'applicazione bancaria ne calcoli gli interessi, oppure un thread del sistema operativo rende disponibili aree di memoria dell'elaboratore in modo che altri programmi possano utilizzarli per memorizzare le proprie strutture dati.

Esercizio.

Implementare un modello produttore-consumatore nel quale un produttore genera dei valori numerici che il consumatore riceve e utilizza.

Per semplicità, in questo esempio si ammette che l'area di scambio sia una variabile in grado di contenere un solo dato, anzichè un array o una struttura dinamica: il programma si limita a scambiare un dato per volta, memorizzandolo nella variabile locale unDato di un oggetto, istanza della classe chiamata Vassoio. Il produttore deposita unDato sul Vassoio e il consumatore lo preleva. Poi il produttore deposita un nuovo dato, e così via.

Il main del programma crea il vassoio oltre a un'istanza di Produttore e una di Consumatore:

Il Produttore ha un costruttore che riceve come parametro il riferimento al vassoio sul quale i due thread si scambiano i dati e avvia il proprio thread. Il metodo run è un ciclo for che per dieci volte mette un dato (un numero intero) sul vassoio.



Analogamente, il Consumatore riceve dal main come parametro lo stesso Vassoio, e il metodo run per dieci volte preleva dal vassoio quanto depositato dal Produttore.

Definizione della classe Vassoio.



Sul vassoio è depositato un solo dato numerico; i metodi metti e prendi sono synchronized in quanto accedono in modo concorrente allo stesso vassoio e vanno evitate interferenze tra essi.

Il metodo metti ricopia nella variabile locale unDato il valore del parametro passato d.

Il metodo prendi restituisce il valore numerico in quel momento presente sul vassoio.

Si osservi che:
il ciclo for del Consumatore potrebbe trovare per due o più volte lo stesso valore numerico sul vassoio; il ciclo for del Produttore potrebbe mettere sul vassoio un nuovo valore prima che il Consumatore abbia avuto il tempo di leggere il precedente: il Consumatore perderebbe in questo modo dei dati.

Nello sviluppo dell'esempio non è stato introdotto alcun controllo sul fatto che ogni singolo dato depositato dal Produttore venga sempre prelevato dal Consumatore (modello Produttore-Consumatore senza garanzia di ricezione). Si accetta che il Consumatore possa perdere qualche dato, o che possa prelevare due volte lo stesso dato dal vassoio. Questo perchè è praticamente impossibile che vi sia una commutazione del contesto da Produttore a Consumatore esattamente a ogni esecuzione dei rispettivi cicli for.

Questo meccanismo è accettabile in tutti quei casi nei quali non è importante che il Consumatore riceva una e una sola volta ogni singolo dato generato dal Produttore.

Modello Produttore-Consumatore con ricezione garantita Approfondimenti

Esistono casi nei quali è importante che ogni singolo dato generato dal produttore sia sempre ricevuto una e una sola volta dal consumatore. Si immagini per esempio che il produttore sia un programma che segnala i pagamenti delle rate di un mutuo e il secondo sia la banca che riceve tali pagamenti: in questo caso non ci può essere nè duplicazione nè perdita di dati.

I prossimi due esempi trattano queste situazioni di ricezione garantita secondo due meccanismi: polling e inter-process communication.

Ricezione garantita con la tecnica del polling.

Per garantire che ogni singolo valore prodotto sia consumato una e una sola volta, si può operare aggiungendo al codice un meccanismo di polling, basato su una variabile booleana datoSulVassoio:

Implementazione della classe



La sincronizzazione tra i due metodi viene ottenuta tramite la variabile booleana datoSulVassoio. Si osservi con attenzione che in questo caso è necessario rimuovere la clausola synchronized dai due metodi, in quanto, in caso contrario, il primo ciclo while che entra in esecuzione non uscirebbe mai dal monitor, e quindi l'altro metodo non avrebbe mai l'opportunità di entrarvi e cambiare il valore della variabile datoSulVassoio.

La situazione che si crea in questo caso viene definita di Stallo (deadlock).

Quando un programma è costituito da diversi thread concorrenti, che condividono varie risorse, è importante garantire l'equità (fairness) tra di essi. Un sistema è equo quando permette a tutti i thread di accedere alle risorse e portare avanti il lavoro. Un sistema equo permette di evitare due problemi noti come deadlock (punto morto) e starvation (inedia, carestia).

Si ha una situazione di starvation quando uno o più thread non riescono ad accedere a una risorsa e quindi non possono eseguire il proprio lavoro. Si indica con deadlock una situazione di starvation indefinita, quando due o più thread sono in attesa di una condizione che non si verificherà mai. Per esempio, il thread 1 è dentro al monitor dell'oggetto A e deve entrare anche nel monitor dell'oggetto B. Nel frattempo, il thread 2 è nel monitor di B e deve entrare nel monitor di A. In questa situazione, ognuno dei due thread impedisce all'altro di progredire.

Per verificare la situazione di deadlock nell'esempio precedente, si provi ad aggiungere la clausola synchronized ai metodi prendi e metti nella classe Vassoio. Il programma, presentato in precedenza, ha caratteristiche di inefficienza: i due cicli while consumano una quantità enorme di tempo macchina senza produrre lavoro utile. Le primitive di inter-process communication di Java permettono invece di ottenere lo stesso risultato in modo molto efficiente.

Ricezione garantita via inter-process communication.

Il modo corretto di scrivere un programma in Java, per garantire che ogni valore prodotto sia consumato una e una sola volta, consiste nell'utilizzare le primitive wait e notify per far comunicare tra loro i due metodi metti e prendi.

Nella classe Vassoio, la variabile booleana datoSulVassoio indica ai due metodi prendi e metti se un dato nuovo è disponibile sul vassoio.

Implementazione della classe

Se la variabile booleana datoSulVassoio ha valore false, il metodo prendi utilizza la primitiva wait per passare nella coda dei processi in attesa fino a che il Produttore non segnala che un dato è pronto da prelevare. Quando questo succede, il metodo prendi legge il dato e con la primitiva notify indica al Produttore che il vassoio è libero.

Se la variabile booleana datoSulVassoio ha valore true, Il metodo resta in wait finchè il Consumatore non indica che il vassoio è libero. A questo punto, il metodo metti deposita sul vassoio il nuovo dato e segnala al Consumatore con la primitiva notify che il nuovo dato è disponibile.

La clausola synchronized garantisce che i due metodi accedano al dato sul vassoio e alla variabile booleana datoSulVassoio in modo sincronizzato. Ogni singolo dato viene depositato e prelevato una e una sola volta. Eseguendo il programma con queste modifiche si ottiene uno scambio corretto di messaggi tra i due processi.


Quinto Esercizio


Implementare l’interfaccia Runnable.
Un’interfaccia definisce un insieme di regole di comportamento che possono essere usate da altre classi. L’interfaccia dichiara i metodi (vuoti) e la classe che implementa l’interfaccia deve definirli.

Una classe può ereditare metodi e proprietà da una sola classe, ma può avere molte interfacce.

Per funzionare in un browser la classe Orologio deve essere una sottoclasse della classe Applet.
Inoltre, l’applet Orologio ha bisogno di un thread in modo che possa aggiornare continuamente la sua figura senza sovraccaricare il processo in cui sta in esecuzione.
Poichè il linguaggio Java non ammette l’ereditarietà multipla, la classe Orologio non può essere sottoclasse di Thread e di Applet. Così la classe Orologio deve usare l’interfaccia Runnable.

Tramite l’interfaccia Runnable l’applet Orologio fornisce il metodo run ai suoi thread.

La classe Orologio contiene il campo membro OrologioTh che è un riferimento a un Thread.

La tecnica per creare il Thread è realizzata nel metodo start dell’Applet, che il browser richiama quando l’utente visita la pagina. Nel metodo start l’operatore new inizializza il campo membro.

Il Thread ottiene il suo metodo run dall’oggetto riferito dal parametro passato nel costruttore (this):
OrologioTh = new Thread(this, "Orologio");
Il secondo argomento del costruttore è il nome del Thread.
import java.awt.Graphics;
import java.util.*;
import java.text.DateFormat;
import java.applet.Applet;
public class Orologio extends Applet implements Runnable {
  private Thread OrologioTh = null;
    public void start() {
      if (OrologioTh == null) {
// il test evita di creare il Thread ogni volta che si ritorna alla pagina
        OrologioTh = new Thread(this, "Orologio");
        OrologioTh.start();
      }
  }
    public void run() {
        Thread Processo = Thread.currentThread();
        while (OrologioTh == Processo) {
            repaint();
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e){
            }
        }
    }
    public void paint(Graphics g) {
        Calendar cal = Calendar.getInstance();
// acquisisce l’orario e lo converte in data
        Date date = cal.getTime();
        DateFormat dateFormatter = DateFormat.getTimeInstance();
        g.drawString(dateFormatter.format(date), 5, 10);
    }
    public void stop() {  
// ridefinisce il metodo stop dell’applet
        OrologioTh = null;
    }
}

Il metodo run dell’Applet cicla fino a quando il browser chiede di fermarsi. Ad ogni ciclo l’orologio si ridisegna. La pagina Web che mostra l’applet è: