Fire de execuție

Firul de execuție (în engleză: Thread) este, în esență, un subproces strict secvențial. Menținând definiția procesului ca un program în curs de execuție, putem considera acum că procesul este format din mai multe fire de execuție care se derulează în paralel, concurând la utilizarea resurselor alocate procesului respectiv.

În limbajul Java, există posibilitatea de a se crea programe care conțin mai multe fire de execuție. Aceasta înseamnă că, la executarea lor, se vor crea mai multe subprocese care se vor desfășura simultan, folosind același procesor și aceeași zonă de memorie. Acest mod de funcționare se numește în engleză multithreading.
 
Chiar și în cazul programelor Java în care nu sunt prevăzute explicit mai multe fire de execuție, în timpul executării lor coexistă cel puțin două astfel de fire: cel al aplicației propriu-zise și cel al colectorului de reziduuri (garbage collector). Colectorul de reziduuri este un fir de execuție cu nivel de prioritate coborât, care funcționează atunci când apar întreruperi în desfășurarea aplicației propriu-zise (de exemplu, când se așteaptă efectuarea unor operații de intrare/ieșire). În consecință, mașina virtuală Java are intrinsec o funcționare de tip multithreading.

Există două moduri de a programa un fir de execuție:
  - prin extinderea clasei Thread;
  - prin implementarea interfeței Runnable.

Clasa Thread

Clasa java.lang.Thread realizează un fir de execuție. Acesta poate fi un fir de execuție obișnuit, sau un demon.
 
Demonul (engleză: daemon thread) este un fir de execuție de prioritate coborâtă, care nu este invocat în mod explicit. El stă "adormit" și intră automat în execuție atunci când sunt îndeplinite anumite condiții.
Un exemplu tipic de demon este colectorul de reziduuri. După cum știm deja, acesta este un fir de execuție de prioritate coborâtă, care intră în funcțiune atunci când în memorie apar obiecte către care nu mai există referințe.

Crearea unui fir de execuție se face invocând unul dintre constructorii clasei Thread. Dintre aceștia, îi menționâm aici pe următorii:
 
    public Thread() - creează un nou fir de execuție cu numele implicit Thread-n, unde n este un număr de ordine;
    public Thread(String name) - creează un nou fir de execuție cu numele name;
    public Thread(Runnable target) - creează un nou fir de execuție, care conține obiectul target cu interfața Runnable, iar numele firului este cel implicit;
    public Thread(Runnable target, String name) - creează un nou fir de execuție, care conține obiectul target cu interfața Runnable și are numele name.

Cele mai frecvent utilizate metode ale clasei Thread sunt run(), start() și sleep(). Iată o listă a principalelor metode ale acestei clase:
 
    public void run() - conține programul care trebuie executat de firul respectiv; Așa cum este ea în clasa Thread, metoda nu face nimic (are corpul vid). Această metodă trebuie redefinită fie prin crearea unei subclase a clasei Thread, fie prin crearea unei obiect cu interfața Runnable, care să fie înglobat în acest Thread (folosind constructorul Thread(Runnable target));
    public void start() - pune firul nou creat în starea "gata pentru execuție";
    public static void sleep(long millis) throws InterruptedException - oprește temporar execuția acestui fir, pentru o durata de millis milisecunde;
    public static void yield() - oprește temporar execuția acestui fir, permițând monitorului să dea controlul altui fir de aceeași prioritate;
    public static Thread currentThread() - întoarce o referință la firul care este executat în momentul curent;
    public final void setPriority(int newPriority) - seteaza prioritatea firului de execuție; prioritatea trebuie sa fie cuprinsă în intervalul [Thread.MIN_PRIORITY, Thread.MAX_PRIORITY] (valorile lor fiind, respectiv, 1 și 10). Nerespectarea acestui interval genereaza  o IllegalArgumentException. Daca nu este stabilită explicit, prioritatea este Thread.NORM_PRIORITY (are valoarea 5). 
    public final int getPriority() - întoarce prioritatea curentă a firului de execuție.
    public final void setName(String name) - pune firului de execuție un nou nume;
    public final String getName() - întoarce numele firului de execuție;
    public final void setDaemon(boolean on) - dă firului de execuție calitatea de demon (dacă argumentul este true) sau de fir obișnuit (dacă argumentul este false); Metoda poate fi invocată numai dacă firul nu este activ, altfel se generează o IllegalThreadStateException.
    public final boolean isDaemon() - testează dacă firul de execuție este demon.

