Predavanje br. index|1|2|3|4|5|6|7|8|9|10|11|12|13|14|HOME


Jedanaesto predavanje - threadovi

Multitasking i multithreading – što je thread? - kako Java koristi threadove? – klase threadova – jednostavni thread – višestruki threadovi – imenovanje threadova – prioriteti threadova – metoda sleep() – sinkronizacija: problem – sinkronizacija: nekoliko pokušaja rješenja - ključna riječ synchronized – sinkroziniranje na objektima – primarni thread – grupe threadova – threadovi demoni – ustupanje kontrole – spajanje threadova – sučelje Runnable – animacija – pravovremenost – titranje slike – pokretanje i zaustavljanje animacije – višestruke nezavisne animacije


Multitasking i multithreading

Prvi kompjuteri mogli su obavljati zadaće samo jednu po jednu. Svi su se programi izvršavali sekvencijalno, jedan po jedan i svaki je zauzimao sve resurse računala. Takav se način rada naziva batch obrada. To je efikasan način za postizanje maksimalne iskoristivosti skupih računala jer se gotovo sve CPU vrijeme troši na stvarnu obradu. Međutim, batch obrada je nepovoljna ako programi koji traju nekoliko sekundi moraju čekati na završetak programa od nekoliko dana.

Time sharing operacijski sustavi su izmišljeni kako bi se omogućilo da više ljudi istodobno koristi jedno računalo. Operacijski sustav ovdje upravlja raspodjelom vremena između različitih programa koji se istodobno izvršavaju.

Jednom kad su sustavi omogućavali različitim korisnicima istodobno izvršavanje programa, preostao je još samo mali korak do toga da isti korisnik izvršava istodobno više programa (multitasking). Svaki aktivni program, uobičajeno je reći proces, imao je svoj vlastiti memorijski prostor, svoj skup varijabli, svoj stack i heap i tako dalje. Jedan process je mogao lansirati drugi process, ali nakon toga svaki od njih se ponašao manje ili više nezavisno. Mehanizmi kao što je poziv udaljenih procedura (remote procedure call, RPC) razvijeni su kako bi omogućili procesima međusobnu interakciju, ali takva je interakcija bila skupa i komplicirana. Takvo stanje potrajalo je nekih dvadesetak godina.

Međutim, nisu samo korisnici ti koji imaju potrebu obavljati više poslova istodobno. Mnogi programi po svojoj su prirodi također takvi. Web browser na primjer, može ispisivati datoteku u pozadini dok istodobno skida stranicu u jednom prozoru i formatira je kako sadržaj pristiže. Sposobnost individualnog programa da radi više od jedne stvari istodobno najefikasnije se implementira kroz koncept threadova (multithreading).


Što je thread?

Thread (u prijevodu: nit, konac, trag, staza) možemo definirati kao tok izvođenja operacija koji se događa nezavisno od procesa ili događaja u okolini. Thread je poput klasičnog programa koji započinje u točki A i završava u točki B. On nema u sebi petlju koja očekuje događaje već se izvršava ne gledajući što se događa oko njega. Bez threadova cijeli bi program povremeno morao stajati zbog nekog procesa koji intenzivno koristi CPU ili zbog neke beskonačne petlje, stavljene u njega namjerno ili pogreškom. Koncept threadova omogućuje da zahtjevni procesi ne ometaju ostale procese u njihovom izvršavanju.

Pokazuje se da je implementiranje threadova složenije od implementiranja multitaskinga u operacijskom sustavu. Razlog relativno jednostavnom implementiranju multitaskinga je što su programi u pravilu izolirani jedni od drugih. Threadovi u jednom programu, međutim, nisu međusobno izolirani i moraju voditi računa jedan o drugome. Na primjer, ako za vrijeme dok se u jednom threadu ispisuje neki tekst, orisnik u drugom threadu obriše dio tog istog teksta, nastaje problem: što u tom slučaju treba biti ispisano?

Okruženja koja, poput Jave, podržavaju threadove imaju mehanizam sinkronizacije. Threadovima je, naime, omogućeno privremeno zaključavanje resursa koje dijele s drugim threadovima, čime se osigurava integritet podataka. Sinkronizaciju, međutim, valja koristiti s mjerom jer će prednosti koje donose threadovi biti poništene ako cijeli sustav bude često morao čekati dok se pojedini resurs ne otključa. Pravilni odabir objekata i metoda koje treba sinkronizirati pripada vještini programiranja i osjećaj za njega stječe se vježbom.


Kako Java koristi threadove?

Java aplikacije i appleti su po prirodi threadovski. Runtime okolina započinje izvršavanje programa sa main() metodom unutar jednog threada. Garbage collection se obavlja u drugom threadu. Screen updating se pojavljuje u trećem threadu. Mogu se također pojavljivati i drugi threadovi, uglavnom vezani uz zadaće koje obavlja applet viewer ili web browser. Sve se to događa bez eksplicitnog znanja programera. Dio vremena zanimat će vas samo ono što se događa u primarnom threadu koji sadrži main() metodu programa. Tada se ne morate ni optrerećivati konceptom threadova.

Ponekad, međutim, želite dodati svoje vlastite threadove nekom appletu ili aplikaciji. Najjednostavniji razlog za to bio bi, na primjer, izdvojiti duga računanja od ostatka programa. Ako želite pronaći desetmilijunti prim broj, ne želite prisiljavati korisnika da čeka besposlen dok računalo obavlja to traženje. Isto vrijedi i za čekanje da neki resurs postane dostupan, recimo velika grafička datoteka koju skidate s mreže. Svaka vremenski zahtjevna operacija trebala bi biti smještena u zasebni thread.

Drugi razlog za korištenje threadova je ravnomjerna raspodjela mogućnosti računala između različitih zadaća. Ako želite iscrtavati slučajne pravokutnike na ekranu, svejedno želite ostaviti korisniku mogućnost da komunicira s appletom. Ako se sav CPU troši na crtanje, to neće biti moguće. Na natjecateljskim (preemptive) operacijskim sustavima kao što je Solaris ili Windows NT, korisnik može u slučaju potrebe, ako ništa drugo, prekinuti aplikaciju. Na kooperativnim (cooperative) sustavima kao što je MacOs ili Windows, to neće biti moguće ako se ne učini reboot mašine, a to je, naravno, loše. Uporabom threadova omogućit ćete da korisnički unos dobije visok prioritet, a crtanje sličica nizak. Tada će korisnik moći zaustaviti applet i bez gašenja računala na glavnom prekidaču.


