Sunteți pe pagina 1din 7

Recapitulare noţiuni fundamentale ale limbajului de programare Java

Caracteristici principale
• Simplitate
Elimină moştenirea multiplă şi supraîncărcarea operatorilor.
• Robusteţe
Elimină pointerii şi realizează managementul automat al memoriei prin utilizarea mai multor
tipuri de garbage collector.
• Complet orientat obiect
• Securitate
Verificarea la run-time a bytecodului (bytecode verifier) pentru a preveni secvenţe ilegale de cod
construite de compilatoare ostile, clase semnate digital, API-uri pentru criptografie, API-uri
pentru autentificare şi controlul accesului, comunicaţii securizate, instrumente pentru
managementul cheilor de criptare şi a certificatelor (PKI – public key infrastructure)
• Neutru din punct de vedere arhitectural
• Portabilitate
Independent de platforma de lucru.
• Limbaj compilat şi interpretat
• Proiectat pentru a fi folosit în medii de calcul distribuite şi sisteme deshise

Thread-uri
În programarea concurentă există două unităţi de bază ale execuţiei: procese şi thread-uri.

Un proces reprezintă un mediu de execuţie care dispune de un set complet şi privat de resurse run-time
cât şi de propriul spaţiu de memorie.

Thread-urile (fire de execuţie), denumite şi procese lightweight, există în cadrul proceselor. Un thread
furnizează un mediu de execuţie care are acces la resursele procesului părinte, inclusiv la memorie şi la
fişierele deschise. Fiecare proces conţine cel puţin un thread.

În cadrul unui proces se pot defini şi executa simultan mai multe thread-uri, permitând astfel execuţia
concurentă a taskurilor în cadrul programelor.

1
Un thread poate fi asemănat cu o versiune redusă a unui proces, ambele rulând simultan şi independent
pe o structură secventială de execuţie a instrucţiunilor. De asemenea, execuţia simultană a thread-urilor
în cadrul unui proces este similară cu execuţia concurentă a proceselor: sistemul de operare alocă ciclic
cuante din timpul procesorului fiecărui proces/thread pâna la terminarea acestora.

Deosebirea majoră dintre un proces şi un thread constă în faptul că thread-urile nu pot rula decât în
cadrul unui proces. O altă deosebire rezultă din faptul că fiecare proces are propria sa memorie (propriul
său spaţiu de adrese), iar la crearea unui nou proces (fork) este realizată o copie exactă a procesului
părinte: cod + date. Prin contrast, la crearea unui thread nu este copiat decât codul procesului părinte.
Prin urmare, toate thread-urile au acces la aceleaşi date (datele procesului părinte). Un thread mai
poate fi privit şi ca un context de execuţie în cadrul procesului părinte.

Thread-urile sunt utile în multe privinţe, însa uzual ele sunt folosite pentru executarea unor operaţiuni
consumatoare de timp (calcule matematice, aşteptarea eliberării unei resurse etc) fără a bloca procesul
principal sau interfaţa grafică cu utilizatorul.

Crearea unui thread


Un thread este reprezentat de un obiect (o instanţă a unei clase). Thread-urile definite de o clasă vor
avea acelaşi cod şi prin urmare aceeaşi secvenţă de instrucţiuni.

În limbajul de programare Java, crearea unei clase care să definească un thread poate fi facută prin două
modalităti:
• prin extinderea clasei java.lang.Thread
• prin implementarea interfeţei java.lang.Runnable

Orice clasă ale cărei instanţe vor conţine cod pentru fire de execuţie trebuie declarată ca fiind
Runnable. Aceasta este o interfaţă care conţine o singură metodă: public void run(). Prin
urmare, orice clasă ce descrie thread-uri va conţine o metodă run() în care este implementat codul ce
va fi executat de către firul de execuţie.

Cea mai importantă clasă care implementează interfaţa Runnable este clasa Thread. Aceasta
implementează un fir de execuţie generic care nu conţine nici o instrucţiune (metoda run() este
goală).

Important: Orice fir de execuţie este reprezentat de o instanţă a clasei Thread sau a unei subclase a
acesteia.

2
Extinderea clasei java.lang.Thread
Cea mai simplă metodă de a crea un fir de execuţie este prin extinderea clasei Thread şi suprascrierea
metodei run() a acesteia. Formatul general al unei astfel de clase este:

public class SimpleThread extends Thread {


public SimpleThread(String nume) {
super(nume);
}
public void run() {
// codul firului de execuţie
}
}

Constructorul clasei primeşte ca argument un String ce va reprezenta numele firului de execuţie. În


cazul în care nu se doreşte specificarea unui nume pentru firul de execuţie atunci se poate renunţa la
definirea acestui constructor şi se va folosi doar constructorul implicit (fără argumente) care creeaza un
fir de execuţie cu un nume generat automat. Numele firului de execuţie poate fi schimbat ulterior prin
apelul metodei setName().