La programarea unui fir de execuție, principala atenție se acordă metodei run(), deoarece ea conține programul propriu-zis, care trebuie executat de acest fir. Totuși, metoda run()nu este invocată explicit. Ea este invocată de mașina virtuală Java, atunci când firul respectiv este pus în mod efectiv în execuție.

Pentru redefinirea metodei Run este necesar sa creem o subclasă a clasei Thread sau sa creem o clasă cu interfața Runnable, și să dăm o instanță a acestei clase ca argument constructorului clasei Thread.

După ce firul de execuție a fost creat (de exemplu prin expresia new Thread()), el există în memorie, dar înca nu poate fi executat. Se găsește, deci, în starea "nou creat" (engleza: born). Pentru a-l face gata de execuție, pentru firul respectiv este invocată metoda start().
 
După invocarea metodei start(), firul este executabil, dar aceasta nu înseamnă că el este pus în execuție imediat: noul fir devine concurent cu cele deja existente (în primul rând cu cel care l-a creat), și va fi pus în execuție în mod efectiv (i se va acorda acces la procesor) atunci când îi vine rândul, conform cu strategia de alocare a resurselor adoptată. 

Un fir în curs de execuție poate fi oprit temporar ("adormit") invocând metoda statică sleep(long millis). Argumentul acestei metode este intervalul de timp cât procesul se va găsi în stare de "somn", exprimat în milisecunde.
 
Referitor la utilizarea acestei metode, remarcăm două aspecte:
  - întrucât ea poate "arunca" o InterruptedException, este necesar să fie prinsă într-un bloc try..catch;
  - întrucat metoda este statică și nu are ca argument un fir de execuție, ea nu poate fi invocată decât în metodele firului de execuție căruia i se aplică.

Fiecărui fir de execuție i se asociază o anumită prioritate. Aceasta este un numar întreg, cuprins între valorile Thread.MIN_PRIORITY și Thread.MAX_PRIORITY (având valorile 1 și respectiv 10). Dacă metoda setPriority(int newPriority) nu este invocată explicit, firului i se dă prioritatea implicita Thread.NORM_PRIORITY, respectiv 5. 

Daca firul de execuție solicită o operație de intrare/ieșire, el rămîne blocat pe întreaga durată a executării operației respective.

În timpul în care firul de execuție "doarme" (ca urmare a invocării metodei sleep()) sau este blocat (deoarece asteapta terminarea unei operatii de intrare/iesire), se pot executa alte fire, de prioritate egală sau inferioară. În schimb, firele de prioritate superioară pot întrerupe în orice moment executarea celor de prioritate inferioară.

Un proces poate fi pus, de asemenea, în așteptare prin invocarea metodei wait(). În acest caz, firul așteaptă confirmarea efectuării unei anumite acțiuni, prin invocarea metodei notify() sau notifyAll(). Aceste trei metode există în clasa Object și servesc la sincronizarea firelor de execuție.
 