Klase threadova

U Javi postoje dva načina da klasu pretvorimo u thread. Jedan je da je učinimo podklasom klase java.lang.Thread. Ako je naša klasa već podklasa neke druge klase, onda to, naravno, neće biti moguće, pa ćemo koristiti drugi način, a to je implementiranje sučelja java.lang.Runnable.

Klasa Thread ima tri glavne metode koje se koriste za upravljanje threadovima:

public native synchronized void start()
public void run()
public final void stop()
 

Metoda start() priprema thread za izvršavanje. Metoda run() je ta koja obavlja zadaću namijenjenu threadu. Thread se normalno završava kad ona završi. Thread se može zaustaviti i pomoću metode stop(), ali se taj način pokazao nesigurnim i zbog toga je ova metoda deprecated (v. objašnjenja u biblioteci potprograma). Zbog istog razloga deprecated su i metode suspend() i resume().

Metoda run() se ne poziva eksplicitno. Ona će, nakon što ste pozvali metodu start() biti automatski pozvana kad za to dođe vrijeme.

Sučelje Runnable omogućuje uporabu koncepta threadova u klasama koje ne mogu biti podklase klase Thread. Ono deklarira samo jednu metodu, run():

public abstract void run()
 

Ako za argument konstruktora Thread() supstituirate objekt iz klase koja implementira Runnable, moći ćete njegovu run() metodu koristiti umjesto run() metode iz klase Thread.


Jednostavni thread

Kad pišete program koji sadrži threadove, možete zamišljati da pišete više različitih programa od kojih svaki ima svoju run() metodu. Pogledajmo slučaj kad je svaki takav thread podklasa od java.lang.Thread. Sljedeći program je thread koji ispisuje brojeve iod-10 do 9.

public class BytePrinter extends Thread {

 

  public void run() {

    System.out.println("pocinjem!");

    for (int b = -10; b < 10; b++) {

      System.out.println(b);

    }

    System.out.println("gotovo!");

  }

 

}

 

Thread ćete lansirati tako da ga proizvedete instanciranjem odgovarajuće klase, a zatim pozovete njegovu start() metodu. Na primjer, da bismo instancirali klasu BytePrinter, postupit ćemo ovako:

 
BytePrinter bp = new BytePrinter();
 

Ova klasa ima samo pretpostavljeni konstruktor bez argumenata, ali nema nikakvog razloga da klase threadova nemaju i drugačije konstruktore, baš kao i bilo koje druge klase.

Sad možete pozvati njegovu  start() metodu:

bp.start(); 
 

Jednom kad je start() metoda pozvana, izvršavanje progerama se dijeli na dva dijela. Jedan dio CPU vremena koristi se za naredbe koje dolaze iza bp.start(), a drugi dio za izvršavanje threada bp. Ne može se unaprijed predvidjeti koje će naredbe biti izvršene prve. Najvjerojatnije je da će biti pomiješane. Thread bp će se nastaviti izvršavati sve dok ne nastupi jedan od sljedećih sedam uvjeta:

Jednom kad programska kontrola dosegne kraj bp-ove run() metode, thread se gasi. Ne možete ga ponovo pokrenuti, ali možete kreirati novu instancu odgovarajuće klase i nju pokrenuti.


Višestruki threadovi

Sljedeći program lansira tri threada tipa BytePrinter:

 
public class ThreadsTest {
 
  public static void main(String args[]) {
  
    BytePrinter bp1 = new BytePrinter();
    BytePrinter bp2 = new BytePrinter();
    BytePrinter bp3 = new BytePrinter();
    bp1.start();
    bp2.start();
    bp3.start();
      
  }
 
}
 

Redosljed ispisa koje proizvodi ovaj program ovisi o implementaciji i uglavnom je nepredvidljiv. Može izgledati otprilike ovako:

 
% javac BytePrinter.java
% javac ThreadsTest.java
% java ThreadsTest
pocinjem!
-10
. . . 
-4
pocinjem!
pocinjem!
-10
-10
-9
. . . 
9
gotovo!
3
. . . 
9
gotovo!
-3
-2
. . . 
9
gotovo!
%

Imenovanje threadova

Često je korisno dati različitim threadovima iste klase imena po kojima ih možete razlikovati. Sljedeći konstruktor klase Thread vam to omogućuje:

 
public Thread(String name)
 

Uobičajeno ga je pozvati iz konstruktora podklase s kojom radimo, kao što je učinjeno u sljedećem primjeru:

 

public class NamedBytePrinter extends Thread {

 

  public NamedBytePrinter(String name) {

    super(name);

  }

 

  public void run() {

    System.out.println(this.getName() + ": pocinjem!");

    for (int b = -10; b < 10; b++) {

      System.out.println(this.getName() + ": " + b);

    }

    System.out.println(this.getName() + ": gotovo!");

  }

 

}

 

Metoda getName() iz klase Thread vratit će ime threada. Sljedeći program omogućuje razlikovanje ispisanih redaka prema threadovima koji su ih proizveli.

 
public class NamedThreadsTest {
 
  public static void main(String[] args) {
  
    NamedBytePrinter frank = new NamedBytePrinter("Frank");
    NamedBytePrinter mary = new NamedBytePrinter("Mary");
    NamedBytePrinter chris = new NamedBytePrinter("Chris");
    frank.start();
    mary.start();
    chris.start();
  
  }
 
}
 
% javac NamedBytePrinter.java
% javac NamedThreadsTest.java
% java NamedThreadsTest
Frank: pocinjem!
Frank: -10
. . . 
Frank: -4
Mary: pocinjem!
Chris: pocinjem!
Mary: -10
. . . 
Mary: 9
Mary: gotovo!
Chris: 3
. . . 
Chris: gotovo!
Frank: -3
. . . 
Frank: 9
Frank: gotovo!
%

