Laborator 1
- Recapitulare utilizare fire de executie (thread-uri) in Java
- Crearea si pornirea unui thread
- Interactiunea cu un thread
- Oprirea completa a unui thread
- Alte exemple
- Recapitulare notiuni de baza pentru sincronizare in Java
Versiunea TLDR - Sumar notiuni laborator
Recapitulare utilizare fire de executie (thread-uri) in Java
O descriere informala a unui thread (fir de executie) ar fi o serie secventiala de instructiuni executate in cadrul unui proces. O aplicatie are cel putin un thread - threadul principal (main thread). In momentul in care o aplicatie Java este lansata in executie, aceasta implica si lansarea altor thread-uri pentru diverse operatii, precum managementul memoriei, notificari de evenimente, etc, dar acestea sunt de regula transparente din punctul de vedere al programatorului. Cand programam o aplicatie in Java, thread-ul principal este cel ce corespunde functiei main incheiat odata cu aceasta si implicit cu executia programului.
Crearea si pornirea unui thread
Exista doua posibilitati principale pentru crearea unui thread in Java. Prima este prin extinderea clasei java.lang.Thread:
public class MyThread extends Thread { public void run (){ System.out.println("This is my thread!"); } }
Codul ce va fi executat de noul thread (in paralel cu thread-ul ce il porneste pe acesta) este continut in metoda run(). Crearea thread-ului este realizata prin instantierea unui nou obiect din clasa ce extinde Thread. Aceasta nu porneste insa si executia thread-ului. Pentru a lansa executia este necesar apelul metodei start(). Se observa ca metoda run() nu se apeleaza in mod direct:
MyThread newThread = new MyThread(); newThread.start();
A doua posibilitate de a crea un thread este prin implementarea interfetei Runnable (care de altfel este implementata si de clasa Thread):
public class MyThread implements Runnable { public void run (){ System.out.println("This is my thread!"); } }
Singura metoda ce necesita implementare obligatorie este tot run() ca in cazul precedent. Pentru a porni executia dupa instantierea unui nou obiect, din nou metoda apelata va fi start() din clasa Thread. Pentru aceasta o instanta Thread trebuie creata pe baza instantei Runnable:
MyThread newThread = new MyThread(); Thread actualThread = new Thread(newThread); actualThread.start();
Interactiunea cu un thread
O instanta Thread sau a unei clase derivate din aceasta poate fi tratata ca orice alt obiect in ce priveste apelul metodelor din clasa respectiva. De exemplu se pot defini metode de tip getter/setter pentru diversi membri adaugati in clasa, se pot seta proprietati specifice deja existente in clasa Thread (ex., numele thread-ului), etc. De subliniat insa ca apelul unei metode dintr-o instanta Thread se va executa in thread-ul apelant, ca pentru orice alt obiect, si nu in cadrul executiei thread-ului pentru care e apelata metoda.
SomeThread someThread = new SomeThread(); someTread.setSomeStuff(); // <-- aceasta se executa in thread-ul curent; // obiectul someThread este modificat // dar modificarea in starea obiectului // nu e executata de obiectul thread modificat in sine
Pentru a verifica daca un thread se afla inca in executie dupa ce a fost pronit se poate apela metoda isAlive(). Aceasta va returna true in cazul in care instanta Thread pentru care e apelata nu a finalizat executia metodei run().
SomeThread someThread = new SomeThread(); someThread.start(); if (someThread.isAlive()) { System.out.println("someThread is not dead yet"); }
Oprirea temporara a unui thread poate fi realizata prin apelul sleep() avand ca parametru durata dorita pentru oprire. Aceasta metoda trebuie apelata de thread-ul ce se doreste a fi oprit si nu poate fi apelata pentru acesta de catre un alt thread. O alta metoda pentru intreruperea temporara a executiei unui thread este yield(). Ca si sleep() si aceasta trebuie apelata de thread-ul ce se va opri, dar efectul consta doar intr-o cedare de prioritate, lasand in fata alte thread-uri ce asteapta sa fie preluate de catre procesor pentru executie.
Un thread poate astepta in mod explicit ca un alt thread sa finalizeze metoda sa run() prin apelul join() pe instanta thread-ului asteptat.
SomeThread someImportantThread = new SomeThread(); someImportantThread.start(); someImportantThread.join(); // <-- asteptam in thread-ul curent pentru finalizarea executiei someImportantThread
Oprirea completa a unui thread
Cea mai simpla metoda de a opri complet un thread este prin setarea unui flag verificat periodic in cadrul metodei run(), pe baza acestuia thread-ul oprindu-si propria executie. (Aceasta abordare poate fi utilizata si pentru a altera comportamentul thread-ului intr-un alt mod dintr-un context extern, de exemplu pentru a-l notifica sa se intrerupa temporar printr-un apel sleep).
public class MyThread extends Thread { boolean alive = true; // <-- aici poate sa apara o problema // care e, si cum s-ar corecta? public void run (){ while (alive) { System.out.println("spending time doing nothing..."); } } public void killThread() { alive = false; } } ... MyThread lazyThread = new MyThread(); lazyThread.start(); lazyThread.killThread(); // <-- la urmatoarea iteratie in cadrul run // dupa ce alive este setat la false lazyThread se va opri
O alta modalitate prin care s-ar putea opri un thread este apelul interrupt() pentru instanta thread-ului. Acest apel seteaza un flag intern pentru thread ce poate fi verificat de catre thread in sine prin metoda isInterrupted(). In plus, apelul interrupt() face ca oricare metoda blocanta precum sleep() sau join() sa isi opreasca executia si sa arunce o exceptie.
public class MyThread extends Thread { public void run (){ while (!isInterrupted()) { System.out.println("spending time doing nothing..."); Thread.sleep(10); } } } ... MyThread lazyThread = new MyThread(); lazyThread.start(); lazyThread.interrupt(); // <-- daca acest apel are loc intr-un moment in care metoda sleep este in executie // va fi aruncata o exceptie; daca are loc la un alt moment atunci // flag-ul intern de intrerupere va fi setat // si la urmatoarea iteratie thread-ul isi va inceta executia
Alte exemple
- PrinterThread - cod pentru un thread ce afiseaza litere dintr-un string
- SumThread - cod pentru un thread ce calculeaza o suma pe un interval
- Test - cod ce include cazuri de test pentru exemplele de mai sus
Recapitulare notiuni de baza pentru sincronizare in Java
Specificatorul "synchronized"
In java fiecare instanta de obiect are asociat un lock intern (monitor). Acest lock poate fi utilizat de thread-uri pentru a se sincroniza la accesul obiectului prin utilizarea specificatorului synchronized. Exista doua moduri principale de utilizare a acestuia.
Prima posibilitate este prin declararea metodelor din clasa obiectului ca synchronized. Cand un thread va executa o metoda synchronized va obtine un lock pentru obiectul respectiv. Orice alt thread ce va inceca sa execute orice metoda synchronized pe aceeasi instanta de obiect va astepta pana ce thread-ul curent va elibera lock-ul in urma finalizarii apelului metodei executate.
public class SharedObject { int x = 0; public synchronized int get() { return x; } public synchronized void inc() { x++; } } public class MyThread extends Thread { SharedObject shared; public MyThread(SharedObject shared) { this.shared = shared; } public void run (){ int i = 0; while (i < 100) { shared.inc(); // <------------------------------------------------------------| i++; // Thread.sleep(10); // | // | } // | } // | // | } // | // | ... // | // | SharedObject myShared = new SharedObject(); // | MyThread thread1 = new MyThread(myShared); // | MyThread thread2 = new MyThread(myShared); // | // | thread1.start(); // <---Cand aceste thread-uri vor executa acest apel thread2.start(); // <-/ unul din ele va obtine lock-ul intern pentru myShared // daca acesta este liber, sau va astepta pana la eliberarea acestuia
A doua modalitate de utilizare a synchronized este prin specificarea explicita a obiectului pentru care lock-ul este cerut si sectiunea de cod pentru care este mentinut. Aceasta modalitate poate fi utila fie pentru sincronizarea accesului doar pentru o parte a codului dintr-o metoda, sau pentru folosirea de lock-uri diferite pentru diferite portiuni de cod. Utilizarea de lock-uri diferite pentru diferite portiuni de cod din aceeasi clasa prin intermediul synchronized e posibila prin folosirea de lock-uri ce corespund altor obiecte decat cel curent. Aceasta poate permite unor grupuri diferite de thread-uri sa execute simultan sectiuni de cod diferite, dar sa se sincronizeze pentru o anumita sectiune care necesita acces exclusiv.
public class SharedObject { int x = 0; int y = 0; SomeObject lockObject = new SomeObject(); //<-- nu conteaza implementarea clasei in acest exemplu // ne intereseaza doar lock-ul intern public synchronized void inc() { //<-- sincronizare prin intermediul lock-ului asociat x++; // obiectului curent } public void dec() { //<-- nesincronizat ca metoda integrala, thread-urile pot incepe // executia dec in timp ce un alt thread executa inc int i = 0; synchronized(lockObject) { //<-- un thread ce va ajunge aici obtine lock-ul pentru lockObject // daca este disponibil; daca lock-ul nu este disponibil y--; // thread-ul va astepta pana la momentul respectiv; // pentru ca lock-ul este pentru un alt obiect, aceasta executie } // nu blocheaza alte thread-uri sa execute inc() unde metoda e // accesata in functie de obtinerea lock-ului instantei curente while (i < 100000) { //<-- putem obtine un avantaj prin acest mod de sincronizare // pentru a mentine aceasta portiune de cod nesincronizat; System.out.println("spending long time"); // daca ar fi sincronizat, un thread ar bloca pe restul ca sa Thread.sleep(10); // modifice variabila partajata y pentru un timp lung in i++; // mod inutil in contextul in care y nu e accesat aici } } } public class MyThreadInc extends Thread { SharedObject shared; public void MyThread(SharedObject shared) { this.shared = shared } public void run (){ int i = 0; while (i < 100) { shared.inc(); i++; Thread.sleep(10); } } } public class MyThreadDec extends Thread { SharedObject shared; public void MyThread(SharedObject shared) { this.shared = shared } public void run (){ int i = 0; while (i < 100) { shared.dec(); i++; Thread.sleep(10); } } } ... SharedObject myShared = new SharedObject(); MyThreadInc thread1 = new MyThreadInc(myShared); MyThreadInc thread2 = new MyThreadInc(myShared); MyThreadDec thread3 = new MyThreadDec(myShared); MyThreadDec thread4 = new MyThreadDec(myshared); thread1.start(); thread2.start(); thread3.start(); thread4.start();
Specificatorul "volatile"
Specificatorul volatile poate fi folosit la declararea unei variabile pentru a ignora potentiale optimizari ale compilatorului ce pot cauza inconsistente asupra starii observabile a memoriei. Valoarea unei variabile ce nu este declarata ca volatile poate fi mentinuta in memoria accesibila local thread-ului (cached) pentru un acces mai rapid, iar thread-ul poate sa piarda observarea unor schimbari efectuate asupra acestei variabile de catre un thread diferit. Prin declararea variabilei ca volatile orice operatii de citire sau scriere asupra acesteia sunt executate in memoria principala partajata, implicand evident un cost crescut de acces, in favoarea unei vederi consistente asupra starii variabilei.
// Stop thread example .. revisited public class MyThread extends Thread { boolean alive = true; public void run (){ int count = 0; while (alive) { //<-------------| count++; // | } // | // | System.out.println("final count is " + count); // | } // | // | public void killThread() { // | // | alive = false; // | } // | // | } // | // | ... // | // | MyThread lazyThread = new MyThread(); // | lazyThread.start(); // | lazyThread.killThread(); // <-- la urmatoarea iteratie while in cadrul run | // dupa ce alive este setat pe false lazyThread se va opri | // ... sau nu | // variabila este setata de thread-ul curent | // si verificata de lazyThread ----------------------------- // care ar putea sa nu observe schimbarea in proria // memorie (cache) ce retine valoarea //Solutie: adaugarea volatile la declararea variabilei alive //va asigura citirea si scrierea obligatorie in memoria partajata //si vizibilitatea valorii reale a variabilei pentru toate thread-urile boolean volatile alive = true;
Locking explicit in Java
Pe langa specificatorul synchronized, Java ofera un mod explicit de locking prin intermediul interfetei Lock (disponibila in pachetul java.util.concurrent.locks) ce este implementata de o serie de clase care pot fi utilizate pentru instantierea de locks. Una dintre aceste clase este ReentrantLock.
Clasa ReentrantLock poate fi folosita pentru instantierea de locks ce pot fi folosite intr-un mod similar cu synchronized descris mai sus pentru accesarea unei portiuni de cod. Principalele metode utilizate in acest scop sunt lock si unlock.
import java.util.concurrent.locks.ReentrantLock; class SharedWork { ReentrantLock lock; SomeObject sharedObject; SharedWork() { lock = new ReentrantLock(); sharedObject = new SomeObject(); } public void accessObject() { MyThread thread1 = new MyThread(); MyThread thread2 = new MyThread(); thread1.start(); thread2.start(); } class MyThread extends Thread { public void run() { while (true) { lock.lock(); //thread-ul obtine lock-ul shareObject.someMethod(); //executa o metoda ce necesita sincronizare lock.unlock(); //thread-ul elibereaza lock-ul } } } }
Ce se intampla daca in situatia de mai sus este aruncata o exceptie?. Spre deosebire de synchronized care va elibera lock-ul intern cand executia programului paraseste sectiunea sincronizata, lock-urile explicite raman blocate pana la apelul unlock. Din acest motiv este recomandata asigurarea ca apelul unlock va fi executat in orice situatie:
... while (true) { lock.lock(); //thread-ul obtine lock-ul try { shareObject.someMethod(); //executa o metoda ce necesita sincronizare } finally { lock.unlock(); //thread-ul elibereaza lock-ul } } ...
Una din proprietatile ReentrantLock este ca metoda lock poate fi apelata cu succes de mai multe ori de thread-ul ce detine lock-ul respectiv. Efectul consta intr-o incrementare interna mentinuta pe instanta lock-ului. In cazul apelului unlock contorul intern este decrementat, iar lock-ul nu este eliberat pana ce acesta nu ajunge la valoarea 0. In situatia in care se doreste ca lock-ul sa nu fie folosit in acest mod re-entrant, practic prin limitarea la incrementarea pana la 1 a acestui contor intern, se poate utiliza metoda isHeldByCurrentThread:
// sample din documentatia Java class X { ReentrantLock lock = new ReentrantLock(); // ... public void m() { assert !lock.isHeldByCurrentThread(); //checking here if the thread does not already hold the lock lock.lock(); try { // ... method body } finally { lock.unlock(); } } }
Valoare curenta a contorului intern se poate obtine prin apelul metodei getHoldCount.