Având în vedere cele de mai sus, punem în evidență urmatoarele stări în care se poate găsi un fir de execuție pe durata ciclului său de viață:
   - nou creat (engleză: born) - este starea în care se găsește imediat ce a fost creat prin operatorul new; în această stare, firul nu poate fi executat;
   - gata de execuție (engleză: ready) - este starea în care firul poate fi pus în execuție; punerea efectivă în execuție se face de către mașina virtuala Java atunci când procesorul este liber și nu sunt gata de execuție fire de prioritate superioară;
  - în execuție (engleză: running) - este starea în care procesul se execută efectiv (ocupă procesorul);
  - adormit (engleză: sleeping) - este starea de oprire temporară, ca urmare a invocării metodei sleep();
  - blocat (engleză: blocked) - este starea în care așteaptă incheierea unei operații de intrare/ieșire;
  - în așteptare (engleză: waiting) - este starea în care firul se găsește din momentul în care se invocă metoda wait(), până când primește o confirmare dată prin invocarea metodei notify();
  - mort (engleză: dead) - este starea în care intră firul de execuție dupa ce s-a încheiat executarea metodei run().

Relațiile dintre aceste stări sunt reprezentate grafic în figura de mai jos.

Denumirile stărilor firului de execuție au fost date în limba engleză, pentru a putea fi urmarită mai ușor documentația originală din Java API.

Trecerea de la starea Born la starea Ready (gata) se face prin invocarea metodei start()

Trecerea de la starea Ready la starea Runningn execuție) se face de către mașina virtuală Java (de către dispecerul firelor) atunci când sunt create condițiile necesare: procesorul este liber și nici un fir de execuție de prioritate superioară nu se găsește în starea Ready.

Trecerea de la starea Running la starea Ready se face la executarea metodei yield(), sau atunci când a expirat tranșa de timp alocată procesului (în cazul sistemelor cu diviziune a timpului).

Trecerea de la starea Running la starea Sleeping se face la executarea metodei sleep()

Trecerea de la starea Sleeping la starea Ready se face când a expirat intervalul de timp de "adormire" (intervalul dat ca argument în metoda sleep(long millis)).

Trecerea de la starea Running la starea Blocked are loc atunci când firul de execuție respectiv solicită efectuarea unei operații de intrare ieșire.

Trecerea de la starea Blocked la starea Ready are loc când s-a încheiat operația de intrare/ieșire solicitată de acest fir.

Trecerea de la starea Running la starea Waiting se face la invocarea metodei wait().

Trecerea de la starea Waiting la starea Ready se face când se primește o confirmare prin invocarea metodei notify() sau notifyAll().

Trecerea de la starea Running la starea Dead are loc atunci când se încheie executarea metodei run() a firului de execuție respectiv.

Se observă că orice fir de execuție își începe ciclul de viață în starea Born și îl încheie în starea Dead. Punerea firului în execuție efectivă (trecerea de la Ready la Running) se face numai de către dispecerul firelor. În toate cazurile în care se încheie o stare de oprire temporară a execuției (Sleeping, Waiting sau Blocked), firul de execuție respectiv trece în starea Ready, așteptând ca dispecerul să-l pună efectiv în execuție (să-l treacă în starea Running). În timpul cât firul de execuție se găsește în orice altă stare decât Running, procesorul calculatorului este liber, putând fi utilizat de alte fire de execuție sau de alte procese.


 
Exemplul 1
Fișierul DouaFireA.java conține un exemplu de aplicație, în care se creează fire de execuție folosind o clasă care extinde clasa Thread. 
 
/* Crearea si utilizarea unei clase de fire de executie care
   extinde clasa Thread dar nu foloseste metodele sleep sau yield.
   Se creeaza doua fire de aceeasi prioritate (Thread.NORM_PRIORITY).
*/

class DouaFireA {

  /* Clasa firelor de executie */
  static class Fir extends Thread {

    Fir(String numeFir) {
      super(numeFir);
      System.out.println("S-a creat firul "+getName());
    }

    /* Redefinirea metodei run() din clasa Thread */
    public void run() {
     System.out.println("Incepe executarea firului "+getName());
     for(int i=0; i<6; i++) {
      System.out.println("Firul "+getName()+" ciclul i="+i);
     }
    }
  } /* incheierea clasei Fir */

  public static void main(String args[]) throws Exception {
    Fir fir1, fir2;
    System.out.println("Se creeaza firul Alpha");
    fir1=new Fir("Alpha");
    System.out.println("Se creeaza firul Beta");
    fir2=new Fir("Beta");
    System.out.println("Se pune in executie firul Alpha");
    fir1.start();
    System.out.println("Se pune in executie firul Beta");
    fir2.start();
    System.out.println("Sfarsitul metodei main()");
  } /* Sfarsitul metodei main() */
}

Clasa Fir din această aplicație extinde clasa Thread, deci instanțele ei sunt fire de execuție. În clasa Fir s-a redefinit metoda run(), astfel încât să conțină programul firului respectiv. În cazul nostru, firul execută un ciclu, în care afișează la terminal un mesaj, conținând numele firului și indicele ciclului executat.

În metoda main() a aplicației se creează două instanțe ale clasei Fir, dându-le, respectiv, numele Alpha și Beta, apoi se lansează în execuție aceste fire. Iată un exemplu de rezultat al executării acestei aplicații:
 
Se creeaza firul Alpha
S-a creat firul Alpha
Se creeaza firul Beta
S-a creat firul Beta
Sfarsitul metodei main()
Incepe executarea firului Alpha
Firul Alpha ciclul i=0
Firul Alpha ciclul i=1
Firul Alpha ciclul i=2
Firul Alpha ciclul i=3
Firul Alpha ciclul i=4
Firul Alpha ciclul i=5
Incepe executarea firului Beta
Firul Beta ciclul i=0
Firul Beta ciclul i=1
Firul Beta ciclul i=2
Firul Beta ciclul i=3
Firul Beta ciclul i=4
Firul Beta ciclul i=5

Remarcăm cele ce urmează.

  - În realitate, în afară de firele de execuție Alpha și Beta create explicit de către noi, mai exista incă două fire de execuție: cel al aplicației propriu-zise (executarea metodei main()) și cel al colectorului de reziduuri. Întrucât nu s-au setat explicit prioritățile, firele Alpha, Beta și main() au toate prioritatea Thread.NORM_PRIORITY.
  - Întrucât toate firele au aceeași prioritate, durata executării fiecărui fir este mică și nu s-au folosit metode de suspendare a execuției (yield(), sleep(), wait()), în momentul în care începe executarea unui fir acesta se execută până la capăt, după care procesorul sistemului este preluat de firul următor. În consecință s-a executat mai întâi metoda main() până la incheierea ei (confirmată prin mesajul "Sfârșitul metodei main()"), după care se execută complet firul Alpha și apoi firul Beta (în ordinea lansării).
  - Aplicația se încheie în momentul în care s-a încheiat ultimul fir de execuție.


 
 
Exemplul 2
În fișierul DouaFireA1.java se reia aplicația din exemplul precedent, dar la sfârșitul fiecărui ciclu de indice i se invocă metoda yield() care suspendă firul în curs și permite astfel să se transmită controlul următorului fir de aceeași prioritate.
 
/* Crearea si utilizarea unei clase de fire de executie care
   extinde clasa Thread. Se foloseste metoda yield() pentru 
   a ceda controlul altui fir de aceeasi prioritate.
   Se creeaza doua fire de aceeasi prioritate (Thread.NORM_PRIORITY).
*/

class DouaFireA1 {

  /* Clasa firelor de executie */
  static class Fir extends Thread {

    Fir(String numeFir) {
      super(numeFir);
      System.out.println("S-a creat firul "+getName());
    }

    /* Redefinirea metodei run() din clasa Thread */
    public void run() {
     System.out.println("Incepe executarea firului "+getName());
     for(int i=0; i<6; i++) {
      System.out.println("Firul "+getName()+" ciclul i="+i);
      yield(); // cedarea controlului procesorului
     }
    }
  } /* incheierea clasei Fir */

  public static void main(String args[]) throws Exception {
    Fir fir1, fir2;
    System.out.println("Se creeaza firul Alpha");
    fir1=new Fir("Alpha");
    System.out.println("Se creeaza firul Beta");
    fir2=new Fir("Beta");
    System.out.println("Se pune in executie firul Alpha");
    fir1.start();
    System.out.println("Se pune in executie firul Beta");
    fir2.start();
    System.out.println("Sfarsitul metodei main()");
  } /* Sfarsitul metodei main() */
}