Prioriteti threadova

Nisu svi threadovi jednako važni. Ponekad je potrebno nekom threadu dodijeliti više vremena nego drugom. Threadovi koji komuniciraju s korisnikom trebaju imati vrlo visok prioritet. S druge strane, threadovima koji obavljaju računanja u pozadini treba dodijeliti nizak prioritet.

Prioritet threada zadaje se cijelim brojem između 1 i 10. Deset je najviši prioritet, jedan je najniži. Normalni prioritet je pet. Threadovi s višim prioritetom dobit će više CPU vremena.

Napomena: primijetite da je ovo izravno suprotno načinu na koji se prioriteti definiraju u UNIXu, gdje veći broj označava manji prioritet.

Zbog udobnosti, klasa Thread definira tri mnemoničke konstante, Thread.MAX_PRIORITY, Thread.MIN_PRIORITY i Thread.NORM_PRIORITY koje možete koristiti umjesto numeričkih vrijednosti.

Prioritet threada zadajete pomoću metode setPriority():

setPriority(int newPriority) 

Sljedeći program dodjeljuje threadovima frank, mary i chris različite prioritete. Iako je chris pokrenut zadnji, vjerojatno će biti gotov prvi, budući da mu je dodijeljen najviši prioritet.

public class MixedPriorityTest {
 
  public static void main(String args[]) {
  
    NamedBytePrinter frank = new NamedBytePrinter("Frank");
    NamedBytePrinter mary = new NamedBytePrinter("Mary");
    NamedBytePrinter chris = new NamedBytePrinter("Chris");
    frank.setPriority(Thread.MIN_PRIORITY);
    mary.setPriority(Thread.NORM_PRIORITY);
    chris.setPriority(Thread.MAX_PRIORITY);
    frank.start();
    mary.start();
    chris.start();
  
  }
 
}
 
% javac MixedPriorityTest.java
% java MixedPriorityTest
Chris: pocinjem!
Chris: -10
. . . 
Chris: 9
Chris: gotovo!
Mary: pocinjem!
Mary: -10
. . . 
Mary: 9
Mary: gotovo!
Frank: pocinjem!
Frank: -10
. . . 
Frank: 9
Frank: gotovo!
%

Metoda sleep()

Ponekad je brzina izvršvanja veća nego što želite. U tom slučaju bit će potrebno usporiti izvršavanje pojedinih threadova. To se radi pomoću metode sleep():

public static void sleep(long millis) throws InterruptedException

Ovdje je millis broj milisekundi koji treba proći prije nego thread nastavi s izvršavanjem. Metoda sleep() može odbaciti java.lang.InterruptedException koju je potrebno uhvatiti pa poziv ove metode mora biti unutar try-catch bloka. Na primjer, ako želimo odgoditi izvršavanje za jednu sekundu, stavili bismo:

   try {
    Thread.sleep(1000);
   }
   catch (InterruptedException e) {
    
   }

Sinkronizacija: problem

Do sad smo promatrali threadove koji se izvršavaju nezavisno jedan od drugoga. Ni jedan thread nije morao znati što rade ostali. Ponekad, međutim, threadovi moraju dijeliti podatke. U tom slučaju važn je osigurati da jedan thread ne promijeni podatke u vrijeme dok ih drugi thread koristi. Klasični primjer je pristup datoteci. Ako jedan thread piše u datoteku u vrijeme dok je drugi thread čita, vjerojatno je da će ovaj drugi thread dobiti nekonzistentne podatke. Promotrimo, na primjer, sljedeći problem:

 
public class Counter {
 
  int i = 0;
  
  public void count() {
    int limit = i + 100;
    while (i++ != limit)  System.out.println(i); 
  }
 
}
 
public class CounterThread extends Thread {
    
  Counter c;  
 
  public CounterThread(Counter c) {
    this.c = c;
  }
 
  public void run() {
    c.count();
  }     
 
  public static void main(String[] args) {
  
    Counter c = new Counter();
    CounterThread ct1 = new CounterThread(c);
    CounterThread ct2 = new CounterThread(c);
    ct1.start();
    ct2.start();
 
  } 
}

Program će se ponašati posve nedeterministički. Nema pravila po kojemu bismo mogli predvidjeti kakav će biti izlaz.

% javac Counter.java
% javac CounterThread.java
% java CounterThread
. . . ? ? ? . . .
%

Sinkronizacija: nekoliko pokušaja rješenja

Ključni je problem u prethodnom programu što dva threada modificiraju attribute istog objekta. Pri tome, redosljed modificiranja je neodređen.

Postoji više mogućih rješenja ovog problema. Nisu, međutim, sva rješenja dobra u svim situacijama. Na primjer, jedno od najjednostavnijih i najizravnijih je učiniti objekt nepromjenjivim (immutable), dakle ne dozvoliti da se mijenja nakon što je jednom konstruiran. Nepromjenjivost možete postići tako da sve attribute proglasite za private, klasu ne snabdijete nikakvom setter metodom i ne dozvolite ni jednoj metodi iz klase (osim konstruktorima) da mijenjaju vrijednosti atributa. Međutim, takvo rješenje je neprikladno za naš problem jer metoda count() mora mijenjati atribut i.

Sličnu stvar možete učiniti tako da attribute deklarirate kao final. Na taj način oni se ne mogu mijenjati nakon što je objekt konstruiran. No to je također neprikladno.

U našem primjeru bilo bi jednostavno učiniti varijablu i lokalnom umjesto da bude atribut:

public class Counter {
 
  public void count() {
    int i = 0;
    int limit = 100;
    while (i++ != limit)  System.out.println(i); 
  }
 
}
 

Time što smo i učinili lokalnom varijablom, svaki thread koji poziva metodu count() na tom objektu dobit će svoju vlastitu varijablu i. Svaki put kad se metoda pozove, zasebni stack se konstruira za njene varijable i argumente. Različiti pozivi metode ne dijele varijable.

Međutim, semantika programa više nije ista. Sada svaki thread broji od 0 do 100. Ako je to bila namjera, rješenje je dobro, ali ako smo htjeli da prvi thread broji od 0 do 100, a drugi od 101 do 200, rješenje ne valja.

