Konkurentno programiranje (Multithreading)
Pod pojmom nit (thread) podrazumeva se jedna sekvencija naredbi koje se izvršavaju unutar programa. Nit, za razliku od programa, ne postoji kao celina koju korisnik može da pokrene. Zapravo, korisnik ne mora ni da zna za njihovo postojanje. Umesto toga, niti se izvršavaju unutar samog programa. Njihovo izvršavanje može biti istovremeno, pri čemu svaka od niti izvršava različite zadatke. Ova osobina Jave (izvršavanje više niti istovremeno) naziva se multithreading ili konkurentno programiranje. Ona omogućava da jedan program podelite u manje celine koje mogu da se izvršavaju paralelno, što drastično smanjuje ukupnu brzinu programa.
Sinhronizacija
Prilikom rada sa nitima, neophodno je znati sledeće. Niti mogu da se zaista izvršavaju paralelno samo ukoliko na sistemu na kome se program izvršava postoji više procesora. Ukoliko postoji samo jedan procesor, tada se niti izvršavaju naizmenično. Na sreću brzina kojom se smenjuju je izuzetno velika, pa iz ugla korisnika izgleda kao da je izvršavanje istovremeno.
Ipak, kod paralelnog rada niti mogući su i određeni problemi. Pogledajte primer koda koji čita neku vrednost i nakon određenog vremena povećava je za 1.
if (vrednost > 0) {
..... // sacekaj neko vreme
vrednost +=1;
}
Različite niti u okviru jednog programa imaju pristup ovoj promenljivoj i moguće je da pozovu metod u okviru koga se ovaj kod nalazi istovremeno. Nevolja nastaje ukoliko su dva metoda izvršila if test pre nego što je vrednost povećana za jedan. U tom slučaju vrednost će biti povećana samo jednom, dok će drugo povećanje biti izgubljeno.
Da bi se izbegle ove i slične greške, koristi se sinhronizacija niti. Sinhronizovani deo koda naziva se kritična sekcija ili atomske operacije. Ova sekcija se prikazuje drugim nitima kao da se dešava istovremeno.
Pogledajte sledeći primer u kome se koristi već pomenuti kod:
public class BrojacNiti {
int vaznaVrednost;
public void prebrojMe() {
vaznaVrednost +=1;
}
public int koliko() {
return vaznaVrednost;
}
}
Predpostavimo da postoji veliki broj niti koji mogu da pozovu metod prebrojMe() istovremeno. Tada je očito da će doći do problema sa naredbom +=. Java omogućava da se ovaj problem prevaziđe na sledeći način:
public class SiguranBrojacNiti {
int vaznaVrednost;
public synchronized void prebrojMe() {
vaznaVrednost +=1;
}
public int koliko() {
return vaznaVrednost;
}
}
Ključna reč synchronized čini da je kod ovog metoda siguran. To znači da će biti dozvoljena samo jedna nit u ovom metodu u jednom trenutku, dok druge moraju da čekaju da ta nit završi sa ovim metodom pre nego što one mogu da ga izvrše.
Ovo takođe znači da sinhronizovanje velikog metoda koji se dugo izvršava nije dobra ideja.
Metod koliko() u prethodnom primeru ne mora da bude sinhronizovan, obzirom da samo vraća trenutnu vrednost promenljive. Međutim, metod koji ga poziva bi trebao da bude. Pogledajte sledeći primer:
public class Point { // redefinise klasu Point paketa java.awt
private float x,y;
public float x() { // nije potrebna sinhronizacija
return x;
}
public float y() { // nije potrebna sinhronizacija
return y;
}
....... // metodi koji postavljaju i menjaju vrednost x iy
}
public class NesigurnoStampaj {
public void print(Point p) {
System.out.println (“X koordinata tacke je “ + p.x()
+ “ a Y koordinata je “ + p.y());
}
}
Ovde su metodi x() i y() analogni metodu koliko(), samo vraćaju vrednost i nisu sinhronizovani. Međutim, iako metod print() samo čita i ispisuje vrednosti koordinata tačke, on ipak čita dve vrednosti. Moguće je da se jedna od ove dve vrednosti promeni u međuvremenu, između poziva metoda p.x() i p.y(), od strane neke druge niti. Da bi ovaj problem bio prevaziđen, moguće je metod print() definisati kao synchronized. Međutim, ključna reč synchronized može biti primenjena i na objekte, a ne samo na metode. Na taj način jedan objekat je “zaključan” dok god jedna nit ne završi izvršavanje određenog dela koda. Imajući to u vidu, gore navedeni problem bismo mogli da rešimo na sledeći način:
public class Stampaj {
public void print(Point p) {
float sigurnoX, sigurnoY;
synchronized(this) {
sigurnoX = p.x(); // ove dve linije se
sigurnoY = p.y(); // izvrsavaju sinhronizovano
}
System.out.println(“X koordinata tacke je “ + sigurnoX
+ “ a Y koordinata je “ + sigurnoY);
}
}
Na ovaj način je zaštićen samo deo koda. Ukoliko jedna nit krene da ga izvršava, druge ne mogu da mu pristupe sve dok prva ne završi sa izvršavanjem.
Prilikom kreiranja klasa uvek je dobro zaštiti određene delove te klase kako ne bi bila moguća gore navedena situacija. Iz tog razloga obično se metodi za promenu vrednosti nekih argumenata objekta definišu kao synchronized. Pogledajte sledeći primer:
public class Point {
private float x, y;
.....
// x() i y() metodi
.....
public synchronized void setXiY(float novoX, float novoY) {
x = novoX;
y = novoY;
}
}
Promenljive x i y su deklarisane kao private, što znači da im je nemoguće pristupiti izvan klase. Metod setXiY() služi za postavljanje novih vrednosti ovih atributa i on je dostupan drugim klasama. Kreiran je kao synchronized, što znači da ako ga jedna nit pozove ostale moraju da čekaju da prva nit završi promenu obe koordinate.
Kreiranje i upotreba niti
U Javi postoji klasa java.lang.Thread. Ova klasa implementira interfejs Runnable. Da biste kreirali svoju nit možete ili da napravite podklasu klase Thread ili da napravite novu klasu koja implementira interfejs Runnable. Na primer:
public class MojaPrvaNit extends Thread {
public void run() {
....... // uradi nesto korisno
}
}
Na ovaj način je napravljena nit koja radi nešto korisno kada se pozove njen metod run(). Da biste je zaista i pokrenuli neophodno je da kreirate njen objekat i pozovete metod run().
MojaPrvaNit nit1 = new MojaPrvaNit();
nit1.start(); // poziva metod run()
Ukoliko želite da zaustavite izvršavanje niti koristite metod stop():
nit1.stop();
Pored ova dva, niti imaju i druge metode. Ovde su navedeni neki od njih.
nit1.suspend(); // prekida izvršavanje niti
nit1.resume(); // nastavlja izvršavanje niti
nit1.sleep(5000); // prekida izvršavanje niti u sledećih 5 sekundi
Metodi stop(), suspend() i resume() se danas smatraju nesigurnim i ne preporučuju se za korišćenje. Poziv ovih metoda dovodi do toga da se bilo koji deo koda koji je bio zaključan, otključa i postane dostupan drugim nitima. Na primer, ukoliko neku nit nit1 prekinete pomoću metoda stop() u trenutku dok menja atribute nekog objekta, taj objekat će odjednom postati dostupan drugim nitima bez obzira što možda nisu svi atributi koji su bili sinhronizovani promenjeni istovremeno.
Interfejs Runnable
Ponekad će biti potrebno da kreirate nit koja nasleđuje neku drugu klasu, a ne klasu Thread. Tada je dovoljno da koristite interfejs Runnable:
public class IzvrsnaKlasa extends NekaKlasa implements Runnable {
public void run() {
........ // uradi nesto korisno
}
}
Ukoliko je na ovaj način kreirana nit, pokrećete je na sledeći način:
IzvrsnaKlasa izv = new IzvrsnaKlasa();
Thread nit = new Thread(izv);
nit.start(); // poziva metod run() indirektno
Pogledajte sledeći primer. U njemu su upotrebljeni i neki novi metodi klase Thread.
1: public class Nit implements Runnable {
2: public void run() {
3: System.out.println(“Izvrsava se nit koja se zove ‘” +
4: Thread.currentThread().getName() + “’”);
5: for (int i = 0; i<20; i++) {
6: System.out.print(i + " ");
7: }
8: System.out.println("Nit ‘" +
9: Thread.currentThread().getName() + "’ je zavrsena");
10: }
11:}
12:
13: public class TesterNiti {
14: public static void main(String args[]) {
15: Nit nit1 = new Nit();
16: for (int i = 0; i<20; i++) {
17: Thread t = new Thread(nit1);
18: System.out.println(“Nova nit je kreirana? “ + (t==null));
19: t.start();
20: try {
21: t.join();
22: }
23: catch (InterruptedException ignorisan) {}
24: // ceka da nit zavrsi izvrsavanje
25: }
26: }
27:}
U ovom primeru je prvo kreirana klasa Nit koja samo implementira interfejs Runnable i služi kao šablon za sve niti koje će biti kreirane. U okviru ove klase poziva se klasni metod currentThread() klase Thread, kako bi se dobila informacija o niti koja se trenutno izvršava, a zatim se poziva metod getName() trenutne niti kako bi se dobila informacija o njenom imenu.
U liniji 17 se na osnovu objekta nit1 kreiraju niti. Zatim se ispisuje poruka o uspešnosti kreiranja nove niti i ta nit se pokreće u liniji 19. Metod join() u liniji 21 znači da se čeka da nit t završi sa izvršavanjem koda. Zbog korišćenja ovog metoda, kada pokrenete program niti će se izvršavati redom, jedna po jedna. Izlaz programa će izgledati ovako:
Nova nit je kreirana? true
Izvrsava se nit koja se zove ‘Thread-1’
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 Nit ‘Thread-1’ je zavrsena
Nova nit je kreirana? true
Izvrsava se nit koja se zove ‘Thread-2’
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 Nit ‘Thread-2’ je zavrsena
Nova nit je kreirana? true
Izvrsava se nit koja se zove ‘Thread-3’
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 Nit ‘Thread-3’ je zavrsena
Međutim, ukoliko ne bi bilo naredbe t.join() (pokušajte da obrišete ovu naredbu zajedno sa try – catch blokom oko nje), niti bi se izvršavale paralelno (naizmenično) i rezultat bi izgledao otprilike ovako:
Nova nit je kreirana? true
Nova nit je kreirana? true
Nova nit je kreirana? true
Nova nit je kreirana? true
Nova nit je kreirana? true
Izvrsava se nit koja se zove ‘Thread-1’
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 Nit ‘Thread-1’ je zavrsena
Izvrsava se nit koja se zove ‘Thread-2’
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 Nit ‘Thread-2’ je zavrsena
Izvrsava se nit koja se zove ‘Thread-3’
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 Nit ‘Thread-3’ je zavrsena
Izvrsava se nit koja se zove ‘Thread-4’
Nova nit je kreirana? true
Izvrsava se nit koja se zove ‘Thread-5’
0 Nova nit je kreirana? true
0 1 Nova nit je kreirana? true
1 2 2 3 3 Izvrsava se nit koja se zove ‘Thread-6’
Prilikom pokretanja programa, automatski se kreira nekoliko niti koje obezbeđuju pravilno izvršavanje programa. Zbog toga treba biti obazriv i voditi računa koja je nit trenutno aktivna. Na primer, naredba Thread.currentThread().stop() bi mogla da potpuno prekine izvršavanje čitavog programa ukoliko se ne vodi računa o tome koja se nit trenutno izvršava.
Pored prethodno korišćenog, klasa Thread ima jos jedan konstruktor koji je vrlo koristan jer dozvoljava da imenujete vaše niti. Na primer,
Thread t = new Thread (nit, “Uzimanje ulaznih podataka”);
Izuzeci proizvedeni od strane niti
Kada radite sa nitima, nekoliko izuzetaka mogu da budu proizvedeni.
Ukoliko je nit prekinuta pomoću naredbe stop(), ona proizvodi grešku klase ThreadDeath. Ukoliko pomoću naredbe catch uhvatite ovaj izuzetak, imaćete informaciju o tome na koji način je uništena nit:
try {
t.start();
NekiMetodKojiMozeDaZaustaviNit();
} catch (ThreadDeath td) {
..............// uradi nesto korisno
throw td; // ponovo vrati gresku
}
Ponovno vraćanje greške pomoću naredbe throw je neophodno kako bi ova nit zaista i bila uništena.
Takođe, Java platforma proizvodi IllegalThreadStateException kada god pokušate da pokrenete metod koji nit ne može da obradi u trenutnom stanju. Na primer, nit koja je zaustavljena pomoću sleep() metoda ne može da bude ponovo pokrenuta pomoću resume() metoda. Isti izuzetak se proizvodi i kada pokušate da suspendujete nit koja se ne izvršava pomoću naredbe suspend().
Na osnovu tipa izuzetka koji je proizveden možete da utvrdite na koji način je uništena nit i da eventualno nešto preduzmete:
try {
...... // kod koji se bavi nitima
} catch (ThreadDeath d) {
// nit je unistena pomocu stop() metoda
throw d; //obavezno prosledite izuzetak
} catch (IllegalThreadStateException e) {
// niti je prosledjen metod koji nije bila u stanju da
// obradi
} catch (InterruptedException e) {
// nit je prekinuta
}