Orice alţi constructori pot definiţi, aceştia fiind utili pentru iniţializarea diverşilor parametri aferenţi
firului de execuţie.

Metoda run() este "inima" oricărui fir de execuţie şi conţine codul pe care acesta trebuie să îl execute.

Un fir de execuţie nou creat (prin instanţierea clasei aferente) nu este lansat automat în execuţie. Acest
lucru se realizează prin apelul metodei start(), definită de clasa Thread.

// creeaza un fir de execuţie cu numele MyThread


SimpleThread t = new SimpleThread("MyThread");
// lansează thread-ul în execuţie
t.start();

Implementarea interfeţei java.lang.Runnable


Implementarea interfeţei Runnable este utilă în cazul în care clasa care urmează a defini fire de
execuţie este deja derivată dintr-o altă clasă diferită de clasa Thread. Cum limbajul de programare Java
nu permite moştenire multiplă, soluţia rămasă este implementarea directă a interfeţei Runnable.

class MyThread extends Parent, Thread // ilegal

Clasa Thread implementează ea însăşi interfaţa Runnable şi, din acest motiv, la extinderea ei
obţineam o implementare implicită a interfeţei. Interfaţa Runnable permite unei clase să specifice
codul unui fir de execuţie (în metoda run() a interfeţei), fără a extinde clasa Thread.

3
Interfaţa Runnable se găseste în pachetul java.lang şi este definită astfel:

public interface Runnable {


public abstract void run();
}

Prin urmare, o clasă care defineşte fire de execuţie prin implementarea interfeţei Runnable trebuie
obligatoriu să implementeze metoda run().

Formatul general al unei clase care implementează interfaţa Runnable este:

public class MyThread implements Runnable {


public void run() {
// codul firului de execuţie
}
}

O clasă care implementează interfaţa Runnable poate fi derivată din orice clasă. Lansarea în execuţie a
unui thread folosind metoda implementării interfeţei Runnable se face după cum urmează:

// creeaza un obiect ce conţine implementarea metodei run()


MyThread t = new MyThread();
// creează şi lansează thread-ul în execuţie
new Thread(t).start();

Observăm că de această dată folosim un alt constructor al clasei Thread pentru a instanţia un fir de
execuţie. Acest constructor primeşte ca parametru o instanţă a unei clase care implementează interfaţa
Runnable.

Simpla instanţiere a unei clase care implemeneaza interfaţa Runnable nu creeaza nici un fir de
execuţie. Specificarea instanţei Runnable în constructorul clasei Thread determină crearea unui fir
de execuţie care la lansarea sa (cu metoda start()) va executa metoda run() a instanţei
Runnable. Acest constructor acceptă ca argument orice instanţă a unei clase ce implementează
interfaţa Runnable. Metoda run() nu trebuie apelată explicit, acest lucru realizându-se automat la
apelul metodei start().

Apelul explicit al metodei run() nu reprezintă nici o eroare, însa metoda va fi executată ca orice altă
metodă obişnuită, fără a se crea nici un fir de execuţie.

4
Ciclul de viaţă al unui thread
Fiecare thread are propriul său ciclu de viaţă: este creat, devine activ prin lansarea sa în execuţie şi, la un
moment dat, se termină. Diagrama de mai jos ilustrează generic stările în care se poate găsi un fir de
execuţie precum şi metodele care declanşează tranziţia dintr-o stare în alta:

Un fir de execuţie se poate găsi în una din următoarele patru stări:


• New Thread
• Runnable
• Not Runnable
• Dead

Starea "New Thread"


Un fir de execuţie se găseşte în această stare imediat după crearea sa, cu alte cuvinte, după instanţierea
unui obiect din clasa Thread sau dintr-o subclasă a acesteia.

Thread t = new Thread(runnableObject);


// t se găseşte în starea "New Thread"

În această stare firul de execuţie nu are alocate nici un fel de resurse sistem. Lansarea propriu-zisă în
execuţie se face prin apelul metodei start().

Starea "Runnable"
După apelul metodei start() un fir de execuţie va trece în starea "Runnable", adică se află în
execuţie.

t.start();
// t se găseşte în starea "Runnable"

Metoda start() realizează următoarele operaţiuni necesare rulării firului de execuţie:


• alocă resursele sistem necesare
• planifică threadul la procesor pentru a fi lansat în execuţie
• apelează metoda run()a obiectului ce reprezintă firul de execuţie

5
Un fir de execuţie aflat în starea "Runnable" nu presupune neapărat că acesta se găseste efectiv în
execuţie, adică instructiunile sale sunt executate de procesor. Acest lucru se întâmplă din cauză că
majoritatea calculatoarelor au un singur procesor iar acesta nu poate rula simultan toate firele de
execuţie care se găsesc în starea "Runnable". Pentru a rezolva această problemă, interpretorul Java
implementează o planificare care partajează dinamic şi corect procesorul între toate firele de execuţie
care sunt în starea "Runnable". Prin urmare, un fir de execuţie care "rulează" poate să îsi aştepte de
fapt rândul la procesor.

