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 Running (în
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