Ponešto općenitije rješenje koje kombinira prethodna dva bilo bi kopirati vrijednost atributa u lokalnu varijablu, a zatim mijenjati samo nju, ostavljajući atribut nepromijenjen unutar metode. Na primjer:

 
public class Counter {
 
  int i = 0;
 
  public void count() {
    int i = this.i;
    int limit = i + 100;
    while (i++ != limit) System.out.println(i); 
  }
 
}
 

Primijetite kako sada lokalna varijabla i prekriva atribut i, na koji se referiramo pomoću ključne riječi this.

Ovaj trik je uglavnom koristan kad ne trebate vratiti promijenjenu vrijednost varijable natrag u atribut nakon što je metoda završila. Sljedeća varijanta sačuvat će stanje, ali je još uvijek podložna nekim, manje očiglednim, sinkronizacijskim problemima:

public class Counter {
 
  int i = 0;
 
  public void count() {
    int i = this.i;
    int limit = i + 100;
    while (i++ != limit) System.out.println(i);
    this.i = i; 
  }
 
}
 

Ovo je, u stvari, još gore nego originalni primjer jer će u 99% slučajeva raditi dobro i problem će biti gotovo nemoguće uočiti ako ga se ne primijeti u izvornom kodu.


Ključna riječ synchronized

Java vam omogućuje da pod određenim uvjetima možete garantirati da neka metoda neće istodobno biti pozvana od više threadova. Ostali threadovi morat će čekati dok prvi thread ne završi. U međuvremenu oni stoje blokirani.

Ovo se postiže primjenom ključne riječi synchronized na promatranu metodu:

public class SychronizedCounter extends Counter {
 
  public synchronized void count() {
    int limit = i + 100;
    while (i++ != limit) System.out.println(i);
  }
 
}
 

Međutim, sinkronizacija ima svoje nedostatke. Ona bitno smanjuje preformanse. Općenito, sinkronizirane metode su tri do deset puta sporije od ekvivalentnih nesinkroniziranih. Sinkronizacija također ne otklanja automatski sve pogreške koje proizlaze iz threadovskog načina izvršavanja.


Sinkroniziranje na objektima

Svakom objektu dodijeljen je tzv. monitor. Kad ključnu riječ synchronized primjenite na neku metodu, dobijete monitor određenog objekta, odnosno zaključavate ga. Dok god jedan thread posjeduje monitor, odnosno lokot nekog objekta, ni jedan drugi thread ne može dobiti taj lokot. (Drugi threadovi će, eventualno moći dobiti lokote drugih objekata iz iste klase).

Kad koristite ključnu riječ synchronized kako biste specificirali da je neka metoda sinkronizirana, zaključavate određeni objekt kojemu ta metoda pripada. (Statičke metode mogu također biti sinkronizirane. U tom slučaju lokot se stavlja na objekt iz klase java.lang.Class koji je pridružen klasi vašeg objekta.) Kako postoji samo jedan lokot za svaki objekt, nije samo sinkronizirana metoda ta koju ne mogu pozvati ostali threadovi istodobno. Također se to odnosi na ostale inkronizirane metode ili blokove koda u tom objektu.

Sinkronizirati možete i na nižim razinama nego što je razina metode. Na primjer, sljedeći program iamo bi problema ako bi drugi thread promijenio vrijednost od i ili this.i u vrijeme dok se obavlja pridruživanje.

public class Counter {
 
  int i = 0;
 
  public void count() {
    int i = this.i;
    int limit = i + 100;
    while (i++ != limit)  System.out.println(i);
    this.i = i; 
  }
 
}
 

To se može ispraviti tako da sinkronizirate linije koda koje referenciraju taj atribut:

 
  public void count() {
    synchronized (this) {
      int i = this.i;
    }
    int limit = i + 100;
    while (i++ != limit) System.out.println(i);
    synchronized (this) {
      this.i = i; 
    }
  }
 

U ovom primjeru sinkronizirali smo sam objekt, tj. this. Možete također sinkronizirati i druge objekte. Na primjer, sljedeća statička metoda koristi jednostavni bubble sort za sortiranje polja cijelih brojeva. Sinkronizirat ćemo to polje da bismo bili sigurni da ga drugi threadovi neće dirati dok ga sortiramo.

 
  public static void bubbleSort(int[] n) {
  
    boolean sorted = false;
    synchronized(n) {
      while (!sorted) {
        sorted = true;
        for (int i=0; i < n.length - 1; i++) {
          if (n[i] > n[i+1]) {
            int temp = n[i];
            n[i] = n[i+1];
            n[i+1] = temp;
            sorted = false;
          } // end if  
       } // end for
     } // end while
   } // end synchronized
   
 } // end sort 
 

Ovdje se ne brinete o tome da se this objekt možda ne promijeni od strane drugog threada. Na kraju, metoda je statička. Brinete se o tome da drugi threadovi ne promijene polje dok ga sortirate.

Također primijetite da u pravilu ne znate ništa o tome što se događa u programu izvan vaše metode. Možda će biti samo jedan thread koji želi pristup polju, a možda će ih biti mnogo. Da li treba ili ne treba sinkronizirati metodu kao što je ova, ovisi o tome gdje će se ona koristiti. Ako pišete metode i klase opće namjene koje će se koristiti u mnogim različitim programima, morate pretpostaviti da će različiti threadovi pozivati metodu istodobno.

Polja možete sinkronizirati jer su to objekti. Ne možete sinkronizirati primitivne tipove podataka kao što su int, float, ili char.

Stringove možete sinkronizirati jer su objekti, ali je to nepotrebno jer su oni i tako nepromjenjivi. (String ne možete promijeniti, možete samo napraviti novi.)


Primarni thread

U pravilu, prilikom izvršavanja Java programa aktivna su bar tri threada. Najprije, tu je glavni thread unutar kojeg se vaš program izvršava. To je thread koji uključuje main() metodu koja je pokrenula vašu aplikaciju. U appletu, to će biti thread unutar kojeg su lansirani applet viewer ili web browser.

Zatim, tu je garbage collector, thread koji za programom čisti memoriju i obavlja razne finalizacije. Taj thread u pravilu ima niski prioritet.

U programima koji koriste AWT aktivan je još jedan thread koji se brine o ažuriranju ekrana i otprilike 100 puta u sekundi provjerava treba li nešto biti obnovljeno (repainted).

Konačno, tu su svi threadovi koje eksplicitno pokrene vaš program. U svako doba program radi u nekom threadu i nikad ne izlazi iz sustava threadova. Možete ustanoviti koji thread se trenutno izvršava pomoću statičke metode currentThread():

public static Thread currentThread()
 

Na primjer, sljedeći program ispisuje ime primarnog threada izvršavanja:

 
public class PrimaryThread {
 
  public static void main(String[] args) {
 
    System.out.println(Thread.currentThread());
 
  }
 
}
 
% javac PrimaryThread.java
% java PrimaryThread
Thread[main,5,main]
%
 

Metodu currentThread() koristite da biste dobili referencu na trenutačno aktivni thread i njime manipulirali.


Grupe threadova

Threadovi su organizirani u grupe. Grupa threadova je jednostavno kolekcija povezanih threadova. Na primjer, iz sigurnosnih razloga svi threadovi koje lansira neki applet smještaju se u jednu grupu. Appletu je dozvoljeno manipulirati threadovima iz svoje grupe, ali ne i onima iz drugih grupa. Na taj način applet može, na primjer, isključiti sistemski garbage collector, a da pri tome ne smeta ostalim programima.

Grupe threadova organizirane su u hijerarhiju roditelj-djeca.

Sljedeći program ispisuje sve aktivne threadove. Uz pomoć metoda getThreadGroup() iz klase java.lang.Thread i getParent() iz java.lang.ThreadGroup popet će se do najviše grupe threadova, a zatim pomoću activeCount() ustanoviti koliko threadova ima u grupi te pomoću metode enumerate() ispisati sve threadove iz glavne grupe i njene djece (što ovdje pokriva sve grupe threadova, inače postoji i rekurzivna varijanta metode enumerate()).

public class AllThreads {
 
  public static void main(String[] args) {
 
    ThreadGroup top = Thread.currentThread().getThreadGroup();
    while(true) {
      if (top.getParent() != null) top = top.getParent();
      else break;
    }
    Thread[] theThreads = new Thread[top.activeCount()];
    top.enumerate(theThreads);
    for (int i = 0; i < theThreads.length; i++) {
      System.out.println(theThreads[i]);
    }
 
  }
 
}
 

Popis threadova se mijenja i ovisan je o sustavu, no može izgledati na primjer ovako:

 
% javac AllThreads.java
% java AllThreads
Thread[Reference Handler,10,system]
Thread[Finalizer,8,system]
Thread[Signal Dispatcher,10,system]
Thread[main,5,main]
% 

Threadovi demoni

Threadovi koji rade u pozadini kao podrška runtime okruženju nazivaju se demoni (daemon threads). Na primjer, clock handler, idle, garbage collector, screen updater su demoni. that work in the background to support the runtime environment are called daemon threads. Virtualna mašina se zaustavlja kad su svi non-demon threadovi završili.

 

Po pretpostavci, threadovi koje kreirate nisu demoni. Ako želite da oni to postanu, poslužit ćete se metodom setDaemon(true). Da biste ustanovili je li neki thread demon ili nije, pozovite metodu isDaemon().

 
  public final void setDaemon(boolean isDaemon)
  public final boolean isDaemon()

Ustupanje kontrole

Budući da u pravilu ne možete predvidjeti da li će vaš program raditi u kooperativnom ili natjecateljskom (preemptive) okruženju, treba omogućiti da threadovi koji intenzivno koriste CPU s vremena na vrijeme prepuste kontrolu ostalim threadovima. Četiri su situacije u kojima će thread prepustiti kontrolu:

Blokiranje nastaje ako thread mora čekati da se neka operacija završi. Obično su to I/O operacije, posebno one koje uključuju pristup preko mreže. Moguće je blokiranje i zbog čekanja na korisnikov unos. Obično je dobro staviti I/O operacije u odvojene threadove visokog prioriteta jer će računalo na taj način biti efikasnije iskorišteno. Ostali threadovi koji intenzivno koriste CPU obavit će mnogo posla dok čekaju na podatke koji trebaju doći preko mreže ili od korisnika koji treba nešto utipkati.

Kad aktivni thread pozove metodu yield(), on će se privremeno zaustaviti i ustupiti kontrolu drugim threadovima. Virtualna mašina će potražiti ima li threadova istog ili višeg prioriteta koji čekaju i ako ima, dodijelit će kontrolu sljedećem u redu. Ako nema, kontrola će se vratiti threadu koji ju je prepustio. Znači da yield() samo signalizira spremnost za prepuštanje kontrole i ne garantira da će thread zaista biti zaustavljen. To ovisi u potpunosti o drugim threadovima.

Ako thread bezuvjetno želi predati kontrolu na neko vrijeme, bez obzira da li drugi threadovi čekaju ili ne, on može pozvati metodu sleep(). Ona će ga uspavati na određeno vrijeme tijekom kojega drugi threadovi imaju priliku za izvršavanje.

public static void sleep(long milliseconds) throws InterruptedException
public static void sleep(long milliseconds, int nanoseconds) throws InterruptedException
 

Konačno, thread može biti suspendiran na neodređeno vrijeme. On sam, a još češće neki drugi thread, može pozvati metodu suspend() koja će izazvati zaustavljanje threada sve dok ga neki drugi thread ne aktivira pozivanjem metode resume(). Međutim, kao što smo već rekli, ove dvije metode su se pokazale nesigurmina i zato su sada deprecated kao i metoda stop() pa ih, dakle, nemojte ni koristiti.


Spajanje threadova

Spajanje threadova znači da jedan thread čeka dok drugi ne završi. Za to postoje tri overloaded join() metode koje blokiraju thread koji ih je pozvao, tako dugo dok se thread čija join() metoda je pozvana ne ugasi.

 
  public final void join() throws InterruptedException 
  public final void join(long milliseconds) throws InterruptedException 
  public final void join(long milliseconds, int nanoseconds) throws InterruptedException 
 

