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


Threadurile java, fie ca sunt create mostenind de la clasa Thread sau implementand interfata Runnable, se pornesc cu metoda start() si se opresc cu metodele stop() si join().

Sincronizarea threadurilor

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:

  • mecanismul synchronized
  • si metodele: wait, notify si notifyAll. Pentru a intelege problematica sincronizarii threadurilor, vom considera problema producatorilor si a consumatorilor. Aceasta spune ca avem mai multi producatori care "produc" in paralel obiecte si le depoziteaza intr-un container comun si avem mai multi consumatori care "consuma" in acelasi timp obiectele depozitate in container de catre producatori. Toti producatorii si consumatorii vor partaja acelasi container. Pentru a simplifica putin lucrurile am ales ca container care va fi partajat de producatori si consumatori, o clasa Product care incapsuleaza o valoare de tip int. Astfel ca producatorii si consumatorii vor produce, respectiv consuma, valori de tip int. Ca sa fie si mai simplu, containerul (obiect de tipul Product) poate contine o singura valoare de tipul int (poate contine un singur produs), iar la un moment dat exista un singur consumator si un singur producator in executie. Pornind de la aceste reguli, am scris urmatorul program pentru problema producatorilor si consumatorilor (producatorii si consumatorii sunt evident threaduri):
    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.

    Eliminarea bug-ului ar consta intr-un mecanism care ar face tot codul din metoda Product.get() atomic. Astfel, daca as avea la dispozitie un mecanism care ar oferi garantia ca atunci cand metoda get() a inceput sa se execute in interiorul unui thread, nici un alt thread in afara de cel curent nu va putea executa vreo instructiune pana nu are rost iesirea din metoda get, bug-ul mentionat mai sus va fi eliminat cu certitudine. Acest mecanism de asigurare a atomicitatii (mecanism de blocare) este introdus in Java prin cuvantul rezervat synchronized. In Java pot sa sincronizez (sa blochez, sa fac atomic) o metoda sau o portiune de cod. Incepem cu metodele sincronizate si apoi vorbim si de portiuni de cod sincronizat.
    Adaugarea cuvantului rezervat synchronized in fata unei metode face ca in timpul executiei, la un moment dat, aceasta metoda sa nu poata fi executata pe acelasi obiect decat de un singur thread. Altfel spus, un singur thread poate executa aceasta metoda la un moment dat. Este important de notat faptul ca, o metoda synchronized poate fi apelata in acelasi timp pe doua obiecte diferite, dar daca o apelam pe acelasi obiect, ea nu se poate executa simultan in doua threaduri. De ce se intampla acest lucru? Datorita modului in care masina virtuala java (JVM) sincronizeaza metodele. Ce se intampla de fapt la executia unei metode synchronized?
    Orice obiect (instanta a unei clase) din cadrul masinii virtuale java are asociat un "blocaj invizibil" (lock - puteti sa va ganditi la acest lock ca la o variabila boolean privata a obiectului care ia valoarea true cand obiectul este folosit/blocat si false cand nu este folosit/deblocat). Cand apelez o metoda a unei clase totdeauna o apelez pe un anumit obiect (exceptie cazul in care metoda este statica, dar aici ne referim numai la metode nestatice). Cand apelez o metoda synchronized intr-un thread, masina virtuala java incearca intai sa obtina blocajul (lock) pe obiectul pe care este apelata metoda. Daca acest obiect este blocat deja (de catre alt apel de metoda synchronized in alt thread), threadul va intra in asteptare pana cand obiectul va fi deblocat. Daca obiectul nu este blocat, threadul curent va bloca obiectul, va rula metoda synchronized, iar apoi va debloca obiectul.

    Vom face acum cateva observatii relative la mecanismul synchronized.
    1. O metoda sincronizata apelata pe doua obiecte diferite, in doua threaduri diferite, nu va sincroniza cele doua threaduri. De ce? Pentru ca blocajele (lock-ul) se va obtine independent pe doua obiecte diferite.
    2. Blocarea se face totdeauna pe obiectul fizic si nu pe referinte la acesta. Astfel ca, nu este bine sa se modifice obiectul dupa care se sincronizeaza, in interiorul codului sincronizat (acest sfat are sens in cazul portiunilor de cod sincronizat si nu in cazul metodelor sincronizate - vezi mai jos) 3. Metodele sincronizate se scriu, in general, in afara claselor Thread (cum este cazul clasei Product din exemplul nostru). De ce?

    Pe langa metode sincronizate, mai pot exista portiuni de cod sincronizat. O portiune de cod sincronizat se scrie in felul urmator:
            ....
            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.
    O varianta si mai speciala de cod sincronizat este sincronizarea dupa clasa, ca in exemplul urmator:
            ....
            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.
    Dupa ce am invatat de mecanismul synchronized, putem sa scriem o varianta mai buna a programului in care metodele get() si set() din clasa Product sunt sincronizate:
    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:
  • metoda wait elibereaza blocajul (lock-ul) pe obiect, spre deosebire de metoda sleep() care nu elibereaza blocajul. Practic, prin aceasta eliberare a blocajului pe care o face wait(), alte threaduri care asteapta dupa blocajul aceluiasi obiect, pot sa intre in lucru, evitand astfel deadlock-ul. Incercati sa rulati programul cu wait() in loc de sleep() si veti vedea ca el se va termina cu succes si nu va mai ajunge la deadlock.
  • metoda wait() se poate "trezi" mai repede decat este specificat in parametrul de timeout, "dormind" astfel un numar variabil de secunde, spre deosebire de metoda sleep care "doarme" intotdeauna un numar fix de secunde. Metoda wait() se poate trezi mai repede decat este specificat prin parametru, daca un alt thread apeleaza metoda notify() sau notifyAll().

    Un alt element demn de observat este ca metoda sleep() tine de clasa Thread, pe cand metoda wait() tine de clasa Object, radacina ierarhiei de clase din limbajul Java. Metodele notify() si notifyAll() "trezesc" threadurile care asteapta (wait()) dupa o anumita conditie. Care este aceasta conditie? Conditia este implicita si se poate descrie prin secventa: "obtinerea blocajului asupra unui obiect". Pentru a observa efectul metodelor notify() si notifyAll(), rulati programul asa cum este prezentat el mai sus, iar apoi, decomentati si apelurile notifyAll() din metodele get() si set() si rulati din nou programul. Veti vedea ca a doua rulare are un timp de executie mai redus decat prima. Acest lucru se intampla pentru ca "wait-urile" nu "dorm" pana la capat pentru ca sunt "trezite" de "notify-uri". O ultima observatie: metodele notify() si notifyAll() sunt tot din clasa Object si nu din clasa Thread.