Lucrarea 1. Fire de execute I

 

Lucrarea 1. Fire de execute I. 1

1. Obiectivul lucrării 1

2. Noţiuni preliminare. 1

2.1. Stările unui fir de execuţie. 2

3. Crearea şi startarea firelor de execuţie. 3

4. Mecanisme de excludere mutuala şi sincronizare intre firele de execuţie. 5

4.1. Excluderea mutuala utilizând cuvântul cheie synchronized. 5

4.3. Sincronizarea intre firele de execuţie utilizând  metoda join() 6

4.4. Pachetul java.util.concurrent 6

5. Setarea priorităţilor firelor de execuţie. 7

6. Terminarea unui fir de execuţie. 7

7. Grupuri de fire de execuţie. 8

8. Swing si firele de execuţie. 8

Exerciţii 8

 

1. Obiectivul lucrării

 

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 .

 

2. Noţiuni preliminare

           

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. 

 

2.1. Stările unui fir de execuţie

 

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;

 

 

3. Crearea şi startarea firelor de execuţie

 

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.

 

 

4. Mecanisme de excludere mutuala şi sincronizare intre firele de execuţie

4.1. Excluderea mutuala utilizând cuvântul cheie synchronized

 

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.

 

4.2.. Sincronizarea firelor utilizând metodele wait(), notify() si notifyAll()

           

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();
}
}

4.3. Sincronizarea intre firele de execuţie utilizând  metoda join()

         

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.          

 

4.4. Pachetul java.util.concurrent

 

Î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/

 

 

5. Setarea priorităţilor firelor de execuţie

           

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.

 

6. Terminarea unui fir de execuţie

 

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.

 

7. Grupuri de fire de execuţie

 

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”);

           

 

8. Swing si firele de execuţie

 

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();
}});

 


Exerciţii

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:

 lab.scd.fire.thread     

Notiuni exemplificate: construirea unui fir utilizand clasa Thread, startarea unui fir, construirea unui fir daemon.

lab.scd.fire.runnable

Notiuni exemplificate: contruirea unui fir utilizand interfata Runnable.

lab.scd.fire.priority

Notiuni exemplificate: setarea prioritatilor firelor de executie

lab.scd.fire.join

Notiuni exemplificate: utilizarea metodei join() pentru a pune in asteptare un fir.

lab.scd.fire.sincronizare

Notiuni exemplificate: sincronizarea accesului la resurse comune folosind bloduri synchronized.

lab.scd.fire.sincronizareProducerConsumer

Notiuni exemplificate: folosirea metodelor wait() si notify() pentru blocarea si deplocarea firelor de executie.

lab.scd.fire.pipe_comm

Notiuni exemplificate: comunicarea intre fire utilizand pipe-uri.

lab.scd.fire.schedule

Notiuni exemplificate: planificarea pentru executie a firelor de executie.

lab.scd.fire.semafor

Notiuni exemplificate: utilizarea clasei java.util.concurrent.Semaphor introdusa in sdk 1.5.0 pentru sincronizarea firelor in accesul resurselor cumune.

lab.scd.fire.tpool

Notiuni exemplificate: utilizarea claselor din pachetul java.util.concurrent pentru a construi si utiliza un thread pool

lab.scd.fire.tpool2

Notiuni exemplificate: utilizarea claselor din pachetul java.util.concurrent pentru a construi si utiliza un thread pool.