Na primjer thread koji pozove t.join() čekat će da se thread t završi pa tek onda nastaviti. Ako pozove t.join(1000) čekat će bar jednu sekundu dok se thread t ne završi i tada će nastaviti.

Spajanje threadova korisno je u situacijama kad threadovi ovise jedan o drugom. Na primjer, može se čekati na učitavanje podataka iz datoteke ili na učitavanje slike s mreže.


Sučelje Runnable

Do sada su svi threadovi s kojima smo radili bili podklase od java.lang.Thread. Ponekad, međutim, želite da se kao threadovi ponašaju i klase koje su već podklase drugih klasa. Tipičan primjer je kad želite da se applet ponaša kao thread. U takvim slučajevima implementirat ćete u vašoj klasi sučelje java.lang.Runnable. Ono deklarira jednu jedinu metodu, run().

public void run()
 

Tu ćete metodu implementirati isto kao što biste to učinili da je vaša klasa podklasa od Thread, samo ćete još deklarirati da implementirate sučelje Runnable.

 
public class MyThreadedClass extends SomeClass implements Runnable {
     .
     .
     .   
  public void run() {
         .
         .
         .
  }
 
}
 

Sad ćete prvo kreirati vaš Runnable objekt, a zatim novi objekt tipa Thread tako da kao argument konstruktoru date vaš objekt. Nakon toga možete pozvati start() metodu iz klase Thread:

 
MyThreadedClass mtc = new MyThreadedClass();
Thread t = new Thread(mtc);
t.start(); 

Animacija

Animacija je jedna od glavnih primjena sučelja Runnable. Da biste pokrenuli objekte u Javi, kreirate thread koji računa sukcesivne frameove na ekranu i poziva metodu repaint() za obnovu slike. Možete također postaviti i beskonačnu petlju u vašu paint() metodu, ali to nije dobro, osobito na non-preemptive sustavima kao što je Mac. To također ne osigurava dobru pravovremenost (timing).

Počnimo sa jednostavnom animacijom odbijanja crvene loptice (prikazane kao krug) od unutrašnjih rubova kutije (prikazane kao pravokutnik). Njene koordinate bit će spremljene u atributu r koji je tipa java.awt.Rectangle. Metoda paint() gledat će taj pravokutnik i iscrtati mu upisani krug.

Appletova run() metoda mjesto je gdje se akcija zapravo događa. Ovdje će se povećavati koordinate loptice i provjeravati da li se ona primakla rubu vidljivog područja. Ako jest, odgovarajuća koordinata će se smanjivati.

import java.awt.*;
import java.applet.*;
 
 
public class Bounce extends Applet implements Runnable {
 
  Rectangle r;
  int deltaX = 1;
  int deltaY = 1;
  
  public void init () {  
    r = new Rectangle( 37, 17, 20, 20);
    Thread t = new Thread(this);
    t.start();    
  }
  
  
  public void paint (Graphics g) {
    g.setColor(Color.red);
    g.fillOval(r.x, r.y, r.width, r.height);
  }
 
  public void run() {
 
    while (true) {  // infinite loop
      r.x += deltaX;
      r.y += deltaY;
      if (r.x >= getSize().width  - 20 || r.x < 0) deltaX *= -1;
      if (r.y >= getSize().height – 20 || r.y < 0) deltaY *= -1;
      this.repaint();
    }
    
  }
 
}
 

<APPLET CODE="Bounce.class"

CODEBASE="http://student.math.hr/~vedris/java/classes"

WIDTH=200 HEIGHT=100>

</APPLET>


Pravovremenost

Prava animacija mora osigurati mehanizam pravovremenosti. Animacija koju smo upravo napravili bit će na nekim sustavima prebrza, a na nekima možda i prespora. Potrebno je na neki način regulirati tu brzinu.

Filmski standard je 24 framea u sekundi. Filmovi se tom brzinom i snimaju, a kod projekcije ne želimo da brzina projektora utječe na brzinu izmjene frameova, pogotovo ako različiti projektori imaju različite brzine. To je, inače, razlog, zašto stari nijemi filmovi izgledaju neprirodno brzo. Oni su snimani brzinom od 15 frameova u sekundi, dok današnji projektori rade sa 24 framea u sekundi. Televizijska slika se obnavlja brzinom od 30 frameova u sekundi na više.

Iako ne možete ubrzati animaciju iznad mogućnosti virtualne mašine, možete je usporiti koristeći metodu sleep(). Sljedeći applet ograničava brzinu kretanja loptice na 50 pixela (vertikalno i horizontalno) u sekundi.

import java.awt.*;
import java.applet.*;
import java.util.*;
 
 
public class SleepyBounce extends Applet implements Runnable {
 
  Rectangle r;
  int deltaX = 1;
  int deltaY = 1;
  int speed = 50;
  
  public void init () {  
    r = new Rectangle(37, 17, 20, 20);
    Thread t = new Thread(this);
    t.start();  
  }
  
  
  public void paint (Graphics g) {
    g.setColor(Color.red);
    g.fillOval(r.x, r.y, r.width, r.height);
  }
 
  public void run() {
 
    while (true) {  // infinite loop
      long t1 = (new Date()).getTime();
      r.x += deltaX;
      r.y += deltaY;
      if (r.x >= getSize().width  - 20 || r.x < 0) deltaX *= -1;
      if (r.y >= getSize().height - 20 || r.y < 0) deltaY *= -1;
      this.repaint();
      long t2 = (new Date()).getTime();
      long sleepTime = speed - (t2 - t1);
      if (sleepTime > 0) {
        try {
          Thread.sleep(sleepTime);
        }
        catch (InterruptedException ie) {
        }
        
      } 
      
    }
    
  }
 
}
 

<APPLET CODE="SleepyBounce.class"

CODEBASE="http://student.math.hr/~vedris/java/classes"

WIDTH=200 HEIGHT=100>

</APPLET>


Titranje slike

Možda ste primijetili titranje slike kod izvršavanja ovog appleta. To je uobičajeni problem animacijskih appleta. Nastaje zbog nesinkroniziranosti obnavljanja slike na fizičkom ekranu i obnavljanja slike koje diktira applet. Kad to dvoje nije u skladu, nastaje titranje.

Dva su načina na koje možete riješiti taj problem. Najjednostavnije rješenje je definiranje tzv. clipping područja. To je pravokutnik unutar kojeg možete crtati, a izvan njega ne možete. Definiranjem clipping područja omeđujete prostor u koji možete crtati. To znači da ništa izvan tog područja neće titrati. Titranje je ograničeno samo na to područje. Nadalje, mala površina se može brzo iscrtati pa je i vjerojatnost titranja manja.

Da biste definirali clipping pravokutnik, pozvat ćete metodu g.clipRect(Rect r) unutar vaše paint(). Ovaj applet je upravo i pogodan za to jer već ima gotov pravokutnik koji treba biti clipping. Pogledajmo sada revidirani applet:

import java.awt.*;

import java.applet.*;

import java.util.*;

 

 

public class ClipBounce extends Applet implements Runnable {

 

  Rectangle r;

  int deltaX = 1;

  int deltaY = 1;

  int speed = 50;

 

  public void init () { 

    r = new Rectangle(37, 17, 20, 20);

    Thread t = new Thread(this);

    t.start(); 

  }

 

 

  public void paint (Graphics g) {

    g.setColor(Color.red);

    g.clipRect(r.x, r.y, r.width, r.height);

    g.fillOval(r.x, r.y, r.width, r.height);

  }

 

  public void run() {

 

    while (true) {  // infinite loop

      long t1 = (new Date()).getTime();

      r.x += deltaX;

      r.y += deltaY;

      if (r.x >= getSize().width  - 20 || r.x < 0) deltaX *= -1;

      if (r.y >= getSize().height - 20 || r.y < 0) deltaY *= -1;

      this.repaint();

      long t2 = (new Date()).getTime();

      long sleepTime = speed - (t2 - t1);

      if (sleepTime > 0) {

        try {

          Thread.sleep(sleepTime);

        }

        catch (InterruptedException ie) {

        }

       

      }

     

    }

   

  }

 

}

 

<APPLET CODE="ClipBounce.class"

CODEBASE="http://student.math.hr/~vedris/java/classes"

WIDTH=200 HEIGHT=100>

</APPLET>

 

Drugi i obično efikasniji način izbjegavanja titranja sastoji se u korištenju tehnike tzv. offscreen slika i update() metode. Pregazite li tu metodu, možete obaviti sve crtanje na offscreen sliku, tj. objekt tipa Image, a zatim kopirati gotovi objekt na ekran. Kopiranje slike odvija se puno brže i glatkije nego iscrtavanje pojedinih elemenata na ekran pa u tom slučaju nema vidljivog titranja.

Recept je jednostavan. Kopirajte sljedeći blok koda u svoj program i titranje će nestati. Dodali ste tri private atributa i public metodu update().

  private Image offScreenImage;
  private Dimension offScreenSize;
  private Graphics offScreenGraphics;
 
  public final synchronized void update (Graphics g) {
 
    Dimension d = this.getSize();
    if((this.offScreenImage == null) 
       || (d.width  != this.offScreenSize.width) 
       || (d.height != this.offScreenSize.height)) {
      this.offScreenImage = this.createImage(d.width, d.height);
      this.offScreenSize = d;
      this.offScreenGraphics = this.offScreenImage.getGraphics();
    }
    this.offScreenGraphics.clearRect(0, 0, d.width, d.height);
    this.paint(this.offScreenGraphics);
    g.drawImage(this.offScreenImage, 0, 0, null);
 
  }

Imat ćemo, dakle:

import java.awt.*;

import java.applet.*;

import java.util.*;

 

 

public class FlickBounce extends Applet implements Runnable {

 

  Rectangle r;

  int deltaX = 1;

  int deltaY = 1;

  int speed = 50;

 

  private Image offScreenImage;

  private Dimension offScreenSize;

  private Graphics offScreenGraphics;

 

  public final synchronized void update (Graphics g) {

 

    Dimension d = this.getSize();

    if((this.offScreenImage == null)

       || (d.width  != this.offScreenSize.width)

       || (d.height != this.offScreenSize.height)) {

      this.offScreenImage = this.createImage(d.width, d.height);

      this.offScreenSize = d;

      this.offScreenGraphics = this.offScreenImage.getGraphics();

    }

    this.offScreenGraphics.clearRect(0, 0, d.width, d.height);

    this.paint(this.offScreenGraphics);

    g.drawImage(this.offScreenImage, 0, 0, null);

 

  }

 

  public void init () { 

    r = new Rectangle(37, 17, 20, 20);

    Thread t = new Thread(this);

    t.start(); 

  }

 

 

  public void paint (Graphics g) {

    g.setColor(Color.red);

    g.fillOval(r.x, r.y, r.width, r.height);

  }

 

  public void run() {

 

    while (true) {  // infinite loop

      long t1 = (new Date()).getTime();

      r.x += deltaX;

      r.y += deltaY;

      if (r.x >= getSize().width  - 20 || r.x < 0) deltaX *= -1;

      if (r.y >= getSize().height - 20 || r.y < 0) deltaY *= -1;

      this.repaint();

      long t2 = (new Date()).getTime();

      long sleepTime = speed - (t2 - t1);

      if (sleepTime > 0) {

        try {

          Thread.sleep(sleepTime);

        }

        catch (InterruptedException ie) {

        }

       

      }

      

    }

   

  }

 

}

 

<APPLET CODE="FlickBounce.class"

CODEBASE="http://student.math.hr/~vedris/java/classes"

WIDTH=200 HEIGHT=100>

</APPLET>


Pokretanje i zaustavljanje animacije

Ponekad je dobro je dati korisniku mogućnost da zaustavi thread koji se izvršava. Sa jednostavnim animacijama kao što je ova iz našeg primjera, uobičajeno je da se animacija zaustavlja i pokreće klikanjem miša.

Da bismo to implementirali, dodat ćemo booleovski atribut bouncing koji će biti true ako i samo ako se thread izvršava. Zatim ćemo dodati MouseListener metodu koja zaustavlja thread ako je pokrenut i pokreće ga ako je zaustavljen. Primijetite da smo se ovdje ipak koristili deprecated metodama suspend() i resume(). Pokušajte preraditi ovaj applet tako da ih izbjegnete!

import java.awt.*;

import java.awt.event.*;

import java.applet.*;

import java.util.*;

 

 

public class StartStopBounce extends Applet implements Runnable, MouseListener {

 

  Rectangle r;

  int deltaX = 1;

  int deltaY = 1;

  int speed = 50;

  boolean bouncing = false;

  Thread bounce;

 

  public void init () {

    r = new Rectangle(37, 17, 20, 20);

    addMouseListener(this);

    bounce = new Thread(this);   

  }

 

  public void start() {

    bounce.start();

    bouncing = true;

  }

   

//kod za reduciranje titranja

  private Image offScreenImage;

  private Dimension offScreenSize;

  private Graphics offScreenGraphics;

 

  public final synchronized void update (Graphics g) {

 

    Dimension d = this.getSize();

    if((this.offScreenImage == null) || (d.width != this.offScreenSize.width)

     ||  (d.height != this.offScreenSize.height)) {

      this.offScreenImage = this.createImage(d.width, d.height);

      this.offScreenSize = d;

      this.offScreenGraphics = this.offScreenImage.getGraphics();

    }

    offScreenGraphics.clearRect(0, 0, d.width, d.height);

    this.paint(this.offScreenGraphics);

    g.drawImage(this.offScreenImage, 0, 0, null);

//kraj koda za reduciranje titranja

 

  }

 

  public void paint (Graphics g) {

    g.setColor(Color.red);

    g.fillOval(r.x, r.y, r.width, r.height);

  }

 

  public void run() {

 

    while (true) {  // infinite loop

      long t1 = (new Date()).getTime();

      r.x += deltaX;

      r.y += deltaY;

      if (r.x >= getSize().width -  20 || r.x < 0) deltaX *= -1;

      if (r.y >= getSize().height - 20 || r.y < 0) deltaY *= -1;

      this.repaint();

      long t2 = (new Date()).getTime();

      try {

        Thread.sleep(speed - (t2 - t1));

      }

      catch (InterruptedException ie) {

      }

    }

   

  }

 

  public void mouseClicked(MouseEvent e) {

 

    if (bouncing) {

      bounce.suspend();

      bouncing = false;

    }

    else {

      bounce.resume();

      bouncing = true;

    }

  

  }

 

  public void mousePressed(MouseEvent e) {}

  public void mouseReleased(MouseEvent e) {}

  public void mouseEntered(MouseEvent e) {}

  public void mouseExited(MouseEvent e) {}

 

}

 

<APPLET CODE="StartStopBounce.class"

CODEBASE="http://student.math.hr/~vedris/java/classes"

WIDTH=200 HEIGHT=100>

</APPLET>


Višestruke nezavisne animacije

Ponekad ima smisla pokrenuti više animacija istodobno. U tom slučaju, applet će implementirati sučelje Runnable, a klase animiranih objekata bit će podklase od Thread. Sljedeći program demonstrira to pomoću appleta u kojem se dvije loptice nezavisno odbijaju od rubova.

 

 

import java.awt.*;

import java.applet.*;

 

public class TwoBall extends Applet implements Runnable {

 

  Ball b1, b2;

  Thread t;

 

//kod za reduciranje titranja

 

  private Image offScreenImage;

  private Dimension offScreenSize;

  private Graphics offScreenGraphics;

 

  public final synchronized void update (Graphics g) {

 

    Dimension d = this.getSize();

    if((this.offScreenImage == null)

       || (d.width  != this.offScreenSize.width)

       || (d.height != this.offScreenSize.height)) {

      this.offScreenImage = this.createImage(d.width, d.height);

      this.offScreenSize = d;

      this.offScreenGraphics = this.offScreenImage.getGraphics();

    }

    this.offScreenGraphics.clearRect(0, 0, d.width, d.height);

    this.paint(this.offScreenGraphics);

    g.drawImage(this.offScreenImage, 0, 0, null);

 

  }

//kraj koda za reduciranje titranja

 

 

  public void init () {

 

    b1 = new Ball(10, 32, getSize());

    b1.start();

    b2 = new Ball(155, 75, getSize());

    b2.start();

    t = new Thread(this);

    t.start();

    

  }

 

 

  public void paint (Graphics g) {

    g.setColor(Color.red);

    g.fillOval(b1.getX(), b1.getY(), b1.getWidth(), b1.getHeight());

    g.fillOval(b2.getX(), b2.getY(), b2.getWidth(), b2.getHeight());

  }

 

  public void run() {

 

    Thread.currentThread().setPriority(Thread.MIN_PRIORITY);

 

    while (true) {  // infinite loop

      this.repaint();

      try {

        Thread.sleep(10);

      }

      catch (InterruptedException e) {     

      }

    }

   

  }

 

 

}

 

class Ball extends Thread {

 

  private Rectangle r;

  private int deltaX = 1;

  private int deltaY = 1;

  private Dimension bounds;

 

  public Ball(int x, int y, Dimension d) {

    r = new Rectangle(x, y, 20, 20);

    bounds = d;

  }

 

  public int getX() {

    return r.x;

  }

 

  public int getY() {

    return r.y;

  }

 

  public int getHeight() {

    return r.height;

  }

 

  public int getWidth() {

    return r.width;

  }

 

  public void run() {

 

    Thread.currentThread().setPriority(Thread.MIN_PRIORITY);

 

    while (true) {  // infinite loop

      r.x += deltaX;

      r.y += deltaY;

      if (r.x + r.width >= bounds.width || r.x < 0) {

        deltaX *= -1;

      }

      if (r.y + r.height >= bounds.height || r.y < 0) {  

        deltaY *= -1;

      }

      try {

        Thread.sleep(30);

      }

      catch (InterruptedException e) {  

      }

     

    }

   

  }

 

}

 

<APPLET CODE="TwoBall.class"

CODEBASE="http://student.math.hr/~vedris/java/classes"

WIDTH=200 HEIGHT=100>

</APPLET>