Rezultatul executarii acestei aplicatii este urmatorul:
 
Se creeaza firul Alpha
S-a creat firul Alpha
Se creeaza firul Beta
S-a creat firul Beta
Sfarsitul metodei main()
Incepe executarea firului Alpha
Firul Alpha ciclul i=0
Incepe executarea firului Beta
Firul Beta ciclul i=0
Firul Alpha ciclul i=1
Firul Beta ciclul i=1
Firul Alpha ciclul i=2
Firul Beta ciclul i=2
Firul Alpha ciclul i=3
Firul Beta ciclul i=3
Firul Alpha ciclul i=4
Firul Beta ciclul i=4
Firul Alpha ciclul i=5
Firul Beta ciclul i=5

Se observă cu ușurință că, în acest caz, executarea celor două fire, Alpha și Beta, are loc intercalat, deoarece, după fiecare parcurgere a unui ciclu într-un fir, se invocă metoda yield() , cedându-se procesorul sistemului în favoarea celuilalt fir. Aceasta nu s-ar fi întâmplat, dacă cele doua fire ar fi avut priorități diferite.


 
Exemplul 3
În fișierul DouaFireB.java s-a reluat aplicația din Exemplul 1, aducându-i-se următoarele modificări:
  - în ciclul cu contorul i din metoda run() a clasei Fir s-a mai introdus un ciclu interior (cu contorul j) care se parcurge de 100000000 ori, pentru a mări în mod artificial durata de execuție; în locul acestuia se putea pune, evident, orice alta secvența de instrucțiuni cu durata de execuție comparabilă;
  - cele două cicluri (pe i și pe j) au fost introduse și în metoda main(), pentru a putea urmări executarea acesteia după lansarea firelor Alpha și Beta.
  Fragmentele de program nou adăugate sunt puse în evidență în textul de mai jos cu caractere aldine (îngroșate).
 
/* Crearea si utilizarea unei clase de fire de executie. In metoda 
   run a firului nu se folosesc metodele sleep() sau yield(), dar se 
   parcurge un ciclu de contorizare (pe j) de 100000000 ori pentru a
   mari durata de executie a ciclului exterior (pe indicele i).
   Se creeaza doua fire de aceeasi prioritate (Thread.NORM_PRIORITY).
*/

class DouaFireB {

  /* Clasa firelor de executie */
  static class Fir extends Thread {

    Fir(String numeFir) {
      super(numeFir);
      System.out.println("S-a creat firul "+getName());
    }

    /* Redefinirea metodei run() din clasa Thread */
    public void run() {
     System.out.println("Incepe executarea firului "+getName());
     for(int i=0; i<5; i++) {
      for(int j=0; j<100000000; j++);
      System.out.println("Firul "+getName()+" ciclul i="+i);
     }
    }
  } /* incheierea clasei Fir */

  public static void main(String args[]) throws Exception {
    Fir fir1, fir2, fir3;
    System.out.println("Se creeaza firele Alpha si Beta");
    fir1=new Fir("Alpha");
    fir2=new Fir("Beta");
    System.out.println("Se pun in executie cele doua fire");
    fir1.start();
    fir2.start();
    for (int i=0; i<5; i++) {
      for(int j=0; j<100000000; j++);
      System.out.println("main() ciclul i="+i);
    }
    System.out.println("Sfarsitul metodei main()");
  } /* Sfarsitul metodei main() */
}

Iată un exemplu de rezultat obținut prin executarea acestei aplicații:
 
Se creeaza firele Alpha si Beta
S-a creat firul Alpha
S-a creat firul Beta
Incepe executarea firului Alpha
Incepe executarea firului Beta
Firul Alpha ciclul i=0
Firul Beta ciclul i=0
main() ciclul i=0
main() ciclul i=1
Firul Alpha ciclul i=1
Firul Beta ciclul i=1
main() ciclul i=2
Firul Alpha ciclul i=2
Firul Beta ciclul i=2
main() ciclul i=3
Firul Alpha ciclul i=3
Firul Beta ciclul i=3
main() ciclul i=4
Sfarsitul metodei main()
Firul Alpha ciclul i=4
Firul Beta ciclul i=4

