Documente Academic
Documente Profesional
Documente Cultură
Bazele multithreading.......................................................................................................................... 2
Clasa Thread şi interfaţa Runnable....................................................................................................... 4
Crearea unui fir de execuţie ................................................................................................................. 4
Interfaţa Runnable ........................................................................................................................... 4
Îmbunătăţiri aduse exemplului ........................................................................................................ 7
Moştenirea unui Thread .................................................................................................................. 9
Crearea mai multor fire de execuţie................................................................................................... 11
Când se încheie un fir de execuţie? ................................................................................................ 14
Priorităţi în fire de execuţie ............................................................................................................... 16
Sincronizarea ..................................................................................................................................... 18
Folosirea metodelor sincronizate ................................................................................................... 19
Blocul synchronized ....................................................................................................................... 21
Comunicarea între fire de execuţie .................................................................................................... 22
Suspendarea, reluarea şi oprirea firelor de execuţie .......................................................................... 26
Grupuri de fire de execuţie ................................................................................................................ 29
Alte metode................................................................................................................................... 30
Bazele multithreading
Există doua tipuri distincte de multitasking: unul bazat pe procese şi al doilea bazat pe fire de
execuţie sau thread-uri. Este important să facem distincţia dintre cele două. Un proces este, în esenţă,
un program ce se află în execuţie. De aceea multitasking bazat pe procese înseamnă faptul că două sau
mai multe programe rulează în acelaşi timp. De exemplu, compilatorul Java va permite crearea unor noi
programe, în timp ce folosiţi un editor de text, sau navigaţi pe Internet.
În cazul multitasking bazat pe thread-uri, acesta este cea mai mică unitate ce va fi programată
de dispecer. Aceasta înseamnă că un program poate realiza mai multe lucruri simultan. De exemplu
aplicaţia de mail Outlook, sau un client de mail va permite si trimiterea/primirea de mail-uri in timp ce
compuneţi un nou mail, sau verificaţi lista de priorităţi dintr-o zi, sau căutaţi un anumit mail, etc.
Avantajul principal al multithreading este că permite scrierea unor programe foarte eficiente,
deoarece se elimină spaţiile inutile temporale prezente în multe programe, atunci când acesta este idle.
Majoritatea dispozitivelor I/O sunt mai lente decât CPU. De aceea programul va petrece majoritatea
execuţiei aşteptând informaţia de la aceste dispozitive. În acest timp, în cazul folosirii multithreading, se
pot executa instrucţiuni care să realizeze alte sarcini. De exemplu, în timp ce un program trimite un fişier
pe Internet, altă parte a programului poate citi de la tastatura, următoarea bucată ce va fi trimisă.
Un fir de execuţie este o parte dintr-un proces executat secvenţial, o serie de instrucţiuni ce vor
fi executate secvenţial. Folosirea a două sau mai multe thread-uri duce la execuţia în paralel a acestor
serii de instrucţiuni în cadrul unui proces. Mai jos avem reprezentat modul de funcţionare al unui proces
pe un singur thread.
Adevărata utilitate a unui thread intervine în cazul în care mai multe fire de execuţie coexistă în
acelaşi proces.
Pe lângă aceste caracteristici, mai este nevoie de o anumită funcţionalitate, numită sincronizare,
ce permite execuţia firelor într-o manieră controlată.
Faţă de modul în care limbajul C tratează această problemă a multithreading-ului , Java ascunde unele
detalii , tocmai pentru a înlesni şi a face cât mai convenient lucrul cu thread-uri.
Clasa Thread şi interfaţa Runnable
În Java, sistemul multithreading este construit pe clasa Thread şi interfaţa Runnable. Pentru a
crea un nou fir de execuţie, va trebui să extindem clasa Thread sau să implementăm interfaţa Runnable.
Clasa Thread defineşte câteva metode care ajută la tratarea situaţiilor ce intervin în lucrul cu
thread-uri. Iată cele mai folosite:
Medoda Descriere
Toate procesele au cel puţin un fir de execuţie ce se numeşte firul principal sau main thread.
Acesta se va executa înainte celorlalte eventual fire de execuţie ce vor fi create ulterior.
Un fir de execuţie se poate crea prin instanţierea unui obiect de tip Thread. Clasa Thread
încapsulează un obiect ce implementează interfaţa Runnable. Există două moduri de a crea un fir:
1. Implementarea interfeţei Runnable
2. Extinderea (moştenirea) clasei Thread
Interfaţa Runnable
În cadrul acestei metode, veţi defini codul ce constituie noul fir de execuţie. Metoda run() poate
apela alte metode, folosi alte clase, şi declara variabile ca orice metodă. Diferenţa majoră este că run()
reprezintă entry point pentru un nou thread din cadrul procesului. Acest thread îşi încheie execuţia
atunci când se iese din funcţia run (cu return).
Modul de lucru este următorul:
1. Se creează o clasă ce implementează interfaţa Runnable.
2. Se instanţiază un obiect de tip Thread, într-una din metodele din acea clasă ( chiar si in
constructorul clasei ). Constructorul unui Thread va fi:
Thread(Runnable threadOb)
3. Odată creat, noul obiect de tip Thread, nu va porni până când nu apelăm metoda start(). În
principiu apelul metodei start() înseamnă apelul metodei run(). Apelul metodei start este:
void start()
Prima dată, clasa MyThread implementează Runnable. Aceasta înseamnă că un obiect de tipul
MyThread va fi folosit pentru a crea un thread şi deci, va fi parametrul unui constructor Thread.
În interiorul metodei run(), există o buclă while, ce contorizează de la 0 la 9. Metoda
Thread.sleep(500); are ca scop, suspendarea firului de execuţie curent timp de 500 de
milisecunde.
În clasa DemoThread, în metoda main(), se creează un nou obiect, de tip MyThread care va fi
apelat ulterior de newThrd:
MyThread mt = new MyThread("Child #1");
Thread newThrd = new Thread(mt);
// incepe noul fir de execute
newThrd.start();
Obiectul mt este folosit pentru a crea un Thread, iar acest lucru este posibil pentru că MyThread
implementează Runnable . Execuţia unui thread începe cu start. Pe firul principal de execuţie,
instrucţiunile vor fi rulate ca şi când start() ar fi o metodă obişnuită. Firul principal de execuţie va rula o
buclă while în care aşteaptă ca mt.count să ajungă la 10. Acest lucru se va întâmpla, datorită faptului
că în thread-ul secundar count creşte.
Iată rezultatul rulării acestui program:
Modificările majore sunt: mutarea obiectului de tip Thread în cadrul clasei MyThread şi pornirea
acestui fir din constructorul clasei MyThread.
Implementarea interfeţei Runnable este un mod de a crea o clasă ce poate instanţia fire. A doua
ar fi extinderea unei clase numită Thread. Atunci când o clasă extinde Thread, va trebui să suprascrie
metoda run(), care este un entry point pentru noul fir. De asemenea trebuie să apeleze start() pentru
ca noul fir să îşi înceapă execuţia.
Pornind de la exemplul anterior, putem transforma acesta aplicând moştenirea lui Thread:
1. Se schimba declaraţia lui MyThread ca să extindă Thread:
Thread thread;
Variabila thread nu mai are nici o utilitate, din moment ce MyThread este un Thread.
Apelul lui super(name); este în fapt, apelul unui Thread cu parametru un String:
Thread(String name);
Exemplele anterioare conţineau doar un singur fir de execuţie. Totuşi, programul poate crea
oricâte astfel de fire. Următorul program creează trei thread-uri:
}
catch(InterruptedException exc)
{
System.out.println("Main thread interrupted.");
}
} while (mt1.count < 10 || mt2.count < 10 || mt3.count < 10);
System.out.println("Main thread ending.");
}
}
Cele trei fire de execuţie vor rula în paralel cu firul principal, acesta aşteptând ca cele trei sa îşi încheie
execuţia. O diagramă a acestei funcţionalităţi este în figura de mai jos:
Este util să ştim când un fir de execuţie s-a încheiat, pentru a controla logica şi fluxul
programului. În exemplele anterioare, acest lucru a fost posibil datorită unei variabile de control şi
anume count. Aceasta este o soluţie slabă din punct de vedere tehnic. Clasa Thread oferă două moduri
de a determina faptul că un fir de execuţie s-a încheiat. Primul este prin funcţia isAllive():
Această metodă returnează true, dacă firul de execuţie, pentru care a fost apelată, încă rulează,
şi false altfel. Pentru a verifica funcţionalitatea isAlive(), vom modifica versiunea clasei
ThreeChildThreads.
class ThreeChildThreads {
public static void main(String args[])
{
System.out.println("Main thread starting.");
//cream trei fire de execuţie
MyThread mt1 = new MyThread("Child #1");
MyThread mt2 = new MyThread("Child #2");
MyThread mt3 = new MyThread("Child #3");
//care vor fi automat lansate
//in bucla while, thread-ul principal
//aşteaptă ca toate firele sa se încheie
do
{
System.out.print(".");
try
{
Thread.sleep(100);
}
catch(InterruptedException exc)
{
System.out.println("Main thread interrupted.");
}
} while (mt1.thread.isAlive() || mt2.thread.isAlive() ||
mt3.thread.isAlive() );
System.out.println("Main thread ending.");
}
}
După cum se poate observa, acest program este la fel ca şi cel anterior, cu o modificare: condiţia
din while se bazează pe această funcţie, oferind robusteţe aplicaţiei.
Cel de-al doilea mod de a aştepta ca un fir să îşi încheie execuţia este join():
class ThreeChildThreads
{
public static void main(String args[])
{
System.out.println("Main thread starting.");
//cream trei fire de execuţie
MyThread mt1 = new MyThread("Child #1");
MyThread mt2 = new MyThread("Child #2");
MyThread mt3 = new MyThread("Child #3");
//care vor fi automat lansate
//in bucla while, thread-ul principal
//aşteaptă ca toate firele sa se încheie
try
{
mt1.thread.join();
System.out.println("Child 1 joined.");
mt2.thread.join();
System.out.println("Child 2 joined.");
mt3.thread.join();
System.out.println("Child 3 joined.");
}
catch(InterruptedException exc)
{
System.out.println("Main thread interrupted.");
}
System.out.println("Main thread ending.");
}
}
Fiecare fir de execuţie are asociat o anumită prioritate. Aceasta este determinată de cât timp în
CPU este alocat acelui thread. În general priorităţile joase denotă puţin timp, iar cele mari denotă mai
mult timp în CPU. Evident, cât de mult timp va avea acces la CPU, un fir de execuţie, influenţează direct,
fluxul logic al programului.
Este important să înţelegem factorii care influenţează accesul la CPU. De exemplu, dacă un fir cu
prioritate mare aşteaptă o resursă auxiliară, atunci el va fi blocat, şi alt fir cu o prioritate mai mică va
avea acces. Totuşi odată ce thread-ul cu prioritatea mai mare are resursa, va opri execuţia firului cu
prioritate mai mică pentru aşi relua propria execuţie. Alt factor care poate afecta dispecerul de fire de
execuţie, este modul în care sistemul de operare implementează multitasking. De aceea, doar prin
asignarea de priorităţi mai mici, mai mari, nu va influenţa neapărat rapiditatea cu care un fir rulează.
Prioritatea înseamnă un acces probabil mai mic sau mai mare la CPU.
Atunci când un fir de execuţie porneşte, prioritatea sa este egală cu cea a thread-ului părinte.
Valoarea nivelului de prioritate trebuie să fie între MIN_PRIORITY şi MAX_PRIORITY. În mod normal,
aceste valori sunt 1 şi 10. Pentru a returna un fir cu prioritate implicită, avem NORM_PRIORITY ce
înseamnă 5. Aceste valori sunt constante în clasa Thread. Se poate obţine prioritatea actuală a unui fior
de execuţie apelând metoda getPriority():
Exemplul următor demonstrează folosirea a două fire de prioritate diferită. Metoda run()
conţine o bulcă while care contorizează iteraţii. Această bulcă se opreşte când numărul de iteraţii
depăşeşte 1000000 sau variabila stop a fost setată pe true. Iniţial stop este setată pe false, dar primul
thread care termină de iterat, o va seta pe true. Evident aceasta va face ca şi al doilea thread să încheie
bucla. La fiecare parcurgere a buclei, variabila string, currentName, este comparată cu firul aflat în
execuţie. Dacă este diferită, se realizează modificarea acestei variabile. Aceasta permite vizualizarea
accesării CPU a unui anumit thread.
Sincronizarea
Atunci când folosim mai multe fire de execuţie, este necesar uneori, să coordonăm fluxul logic,
acest proces numindu-se sincronizare. Cel mai simplu motiv pentru această sincronizare, este ca două
sau mai multe fire să aibă acces la o resursă comună, dar doar un singur fir să acceseze la un moment
dat acea resursă. De exemplu scrierea într-un fişier, efectuată din două fire de execuţie trebuie
controlată. Aceasta se realizează astfel: un fir este pus în stare de aşteptare până când firul care are
acces la resursă termină acţiunea, urmând ca firul suspendat să îşi reia execuţia.
Sincronizarea în Java este realizată prin intermediul conceptului de monitor, ce controlează
accesul la un obiect. Monitorul funcţionează implementând conceptul de blocare. Când un obiect este
blocat de un fir de execuţie, nici un alt fir de execuţie nu are acces la acel obiect. Când firul de execuţie
eliberează acel lock, obiectul devine disponibil pentru celelalte fire de execuţie.
Toate obiectele în Java au un monitor. Această caracteristică este implementată în limbaj. De
aceea toate obiectele pot fi sincronizate. Acest lucru se va realiza prin cuvântul cheie synchronized şi alte
câteva metode pe care toate obiectele le au. Există două metode prin care se poate sincroniza fluxul
logic al unui program.
Folosirea metodelor sincronizate
Se poate sincroniza accesul la o metodă folosind cuvântul cheie synchronized. Atunci când acea
metodă este apelată , firul de execuţie în care s-a făcut apelul intră în obiectul monitor, care blochează
obiectul. În timpul blocării, nici un alt thread nu poate intra în metodă, sau accesa orice metodă
sincronizată. Atunci când firul de execuţie revine din metodă, se realizează deblocarea.
Următorul program demonstrează folosirea acestei metode de sincronizare:
Child #2 starting.
Running total for Child #2 is 1
Child #1 starting.
Running total for Child #1 is 1
Running total for Child #2 is 3
Running total for Child #1 is 5
Running total for Child #2 is 8
Running total for Child #1 is 11
Running total for Child #2 is 15
Running total for Child #1 is 19
Running total for Child #1 is 24
Running total for Child #2 is 29
Sum for Child #1 is 29
Child #1 terminating.
Sum for Child #2 is 29
Child #2 terminating.
Evident, rezultatele pot varia în funcţie de maşină, sistem de operare, etc.
Blocul synchronized
Deşi crearea metodelor sincronizate, în cadrul claselor, este un mod eficient şi uşor de
sincronizare, nu funcţionează pentru orice caz. De exemplu, să presupunem că dorim accesul sincronizat
la o metodă care nu este declarată ca atare. Acest lucru se poate întâmpla în cazul în care clasa a fost
creată de altcineva şi nu avem acces la codul sursă. Pentru a accesa obiecte care nu au fost anterior
sincronizate, avem la dispoziţie blocuri sincronizate:
synchronized(object)
{
//instrucţiunile ce trebuie sincronizate
}
Aici object este referinţa către obiectul de sincronizat. Un bloc sincronizat asigură faptul că
apelul unei metode membră a obiectului object, va avea loc într-un monitor al firului de execuţie
apelant. De exemplu putem reedita programul mai sus considerând că metoda a fost declarată fără
cuvânt cheie synchronized:
În acest fel se asigură un calcul predictiv al sumei format din elementele lui sa.
Să considerăm următoarea situaţie. Un fir numit T execută o metodă sincronizată şi are nevoie
de o resursă R, care este temporar indisponibilă. Ce ar trebui să facă T? Dacă T intră într-o buclă de
aşteptare a lui R, blochează obiectul prevenind alte fire să o acceseze. Această abordare este
consumatoare de timp şi poate duce la deadlock-uri adică blocare permanentă. O altă soluţie ar fi ca T
să elibereze temporar controlul obiectului, permiţând altui fir să ruleze. Când R devine disponibilă, T
poate fi notificat şi îşi poate relua execuţia. Acest tip de abordare, presupune existenţa unei comunicări
între fire diferite. În Java aceasta se face prin metodele wait(), notify() şi notifyAll().
Aceste metode aparţin tuturor obiectelor, deoarece sunt implementate în clasa Object. Aceste
metode pot fi apelate dintr-o metodă sincronizată. Atunci când un thread este blocat temporar,
apelează wait(). Aceasta va face ca firul să intre în sleep, şi monitorul pentru acel obiect să fie eliberat,
permiţând altui fir să folosească obiectul. Mai târziu, firul ce era oprit, este trezit, atunci când un alt fir
care va intra în acelaşi monitor, apelează metoda notify(), sau notifyAll(). Un apel la metoda notify() va
reporni firul oprit.
Iată diversele forme ale metodei wait():
Ultimele două forme ale metodei semnifică aşteptarea până când apare o notificare sau până
când o perioada de timp, dată de parametrii, trece. Mai jos avem formele funcţiilor de notificare:
class TicTac
{
synchronized void tic(boolean running)
{
if(!running)
{ //opresc metoda
notify(); //anunţ celălalt thread
return;
}
System.out.print("Tic ");
notify(); //il las pe tac sa ruleze
try
{
wait(); // aştept pe tac
}
catch(InterruptedException exc)
{
System.out.println("Thread interrupted.");
}
}
synchronized void tac(boolean running)
{
if(!running)
{ // opresc pe tac
notify(); // anunţ pe tic
return; // adică celălalt fir
}
System.out.println("Tac");
notify(); // il las pe tic sa ruleze
try
{
wait(); // aştept ca tic sa ruleze
}
catch(InterruptedException exc)
{
System.out.println("Thread interrupted.");
}
}
}
class MyThread implements Runnable
{
Thread thread;
TicTac ttOb;
// un nou fir de execuţie
MyThread(String name, TicTac tt)
{
thread = new Thread(this, name);
ttOb = tt;
thread.start(); // pornesc firul
}
// încep execuţia firului
public void run()
{
if(thread.getName().compareTo("Tic") == 0)
{
// sunt in thread-ul tic
for(int i=0; i<5; i++) ttOb.tic(true);
//la sfarsit opresc tic
ttOb.tic(false);
}
else
{
// sunt in thread-ul tac
for(int i=0; i<5; i++) ttOb.tac(true);
//la sfarsit opresc tac
ttOb.tac(false);
}
}
}
class ThreadsDemo
{
public static void main(String args[])
{
TicTac tt = new TicTac();
MyThread mt1 = new MyThread("Tic", tt);
MyThread mt2 = new MyThread("Tac", tt);
try
{
mt1.thread.join();
mt2.thread.join();
}
catch(InterruptedException exc)
{
System.out.println("Main thread interrupted.");
}
}
}
În acest exemplu există trei clase, TicTac, MyThread, ThreadsDemo. Prima clasa, conţine
două metode sincronizate:
synchronized void tic(boolean running)
synchronized void tac(boolean running)
Acestea, au următoarea logică:
if(!running)
{ //opresc metoda
notify(); //anunţ celălalt thread
return;
}
System.out.print("Tic ");
notify(); //il las pe tac sa ruleze
wait(); // aştept pe tac
Dacă parametrul transmis nu este încă false (mai am drept să rulez metoda) atunci afișez Tic şi
dau dreptul lui Tac, urmând să aştept până când thread-ul ce rulează Tac, să mă înştiinţeze că a rulat în
monitor. Dacă parametrul running este false, atunci anunţ celălalt fir de execuţie şi părăsesc metoda.
Acelaşi lucru are loc şi pentru metoda tac, evident referitor la thread-ul Tic.
Clasa MyThread, ce implementează interfaţa Runnable, reprezintă cele două fire de execuţie
suport pentru metodele tic() şi tac(). În firul de execuţie pentru Tic se apelează de cinci ori metoda tic()
cu parametru true, şi ultima dată cu parametru false, pentru a permite deblocarea firului care executa
tac(). Acelaşi lucru este valabil viceversa pentru Tac.
În clasa ThreadsDemo se creează cele două fire de execuţie, se lansează şi se aşteaptă
terminarea lor. Iată ce se întâmplă la rularea programului:
Tic Tac
Tic Tac
Tic Tac
Tic Tac
Tic Tac
Dacă modificăm însă, metodele tac() şi tic() eliminând metodele de comunicare şi anume
wait() şi notify()rezultatul poate fi:
Uneori este util să suspendăm execuţia unui thread. De exemplu, un fir de execuţie poate fi
folosit pentru afişarea orei. Dacă utilizatorul nu doreşte vizualizarea orei, acest fir va fi suspendat. El va
putea fi repornit (resume) ulterior. Metodele ce realizează acest lucru sunt:
Totuşi folosirea acestora este îngrădită, de unele probleme cauzate de suspend() în trecut. De
exemplu, dacă un thread obţine un lock pe o zonă critică, şi el este suspendat, acele lock-uri sunt eterne.
În acest timp, alte fire de execuţie aşteaptă deblocarea lor. Metoda resume() este de asemenea
învechită. Nu cauzează probleme, dar nu poate fi folosită fără metoda suspend(). De asemenea metoda
stop() a fost considerată învechită.
Următorul exemplu reprezintă un mod în care se pot folosi aceste funcţii:
Clasa MyThread, defineşte două variabile booleane, suspended şi stopped. Metoda run()
conţine blocul sincronizat ce verifică variabila suspended. Dacă este true atunci metoda wait() este
apelată şi suspendă execuţia firului. Metoda care stabileşte valoarea variabilei la true, este
mysuspend(). Pentru a relua firul, avem myresume(), care setează variabila suspended pe true şi
apelează notify(), pentru a relua execuţia firului. Ultima metodă este mystop() şi conţine paşii care duc la
oprirea firului de execuţie. În clasa SuspendDemo, metodele descrise sunt apelate în alternanţă pentru
a opri/relua execuţia firului de câteva ori. Rezultatul rulării acestui program este:
My Thread starting.
1 2 3 4 5 6 7 8 9 10
11 12 13 14 15 16 17 18 19 20
21 22 23 24 25 26 27 28 29 30
31 32 33 34 35 36 37 38 39 40
Suspending thread.
Resuming thread.
41 42 43 44 45 46 47 48 49 50
51 52 53 54 55 56 57 58 59 60
61 62 63 64 65 66 67 68 69 70
71 72 73 74 75 76 77 78 79 80
Suspending thread.
Resuming thread.
81 82 83 84 85 86 87 88 89 90
91 92 93 94 95 96 97 98 99 100
101 102 103 104 105 106 107 108 109 110
111 112 113 114 115 116 117 118 119 120
Stopping thread.
My Thread exiting.
Main thread exiting.
Fiecare fir de execuţie este membru al unui grup de fire. Un grup de thread-uri oferă un
mecanism pentru colectarea multiplelor fire într-un singur obiect pentru a manipula aceste fire. De
exemplu, se pot suspenda toate firele printr-o singură instrucţiune.
Dacă vom crea un fir fără a specifica grupul din care face parte, automat va fi pus în acelaşi grup
cu cel al firului părinte. Acest grup se mai numeşte şi current thread group. Atunci când o aplicaţie este
pornită, Java creează un TreadGroup numit main. Pentru a crea un grup separat de acesta, explicit, avem
la îndemână următoarele construcţii:
theGroup = myThread.getThreadGroup();
Alte metode
Iată, spre exemplu, cum se poate stabili prioritatea maximă, atât pe fir cât şi pe grup:
class MaxPriorityDemo
{
public static void main(String[] args)
{