Starea "Not Runnable"


Un fir de execuţie ajunge în acestă stare în una din următoarele situaţii:
• devine "adormit" prin apelul metodei sleep() a clasei Thread
• a apelat metoda wait() a clasei Object, asteaptând ca o anumită condiţie să fie satisfăcută
• este blocat într-o operaţiune de intrare/ieşire

În intervalul de timp în care un fir de execuţie "doarme", acesta nu va fi executat chiar dacă procesorul
este disponibil. După expirarea acestui interval threadul revine în starea "Runnable", iar dacă procesorul
este disponibil îsi va relua execuţia.

Pentru fiecare tip de intrare în starea "Not Runnable", există o secvenţă specifică de ieşire care readuce
firul de execuţie în starea "Runnable":
• daca un thread a fost "adormit", el redevine "Runnable" după scurgerea intervalului de timp
specificat la apelul metodei sleep().
• daca un thread aşteaptă o anumită condiţie (wait()), atunci un alt thread trebuie să îl
informeze când acea condiţie a fost îndeplinită, moment în care se revine în starea "Runnable";
acest lucru se realizează prin apelul metodelor notify() sau notifyAll() din clasa
Object.
• dacă un thread este blocat într-o operaţiune de intrare/ieşire atunci el redevine "Runnable"
atunci când acea operaţiune s-a terminat.

Starea "Dead"
Este starea în care ajunge un fir de execuţie la terminarea sa. Un fir de execuţie trebuie să se termine în
mod natural prin terminarea metodei run() pe care o execută. Deşi clasa Thread pune la dispoziţie
metoda stop(), metodă ce forţează terminarea unui fir de execuţie, apelul acesteia este
nerecomandat deoarece există riscul de a lăsa descoperite obiecte în stari inconsistente.

Sincronizarea firelor de execuţie


Un segment de cod ce gestionează o resursă comună mai multor de fire de execuţie concurente se
numeşte secţiune critică.
În Java o secţiune critică poate fi un bloc de instrucţiuni sau o metodă.
Controlul accesului într-o sectiune critică se face prin cuvântul cheie synchronized.

6
Platforma Java asociază un monitor fiecărui obiect al unui program ce contine secţiuni critice care
necesită sincronizare. Acest monitor indică dacă resursa critică este accesată de vreun fir de execuţie
sau este liberă, cu alte cuvinte "monitorizează" resursa critică.

În cazul în care o resursă critică este accesată, accesul altor fire de execuţie la aceasta este restricţionat.
În momentul în care resursa este eliberată, accesul celorlalte fire de execuţie este permis.

În momentul în care un thread apelează o metodă sincronizată a unui obiect, acesta va obţine monitorul
obiectului în cauză blocând astfel accesul altor fire de execuţie la metodele sincronizate ale obiectului
respectiv. Acest lucru este logic deoarece mai multe sectiuni critice (metode sincronizate) ale unui
obiect gestionează de fapt o singură resursă critică.

Punerea unui fir de execuţie în aşteptare se realizează cu metoda wait() a clasei Object, care are
trei forme:

void wait()
void wait(long timeout)
void wait(long timeout, long nanos)

După apelul metodei wait(), firul de execuţie curent eliberează monitorul asociat obiectului şi
aşteaptă ca una din următoarele condiţii să fie îndeplinită:
• un alt thread informează thread-urile care "aşteaptă" la un anumit monitor să se "trezească";
acest lucru se realizează prin apelul metodelor notify() sau notifyAll() din clasa
Object.
• perioada de aşteptare specificată a expirat.

Metoda notifyAll() informează toate firele de execuţie care sunt în aşteptare la monitorul
obiectului curent îndeplinirea condiţiei.
Metoda notify() informează doar un singur fir de execuţie (din mai multe posibile fire de execuţie
care aşteaptă). Acest fir de execuţie este ales în mod arbitrar.

Aplicaţie de laborator
Scrieţi un program care să evidenţieze terminarea naturală a unui thread folosind o variabilă de control
a execuţiei thread-ului.

Scrieţi un program care să evidenţieze prioritatea diferită a două thread-uri (se va folosi metoda
setPriority() a clasei Thread). De exemplu un thread va afişa pe ecran X, iar celălalt O.

Scrieţi un program care să simuleze un proces producător-consumator prin intermediul unei resurse
comune sincronizate. Folosiţi metodele wait() şi notify() ale clasei Object pentru a garanta
ordinea corectă de acces asupra resursei critice.

S-ar putea să vă placă și