Threaduri simple in Java
In Java putem crea threaduri fie extinzand (mostenind, derivand din) clasa Thread, fie implementand interfata Runnable. Esential este ca clasa noastra thread sa aiba o metoda public void run() care constituie corpul principal de instructiuni care se va executa in cadrul threadului. Un exemplu simplu de program care foloseste threaduri java este urmatorul. Acest program creeaza 10 threaduri si fiecare thread isi va tipari la pornire si la oprire propriul ID (un numar unic alocat threadului la creare).
import java.util.*; class AThread extends Thread { int id; AThread() { id = 0;} AThread(int id) { this.id = id;} public void run() { Random rand = new Random(); System.out.println("Thread "+id+" starting .."); try { sleep(((int)(1+ Math.random()))*10000); } catch (Exception ex) { System.err.println("exception caught while sleeping"); } System.out.println("Thread "+id+" exiting.."); } } public class ThrEx { public static void main(String args[]) { AThread threads[] = new AThread[10]; for(int i=0; i<10; i++) threads[i] = new AThread(i); for(int i=0; i<10; i++) threads[i].start(); } }
Conceptul de thread (fir de executie) este folosit in programare pentru a eficientiza executia programelor, executand portiuni distincte de cod in paralel, in interiorul aceluiasi proces. Cateodata insa, aceste portiuni de cod care constituie corpul threadurilor, nu sunt complet independente si in anumite momente ale executiei, se poate intampla ca un thread sa trebuiasca sa astepte executia unor instructiuni din alt thread, pentru a putea continua executia propriilor instructiuni. Aceasta tehnica prin care un thread asteapta executia altor threaduri inainte de a continua propria executie, se numeste sincronizarea threadurilor. Java ofera urmatoarele facilitati pentru sincronizarea threadurilor:
class Consumer extends Thread { private Product prod; public Consumer(Product prod) { this.prod = prod; } public void run() { int value = 0; for (int i = 0; i < 10; i++) { value = prod.get(); try { sleep(1+(int)(Math.random() * 100)); } catch (InterruptedException e) { } } } } class Producer extends Thread { private Product prod; public Producer(Product prod) { this.prod = prod; } public void run() { for (int i = 0; i < 10; i++) { prod.set(i); try { sleep(1+(int)(Math.random() * 100)); } catch (InterruptedException e) { } } } } class Product { private int value; private boolean available; // daca sunt produse in container Product() {value = -1; available = false;} public boolean empty() { return !available; } public int get() { // consuma produs while (available==false) { try { Thread.currentThread().sleep(5); } catch(Exception ex) { System.err.println("error sleeping");} } available = false; System.out.println("am consumat produsul "+value); return value; } public void set(int val) { // produce produs while (available==true) { try { Thread.currentThread().sleep(6); } catch(Exception ex) { System.err.println("error sleeping");} } available = true; value = val; System.out.println("am produs "+val); } } public class Thrsynchr { public static void main(String args[]) { Product prod = new Product(); Consumer c = new Consumer(prod); Producer p = new Producer(prod); p.start(); c.start(); } }In programul precedent se folosesc metodele get() si set() din clasa Product pentru a consuma, respectiv produce, o valoare de tipul int. Unul dintre bug-urile care pot aparea in cazul in care modific programul in asa fel incat sa avem 2 consumatori care sa consume in paralel din acelasi container (care poate contine maxim o valoare de tipul int) si un singur producator care sa puna cate o valoare in acel container, este situatia in care cei doi consumatori consuma aceeasi valoare produsa o singura data de catre producator. Acesta este un bug-urile complicat, deoarece aparitia lui este aleatoare, drept urmare, el poate fi reprodus destul de greu. Situatia este perfect plauzibila daca ne gandim putin. Sa ne gandim ca initial containerul e gol. Consumatorul 1 tot executa ciclul while (available==false) { ... } din metoda Product.get() deoarece nu exista nici o valoare de consumat. La fel si Consumatorul 2 este blocat de ciclul while pana producatorul va produce o valoare. Cat timp cei doi consumatori sunt in ciclul while, threadul Producator produce o valoare. Sa presupunem ca primul care va iesi din while va fi Consumatorul 1. Consumatorul 1 va consuma valoarea din container, dar sa presupunem ca inainte de a seta available la false, Consumatorul 2 iese si el din while si incepe si el sa consume acceasi valoare pe care o consuma Consumatorul 1. In cazul exemplului nostru simplu, e drept, aceasta situatie poate avea loc mai rar (totusi ea se poate intampla!), dar in cazul programelor mai complexe care contin mai multe linii de cod intre partea de verificare a disponibilitatii datelor (ciclul wait) si partea de consum efectiv a valorii (available=false), acest bug poate aparea suparator de frecvent.
.... synchronized(ob) { //instructiuni .... } ....In exemplul de mai sus, blocajul se va face pe obiectul ob si va dura pe toata durata codului din interiorul blocului synchronized.
.... synchronized(Class.forName("MyClass")) { //instructiuni .... } ....In acest exemplu, un singur obiect (nu numai o singura metoda!) de tipul MyClass poate fi folosit (apelabil) la un moment dat de catre un singur thread. Acest tip de sincronizare (dupa clasa) este cea mai dura sincronizare si face codul aproape serial (un singur thread se executa la un moment dat si toate celelalte asteapta), astfel ca face ineficienta utilizarea threadurilor.
class Consumer extends Thread { private Product prod; public Consumer(Product prod) { this.prod = prod; } public void run() { int value = 0; for (int i = 0; i < 10; i++) { value = prod.get(); try { sleep(1+(int)(Math.random() * 100)); } catch (InterruptedException e) { } } } } class Producer extends Thread { private Product prod; public Producer(Product prod) { this.prod = prod; } public void run() { for (int i = 0; i < 10; i++) { prod.set(i); try { sleep(1+(int)(Math.random() * 100)); } catch (InterruptedException e) { } } } } class Product { private int value; private boolean available; // daca sunt produse in container Product() {value = -1; available = false;} public boolean empty() { return !available; } public synchronized int get() { // consuma produs while (available==false) { try { Thread.currentThread().sleep(5); //wait(5000); } catch(Exception ex) { System.err.println("error sleeping");} } available = false; System.out.println("am consumat produsul "+value); //notifyAll(); return value; } public synchronized void set(int val) { // produce produs while (available==true) { try { Thread.currentThread().sleep(6); //wait(6000); } catch(Exception ex) { System.err.println("error sleeping");} } available = true; value = val; System.out.println("am produs "+val); //notifyAll(); } } public class Thrsynchr { public static void main(String args[]) { Product prod = new Product(); Consumer c = new Consumer(prod); Producer p = new Producer(prod); p.start(); c.start(); } }Daca vom rula programul de mai sus, vom observa insa ca, la un moment dat, chiar daca executia nu s-a terminat, programul nu mai afiseaza nimic. Se intampla ceea ce se numeste deadlock, adica doua threaduri asteapta la infinit unul dupa celalalt, ca sa-si poata continua executia, fara insa a reusi sa execute instructiuni nici unul dintre threaduri. De ce apare deadlock-ul? Este foarte simplu. Sa presupunem ca containerul este initial gol. Consumatorul va bloca obiectul prod si va executa metoda get(). El insa se va opri in ciclul while (available==false) {...} deoarece nu este inca nici un produs in container. Threadul Producator incearca in zadar sa produca o valoare, apeland metoda set() deoarece pentru a putea apela aceasta metoda, threadul trebuie sa blocheze acelasi obiect prod care este blocat de catre threadul Consumator. Astfel ca threadul Consumator va astepta in ciclul while dupa threadul Producator sa produca o valoare, in timp ce threadul Producator nu poate produce o valoare pentru ca nu poate sa obtina blocajul pe obiectul prod (blocaj detinut de threadul Consumator). Solutia este inlocuirea apelului sleep() in metodele get() si set() cu metoda wait(). Metoda wait() face aproximativ acelasi lucru ca si metoda sleep, cu doua diferente esentiale: