Stati di un thread, priorità.


Java Sun

Earthweb
(ex Gamelan)

Jars

Java Boutique

JavaWorld

PIP






  JSem: 5.3

Stati di un thread

Se sul computer c'è un'unica CPU, a un dato momento può essere eseguito un unico thread; la CPU passerà da un thread a l'altro per dare l'impressione della simultaneità. Bene, ma in base a quali meccanismi viene scelto un thread piuttosto che un altro e come può il programmatore intervenire in queste scelte?
Prima di tutto un thread ha vari stati:

  • New Thread: il thread è vuoto, non può fare nulla se non chiamare start().


  • Runnable: lo diventa dopo che start() è stato chiamato: start() alloca la memoria necessaria e chiama run(); il thread è pronto e quando la CPU lo chiamerà, potrà entrare in esecuzione.


  • Not Runnable: il thread non è morto ma:

  • è stato messo a riposo per esempio con il metodo sleep() tornerà Runnable dopo che il tempo specificato è scaduto.
  • è in attesa (per es. con il metodo wait()) che una certa condizione sia verificata, tornerà Runnable quando qualche oggetto gli avrà notificato (con notify) che la condizione è verificata.
  • il thread è bloccato da processi I/O (Input/Output): tornerà Runnable quando questi processi saranno terminati.


  • Dead: il metodo run() è terminato. Una volta morto un thread esiste ancora come oggetto, ma non può più tornare Runnable (e questo, forse, spiega la terminologia un pò macabra).


  •    Il metodo isAlive() ritorna true se il thread è Runnable o Not Runnable e ritorna false se il thread è New Thread (sostanzialmente in gestazione ma non ancora nato) o Dead. Se isAlive() ritorna true (risp. false) non è possibile distinguere tra Runnable e Not Runnable (risp. New Thread e Dead).
    Un thread può quindi passare più volte dallo stato Not Runnable allo stato Runnable; inoltre siccome la CPU, a un dato momento, può eseguire un unico thread, un thread Runnable passerà più volte da uno stato di "pronto" a uno stato di "esecuzione" (e viceversa).

    Priorità

    In Java è possibile assegnare ad ogni thread un numero (int) che indica la sua priorità. Quando un thread viene creato, eredita, per default, la priorità del thread che lo ha creato. E' comunque possibile modificare questa priorità con il metodo setPriority() (vedere la doc).
    Tra tutti i threads in attesa di esecuzione, il runtime di Java sceglierà quello con la priorità più alta. Questo thread andrà in esecuzione finchè:

  • un thread con una priorità più alta diventa Runnable (il runtime di Java è "preemptive").
  • il thread diventa Dead o chiama yield per cedere la CPU.
  • sui sistemi con time-slicing (spartizione del tempo), il tempo del thread è scaduto.


  • Due parole per chiarire l'ultima frase. I vari sistemi operativi affrontano il problema del multithreading essenzialmente in due modi:

  • Preemptive method: il thread con la priorità più alta viene eseguito fino alla sua morte, messa in attesa o creazione di un thread con priorità maggiore.


  • Time slicing: un thread ha comunque un determinato tempo di esecuzione dopodichè è messo nello stato "pronto".


  • Il metodo "preemptive" (solaris, unix) è più prevedibile ma presenta l'inconveniente che un thread con alta priorità potrebbe eseguirsi in eterno, impedendo l'esecuzione di altri threads. La spartizione del tempo (Windows, Macintosh) è meno prevedibile, ma impedisce a threads troppo egoisti di impossesarsi di tutte le risorse.
    Il runtime di Java abbiamo detto è piuttosto del tipo "preemptive", ma questo non è del tutto vero perchè si riserva comunque il diritto di eseguire threads con priorità più basse se questo può evitare il "congelamento" (starvation) del programma.

    Morale: non fare troppo affidamento nel codice sulle priorità tra threads!
    Per illustrare quanto detto finora ecco un piccolo programma (preso dal "Java tutorial" Campione-Walrath (Addison-Wesley 1998)) che si propone di fare una gara tra due threads. Iniziamo col definire la classe dei threads:

    public class SelfishRunner extends Thread{
    private int tick=1;
    private int num;
    //costruttore
    public SelfishRunner(int num){
    	this.num = num;
    }
    public void run(){
       while(tick < 400000){
       tick++;
       if((tick % 50000) == 0){
       System.out.println("Thread #"+num+", tick = "+tick);
    		}
    	}
      }
    }    


    La classe si chiama SelfishRunner (selfish = egoista) perchè il metodo run() contiene un blocco while e il thread, quando in esecuzione, non lascerà volentieri la CPU finchè il suo ciclo while non sarà terminato.
    Passiamo adesso al programma:

    public class Gara{
    
    public static void main(String[] args){
    SelfishRunner[] runners = new SelfishRunner[2];
     
     for(int i=0; i < 2;i++){
     	runners[i] = new SelfishRunner(i);
     	runners[i].setPriority(2);
    }
    for(int i=0; i<2;i++){
    	runners[i].start();
    		}
    	}
    }    

    Qui si creano due threads con uguale priorità; se il sistema operativo segue il metodo del "time slicing" benchè i threads siano (visto il loro metodo run) un pò egoisti, l'output non dovrebbe essere del tutto squilibrato:
    
    Thread #0, tick = 50000
    Thread #0, tick = 100000
    Thread #1, tick = 50000
    Thread #1, tick = 100000
    Thread #1, tick = 150000
    Thread #1, tick = 200000
    Thread #1, tick = 250000
    Thread #1, tick = 300000
    Thread #1, tick = 350000
    Thread #1, tick = 400000
    Thread #0, tick = 150000
    Thread #0, tick = 200000
    Thread #0, tick = 250000
    Thread #0, tick = 300000
    Thread #0, tick = 350000
    Thread #0, tick = 400000
    (output ottenuto con Windows 98)
    Se invece si cambiano le priorità, per esempio runners[i].setPriority(i+2); nella sesta riga della classe Gara, allora non c'è più gara:
    
    Thread #1, tick = 50000
    Thread #1, tick = 100000
    Thread #1, tick = 150000
    Thread #1, tick = 200000
    Thread #1, tick = 250000
    Thread #1, tick = 300000
    Thread #1, tick = 350000
    Thread #1, tick = 400000
    Thread #0, tick = 50000
    Thread #0, tick = 100000
    Thread #0, tick = 150000
    Thread #0, tick = 200000
    Thread #0, tick = 250000
    Thread #0, tick = 300000
    Thread #0, tick = 350000
    Thread #0, tick = 400000
    

    Infatti il thread #1 ha priorità 3 mentre il thread #0 ha priorità 2.
    Con un sistema che non addotta il "time-slicing" si dovrebbe ottenere questo output (con thread#0 al posto di thread#1) anche quando i threads hanno la stessa priorità.



     next		content		previous