Constatăm că, întrucat durata de execuție a fiecărui fir a devenit mult mai mare decât în exemplul precedent și toate cele trei fire (Alpha, Beta si main()) au aceeași prioritate (Thread.NORM_PRIORITY), se aplică o strategie de partajare a timpului, astfel încât timpul de funcționare al procesorului este alocat sub forma de tranșe succesive fiecăruia dintre procese. În consecință, se creeaza impresia că cele trei procese se desfășoară în paralel, simultan în timp.


 
Exemplul 4
În fișierul DouaFireC.java se reia aplicația din exemplul precedent, dar s-a renunțat la ciclurile interioare (cu contorul j), în schimb după parcurgerea fiecărui ciclu de indice i se pune firul respectiv in așteptare timp de 1000 milisecunde prin invocarea metodei sleep. Această metodă aruncă excepția InterruptedException care trebuie captată.

În mod similar s-a procedat și în ciclul din metoda main. Având însă în vedere că metoda main() nu se găsește într-o clasa derivata din Thread, la invocarea metodei statice sleep a fost necesar să se indice clasa căreia îi aparține, deci invocarea s-a făcut sub forma Thread.sleep(1000).
 
/* Crearea și utilizarea unei clase de fire de execuție. În metoda 
   run a firului se foloseste metoda sleep(1000) pentru a pune 
   firul in asteptare pe o durata de 1000 milisecunde. Similar se
   procedeaza in metoda main()
*/

class DouaFireC {

  /* Clasa firelor de executie */
  static class Fir extends Thread {

    Fir(String numeFir) {
      super(numeFir);
      System.out.println("S-a creat firul "+getName());
    }

    /* Redefinirea metodei run() din clasa Thread */
    public void run() {
     System.out.println("Incepe executarea firului "+getName());
     for(int i=0; i<5; i++) {
      System.out.println("Firul "+getName()+" ciclul i="+i);
      try {
        sleep(1000);
      }
      catch(InterruptedException e) {}
     }
    }
  } /* incheierea clasei Fir */

  public static void main(String args[]) throws Exception {
    Fir fir1, fir2, fir3;
    System.out.println("Se creeaza firele Alpha si Beta");
    fir1=new Fir("Alpha");
    fir2=new Fir("Beta");
    System.out.println("Se pun in executie cele doua fire");
    fir1.start();
    fir2.start();
    for (int i=0; i<5; i++) {
      System.out.println("main() ciclul i="+i);
      try {
        Thread.sleep(1000);
      }
      catch(InterruptedException e) {}
    }
    System.out.println("Sfarsitul metodei main()");
  } /* Sfarsitul metodei main() */
}

Iată rezultatul executării acestei aplicații:
 
Se creeaza firele Alpha si Beta
S-a creat firul Alpha
S-a creat firul Beta
Se pun in executie cele doua fire
main() ciclul i=0
Incepe executarea firului Alpha
Firul Alpha ciclul i=0
Incepe executarea firului Beta
Firul Beta ciclul i=0
main() ciclul i=1
Firul Alpha ciclul i=1
Firul Beta ciclul i=1
main() ciclul i=2
Firul Alpha ciclul i=2
Firul Beta ciclul i=2
main() ciclul i=3
Firul Alpha ciclul i=3
Firul Beta ciclul i=3
main() ciclul i=4
Firul Alpha ciclul i=4
Firul Beta ciclul i=4
Sfarsitul metodei main()

În timp ce un fir "doarme", se pot executa alte fire, chiar dacă ele sunt de prioritate inferioară!



© Copyright 2000 - Severin BUMBARU, Universitatea "Dunărea de Jos" din Galați