2.1. Stările unui fir de execuţie
3. Crearea şi startarea firelor de execuţie
4. Mecanisme de excludere mutuala şi sincronizare
intre firele de execuţie
4.1. Excluderea mutuala utilizând cuvântul cheie
synchronized
4.3. Sincronizarea intre firele de execuţie
utilizând metoda join()
4.4. Pachetul java.util.concurrent
5. Setarea priorităţilor firelor de
execuţie
6. Terminarea unui fir de execuţie
7. Grupuri de fire de execuţie
8. Swing si firele de execuţie
Scopul acestei lucrări este de a prezenta modul de lucru cu fire de
execuţie (thread-uri) in Java şi
însuşirea tehnicilor de lucru cu
fire: crearea, startarea, oprirea firelor,
sincronizarea si excluderea mutuala între fire .
De multe ori este nevoie ca un program sa fie divizat in mai multe
componente care sa ruleze independent. Aceste componente se numesc fire de
execuţie (eng. Thread).
Un fir de execuţie reprezintă o secvenţa de instrucţiuni ce
se executa in interiorul unui proces. In figura 1.1. este ilustrat schematic un
program ce conţine doua fire de execuţie.
Figura.
1.1 Un program ce conţine doua fire de execuţie.
Un exemplu de aplicaţie in care firele de execuţie sunt
folosite este un browser de internet.
În momentul in care încărcaţi o pagină puteţi realiza scroll in pagina şi citi conţinutul acesteia
deşi nu toate imaginile au fost încărcate, procesul de încărcare
a acestora relizându-se în background
pe fire de execuţie separate.
Trebuie făcută distincţie între fire de execuţie
şi procese. Deşi ambele concepte implică execuţia în
paralel a unor secvenţe de cod există o serie de desebiri
fundamentale între procese şi fire de execuţie.
Procesele sunt entităţi independente ce se execută
independent şi sunt gestionate de către nucleul sistemului de
operare.
Firele de execuţie sunt secvenţe ale unui program (proces) ce
se execută aparent în paralel în
cadrul unui singur proces.
Un alt exemplu de aplicaţie în care este nevoie de fire de
execuţie este o aplicaţie de tip server ce oferă servicii unor
aplicaţii client. Dacă nu am avea la dispoziţie tehnologia
firelor de execuţie aplicaţia server ar fi obligată să
deservească cererile clienţilor în mod secvenţial în ordinea
sosirii cererilor acestora. Folosind fire de excuţie
aplicaţia server va putea deservi în paralel doi sau mai mulţi
clienţi, alocându-se pentru fiecare cerere (client) câte un fir de
execuţie. Un exemplu este un server HTTP ce transmite clienţilor
resurse de tip pagini web.
Un fir de execuţie se poate afla in una dintre următoarele 4
stări:
1. New – obiectul fir de execuţie a fost creat
dar înca nu a fost startat.
2. Runnable – Firul se afla in starea in care poate fi
rulat in momentul in care procesorul devine disponibil.
3. Dead – Calea normala prin care un fir se termina
este prin ieşirea din metoda run(). Se poate
forţa terminarea firului apelând metoda stop() dar nu se recomanda folosirea sa, fiind o metoda “deprecated” in Java2.
4. Blocked – Firul de execuţie este blocat si nu
poate fi rulat, chiar daca procesorul este disponibil.
Starea
blocat (blocked) prezintă cel mai mare interese.
Un fir poate intra in starea blocat următoarele situaţii:
-
in
cadrul firului s-a apelat metoda Thread.sleep(sec)
care determina blocarea firului pentru un număr specificat de secunde;
-
firul a
apelat o metoda sincronizata ce are monitorul acaparat de către un alt
fir;
-
a fost
apelata metoda wait();
-
firul
aşteaptă după o operaţie cu fluxuri de intrare –
ieşire;
După cum se ştie punctul de start al unei aplicaţii Java
este funcţia main. În momentul lansării în
execuţie a aplicaţiei aceasta va conţine un fir de execuţie
ce va executa această funcţie main. Aşadar
o aplicaţie java conţine cel puţin un fir de execuţie.
Pentru a crea fire de execuţie suplimentare in Java se pot utiliza
doua procedee: extinderea clasei Thread sau
implementarea interfeţei Runnable.
OBSERVATIE: O aplicaţie Java îşi termină execuţia în
momentul în care toate firele de execuţie şi-au terminat
execuţia (cu excepţia firelor de tip daemon
la care se va reveni ulterior pe parcursul laboratorului).
În continuare vor fi descrise cele două mecanisme java prin
intermediul cărora pot fi construite fire de execuţie în Java.
3.1. Extinderea clasei Thread
Un fir de execuţie poate fi construit plecând de la o clasă
definită de utilizator şi moştenind (extinzând) clasa Thread. Clasa Thread este o
clasă definită în cadrul pachetului java.lang
(unul dintre pachetele standard Java) şi această clasă
defineşte toate proprietăţile şi funcţionalităţile
unui fir de execuţie.
O clasă ce extinde clasa Thread devine
clasă de tip fir de execuţie astfel încât un obiect
instanţă al acestei clase va putea fi folosit pentru a lansa în
execuţie un fir.
class Sortare extends Thread
{
public
void run()
{
// activitatile desfasurate de catre firul de executie
…
}
}
O clasă de tip fir de execuţie va trebui sa suprascrie metoda run() (moştenită din cadrul clasei Thread). În cadrul acestei metode va trebui definită
secvenţă de instrucţiuni ce va fi executată de către
firul de execuţie.
În momentul lansării in execuţie a unui fir se va executa
secvenţa de instrucţiuni definită în cadrul metodei run().
Pentru a lansa în execuţie un fir se foloseşte metoda start()
(moştenită de asemenea din cadrul clasei Thread).
Această metodă este responsabilă cu iniţializarea tuturor
mecanismelor necesare pentru a putea executa un fir de execuţie, după
care va apela automat metoda run().
Sortare s1 = new Sortare();
s1.start();
OBSERVATIE: metoda run() nu trebuie apelata in
mod explicit de către programator pentru că aceasta va fi
apelată în mod automat atunci când firul de execuţie este lansat în
execuţie folosind metoda start().
OBSERVATIE: un fir de execuţie o data startat
nu va mai putea fi startat înca
odata. Daca se încearcă apelarea metodei start()
pentru un fir deja pornit, se va genera o eroare.
3.2. Implementarea interfeţei Runnable
Există situaţii în care nu poate fi
extinsă clasa Thread pentru a construi un fir de
execuţie (un exemplu este situaţia în care o clasa ce trebuie
definită ca fir de execuţie moşteneşte deja o altă
clasă – având în vedere faptul că Java nu acceptă moştenire
multiplă suntem puşi în imposibilitatea de a folosi mecanismul
extinderii clasei Thread pentru a construi firul de
execuţie).
Cel de al doilea mecanism de creare a unui fir de
execuţie este prin implementarea interfeţei Runnable.
class Sortare implements
Runnable{
private
Thread thread=null;
public
void start(){
if(thread==null)
{
thread = new Thread(this);
thread.start();
}
}
public
vod run()
{
//activitatile
desfasurate de catre firul
de executie
…
}
}
Startarea firului de execuţie se face în mod similar ca
şi în cazul extinderii clasei Thread.
Sortare s1 = new Sortare();
s1.start();
Se observă că în cazul implementării interfeţei Runnable, utilizatorul defineşte propria metodă
start(), în cadrul căreia se construieşte un obiect de tip Thread, ce va primi ca argument în cadrul constructorului o
referinţă către obiectul curent, obiect curent ce conţine
metoda run(). În momentul apelării metodei
start() din cadrul obiectului de tip Thread, se va executa
metoda run() din cadrul obiectului transmis ca
argument la crearea acestuia.
Unul dintre aspectele cele mai importante de care trebuie ţinut
cont atunci când se lucrează cu fire de execuţie este modul in care
acestea accesează resursele comune. Astfel in momentul in care doua sau
mai multe fire împart o resursa comuna acestea trebuie sa î-şi
sincronizeze accesul la acea resursa.
Un exemplu de astfel de situaţie este cel al firelor consumator\producator. Un fir introduce date intr-un buffer, iar cel de al doilea citeşte datele puse de
primul fir in buffer. Evident cele doua fire nu
trebuie sa acceseze simultan bufferul si trebuie sa
aibă un acces sincronizat la acesta.
Prevenirea coliziunii in java se face utilizând metodele sincronizate. O
metoda este sincronizata atunci când are in fata cuvântul cheie synchronized.
synchronized void
f() { /* ... */ }
synchronized void g(){ /* ... */ }
Numai un singur fir poate accesa la un moment dat o metoda sincronizata
a unui obiect. Fiecare obiect are inclus (automat) un monitor sau un
zăvor. In momentul in care o metoda sincronizata a unui obiect este
apelata, monitorul este achiziţionat de către firul care a accesat
metoda. Atâta timp cat metoda sincronizata este in execuţie nici o alta
metoda sincronizata nu va mai putea fi apelata de către un alt fir.
Observaţie: In momentul in care un fir încearcă sa acceseze o
metoda sincronizata a unui obiect care are monitorul acaparat de către un
alt fir, acesta se blochează in aşteptarea eliberării
monitorului.
Metodele wait() si notify()
sunt utilizate pentru a bloca şi debloca firele de execuţie. O
observaţie importantă referitoare la aceste două metode este ca
ele fac parte din clasa Object. Motivul acestei
poziţionări este pentru că aceste două metode manipulează
monitoarele obiectelor, si, la rândul lor, monitoarele se găsesc la
nivelul oricărui obiect.
Metodele wait si notify
pot fi apelate doar din interiorul unor
blocuri sau metode sincronizate. Ceea ce inseamnă
ca aceste metode, pentru un obiect data, pot fi apelate doar de către
deţinătorul monitorului obiectului.
In momentul in care un fir este blocat prin apelarea metodei wait() monitorul deţinut de respectivul fir este
eliberat.
Observaţie: Metoda wait(milis) poate fi utilizata in locul metodei Thread.sleep(milis), avantajul
fiind ca firul poate fi deblocat înainte de expirarea timpului de
aşteptare (ceea ce in cazul sleep() nu este
posibil).
Observaţie: Nu
utilizaţi metodele stop() si suspend() deoarece folosirea acestora
in cadrul unor blocuri sincronizate duce la deadlok
întrucât monitorul nu este eliberat. In listingul
următor este prezentat o scurta secvenţa care determina deadlock-ul.
class oclasa
{ //...
synchronized void f()
{ Thread.currentThread().stop();
}
}
Metoda join() aparţine clasei Thread
si este utilizata pentru a determina un fir de execuţie să
aştepte terminarea unui alt fir de execuţie.
Începând cu versiunea 1.5 a Java Standard Development
Kit, a fost introdus un set de pachete utilitare
denumite „ Concurrency Utilities
packages”. Aceste pachete conţin clase pe care
programatorul le poate folosi pentru a implementa diferite mecanisme de
comunicare, sincronizare şi gestionare a firelor de execuţie. Cele
mai multe dintre aceste clase utilitare se regăsesc în pachetul java.util.cuncurrent. Câteva dintre funcţionalităţile
noi sunt:
-
Obiecte
de tip Lock – lucrează într-o manieră
similară cu monitoarele implicite folosite de blocurile sincronizate. La
fel ca în cadrul monitoarelor implicite, doar un singur fir poate
achiziţiona un Lock.
Acestea suportă de asemenea wait/notify. Unul dintre principalele avantaje al
obiectelor Lock este posibilitatea de a verifica
disponibilitatea unui monitor înainte de a încerca achiziţionarea
acestuia.
-
Variabile
atomice – a fost introdus un set de clase ce permite manipularea în mod atomic
a tipurilor primitive şi a tipurilor referinţă.
-
Obiecte
de tip Executor – ce permit planificarea pentru execuţie ulterioară a
task-urilor şi mecanisme pentru lucrul cu threadpool-uri.
Referinţe suplimentare cu privire la facilităţile
introduse se găsesc la adresa: http://java.sun.com/developer/technicalArticles/J2SE/concurrency/
Prioritatea unui fir de execuţie spune planificatorului de
execuţie a firelor cat de important este acesta. Aceasta insemnă că dacă mai multe fire de execuţie
sunt blocate in aşteptarea execuţiei, planificatorul îl va alege
prima data pe cel cu prioritate mai mare. Aceasta nu înseamnă ca firele cu
prioritate mai mica nu vor avea alocat timp din procesor.
Pentru a seta prioritatea unui fir se utilizează metoda setPriority(), iar pentru obţinerea
priorităţii unui fir se utilizează metoda getPriority() (metodele fac parte din clasa Thread)
. Firele pot primi priorităţi intre valorile MIN_PRIORITY si MAX_PRIORITY (constante definite în clasa Thread).
Observaţie: Programatorul
nu trebuie sa se bazeze pe priorităţi in construirea programului,
întrucât acesta poate da rezultate diferite pe sisteme diferite.
O deosebita atenţie trebuie data modului in care un fir de
execuţie se termina. Terminarea unui fir de execuţie utilizând metoda
stop() nu este recomandata, de altfel, aceasta metoda este “deprecated”
(nu mai este recomandată de către Sun pentru a fi utilizată).
Calea recomandata de terminare a unui fir este prin revenirea normala ( return ) din run(). Aşadar
in cadrul metodei run se recomandă
adăugarea unor blocuri condiţinale care
să determine terminarea firului de execuţie, respectiv ieşirea
din metoda run.
Acest mecanism nu este util de fiecare data deoarece in momentul in care
un fir este blocat ( datorita unor instrucţiuni wai(),
sleep(), join(), operatii I/O ), acesta nu poate verifica nici o
condiţie. Pentru a termina un fir care este blocat se utilizează
metoda interrup(). Aceasta metoda determina aruncarea
unei excepţii InterruptedException care trebuie
prinsa.
Orice fir de execuţie aparţine unui grup (thread
grup). Acesta poate fi grupul default sau poate
aparţine unui grup specificat in mod explicit in momentul creierii
acestuia (Obs. Un fir o data creat nu poate trece dintr-un grup in alt grup).
Daca nu este specificat atunci grupul implicit din care face parte un fir este
grupul sistem. De asemenea la rândul lor noile grupuri create sunt grupate
ierarhic, si in vârf se afla acelaşi grup sistem.
Lucrul cu grupuri de fire este util atunci când se doreşte ca
printr-o singura comanda sa se controleze toate firele aparţinand
unui grup. Crearea unui grup se face utilizând clasa ThreadGrup.
ThreadGroup
tg = new ThreadGroup(“myg”);
Metodele claselor swing nu sunt thread-safe,
aceasta înseamnă ca nu sunt proiectate pentru cazul in care sunt apelate
simultan de către mai multe fire de execuţie. Aşadar o data startat firul de tratare a evenimentelor Swing (apeland setVisible(), pack() sau orice alta metoda ce face vizibila o fereastra)
nu mai poate fi apelată nici o metodă a claselor Swing
in siguranţă de către mai multe fire. Pentru că un fir
să modifice o componenta se utilizează metoda invokeLater()
astfel:
Swingutilities.invokeLater( new Runnable()
{ public void run()
{ some_window.repaint();
}});
Importaţi în
mediul Eclipse proiectul ce exemplifică noţiunile prezentate în acest
laborator (link proiect*).
In cadrul proiectului java se regăsesc următoarele
pachete:
Notiuni exemplificate: construirea unui fir utilizand clasa
Thread, startarea
unui fir, construirea unui fir daemon.
Notiuni exemplificate: contruirea unui
fir utilizand interfata Runnable.
Notiuni exemplificate: setarea prioritatilor
firelor de executie
Notiuni exemplificate: utilizarea metodei join() pentru a pune in asteptare un fir.
Notiuni exemplificate: sincronizarea accesului la resurse comune folosind bloduri
synchronized.
Notiuni exemplificate: folosirea metodelor wait() si notify() pentru
blocarea si deplocarea firelor
de executie.
Notiuni exemplificate: comunicarea intre fire utilizand pipe-uri.
Notiuni exemplificate: planificarea pentru executie a firelor
de executie.
Notiuni exemplificate: utilizarea clasei java.util.concurrent.Semaphor introdusa in sdk
1.5.0 pentru sincronizarea firelor in accesul resurselor cumune.
Notiuni exemplificate: utilizarea claselor din pachetul java.util.concurrent
pentru a construi si utiliza un
thread pool