Rezultatul execuției: afișarea mesajului par de 4 ori, afișarea
mesajului impar de 3 ori, afișarea mesajului după atribuire,
afișarea mesajului impar de 3 ori, afișarea mesajului OK,afișarea
mesajului impar de 3 ori.
Limitări ale paralelismului: Limbajul Java este conceput pentru
a suporta programarea concurentă. Fără îndoială, caracterul de multithreading
al aplicațiilor este dezvoltat tot mai mult în industria soft. Pe lângă
avantajele oferite de astfel de aplicații, apar și unele puncte delicate
care se cer cu grijă tratate de către programatori.
Siguranța: Când thread-urile nu sunt complet independente, în
timpul execuției, fiecare poate influența celelalte thread-uri. Pentru a
evita acest lucru, obiectele de tip thread pot folosi mecanismele de sincronizare
sau tehnici de excluziune care să prevină intercalarea execuțiilor. Utilizarea
thread-urilor multiple, implicând obiecte proiectate pentru a lucra în mod
secvențial, conduce la programe greu de citit și greu de depanat.
Timpul de viață: Activitățile din programele concurente se pot
opri pur și simplu, dintr-o varietate de motive. De exemplu din cauză că
alte activități consumă cicluri CPU sau din cauză că 2 activități diferite
sunt blocate, fiecare așteptând pe cealaltă pentru a continua.
Nedeterminismul: Activitățile cu caracter multithread pot fi intercalate
în mod arbitrar. Nici un program nu rulează identic la 2 execuții. Activitățile
ce necesită un mare volum de calcule se pot întrerupe înainte ca aceste
calcule să fie satisfăcute. Acest lucru face ca programele multithreading
să fie greu de înțeles, de depanat și greu predictibile. Construcția unui
thread, setarea lui și utilizarea metodelor determină un consum de memorie
mai ridicat decât folosirea obiectelor normale.
Sincronizarea: Metodele Java implicate în sincronizare sunt mai
lente decât cele care nu beneficiază de protecția oferită de sistemul de
sincronizare.
Fire de execuție (Thread-uri)
Thread-urile pot fi folosite în 2 moduri. Prima metodă constă în implementarea
unei interfețe Runnable. Exemplu:
public class Simple implements Runnable{
protected String message;
protected TextArea text;
//constructorul clasei
public Simple (String m, TextArea t){
message = m;
text = t;
}
public void run(){
text.appendText(message);
}
}
Obiect de tip Runnable: Clasa Simple implementează o interfață
de tip Runnable. Această interfață are doar o singură metodă run(),
nu are argumente la intrare și nu întoarce rezultate.
public interface Runnable{
public void run();
}
Interfețele sunt mai abstracte decât clasele, deoarece ele nu spun nimic
despre reprezentare sau cod. Ele descriu doar semnătura (nume, argumente
și tipuri de rezultate) ale operațiilor publice. Clasele ce implementează
interfața Runnable nu au nimic în comun exceptând folosirea unei
metode run(). Versiunea secvențială a programului:
public class Sapplet extends Applet{
protected TextArea text;
protected Simple hello;
protected Simple bye;
public Sapplet(){
text = new TextArea(4, 40); // 4 randuri si 40 coloane
hello = new Simple("Hello\n", text);
bye = new Simple("Bye\n", text);
}
public void init(){
add(text);
}
public void start(){
hello.run();
bye.run();
}
}
Clasa Sapplet va fi executată secvențial. După invocarea metodei start(),
se va continua cu rularea primului obiect (hello) până la execuția completă.
După ce programul revine din execuție prin instrucțiunea return din metoda
run(), se începe rularea celui de-al doilea obiect (bye). Prin urmare, în
fereastra TextArea vor apărea cele două mesaje succesiv și complet separate.
În acest exemplu, nu avem o aplicație multithreading, execuția celor două
obiecte de tip Runnable fiind făcută în mod secvențial.
Versiunea multithreading: A doua cale de folosire a interfeței
Runnable constă în crearea unui nou thread folosind metoda new Thread(Runnable
x) Exemplu:
public class TApplet extends Applet{
protected TextArea text;
protected Simple hello;
protected Simple bye;
public TApplet(){
text=new TextArea(4,40);
hello=new Simple("Hello\n", text);
bye=new Simple("Bye\n", text);
}
public void init(){
add(text);
}
public void start(){
new Thread(hello).start();
new Thread(bye).start();
}
}
Observație: Metoda care este apelată în thread este start(). Thread.start()
determină execuția metodei Runnable.run() Se știe că obiectul de tip applet
deține și el o metodă start() care nu are nici o legătură cu metoda cu aceași
nume din Thread.
Sincronizarea: Când 2 sau mai multe thread-uri accesează același
obiect, ele pot interfera.
Instrumentul principal, pentru evitarea acestei interferențe, este mecanismul
de sincronizare. Principalul merit al sincronizării este asigurarea că un
singur thread obține accesul la un obiect într-un moment dat. Exemplu:
Dacă java.awt.TextArea nu ar fi fost implementat folosind sincronizarea,
am fi putut face un mic program de ajutor care asigură că un singur thread
execută TextArea.appendText() la un moment dat.
Class Appender{
private TextArea text;
public Appender(TextArea t){
text=t;
}
synchronized void append(String s){
text.appendText(s);
}
}
public class ThreadApplet extends Applet{
protected TextArea text;
protected Appender appender;
protected Simple hello;
protected Simple bye;
public ThreadApplet(){
text=new TextArea(4,40);
appender=new Appender(text);
hello=new Simple("Hello\n",appender);
bye=new Simple("Bye\n",appen
der);
}
public void init(){
add(text);
}
public void start(){
new Thread(hello).start();
new Thread(bye).start();
}
}
public class Simple{
protected Appender appender;
protected String message;
public Simple (String m, Appen
der a){
message=m;
appender=a;
}
public void run(){
appender.append(message);
}
}
Metodele de control ale thread-ului:
start(): determină apelarea
metodei run() ca o activitate independentă. Fără o instrucțiune specială,
cum ar fi stop(), rularea thread-ului se termină când metoda run() întoarce
return.
isAlive(): întoarce true dacă un thread a fost startat dar nu a fost încă
terminat.
stop(): termină irevocabil activitatea unui thread. Nu are loc omorârea
thread-ului, doar activitatea îi este stopată. Deci metoda start() poate
fi din nou apelată pentru același obiect de tip Thread.
suspend(): oprește temporar thread-ul ce va continua execuția după invocarea
metodei resume().
sleep(): determină suspendarea execuției unui thread pentru un timp dat
în milisecunde. Thread-ul poate să nu continue imediat după timpul dat dacă
există alt thread activ.
interrupt(): determină întreruperea instrucțiunilor sleep(), wait() cu
o InterruptException care poate fi prinsă, captată și prelucrată într-un
mod specific aplicației.
Priorități: Cazul fericit al rulării programelor multithreading
este cazul multiprocesor. Majoritatea mașinilor disponibile la ora actuală
pe piață au un singur procesor, acesta prin capacitatea de efectuare a unui
număr tot mai mare de operații pe secundă putând face destul de bine fată
aplicațiilor multithreading. Un thread este runnable, dacă a fost startat
dar nu a fost terminat, suspendat , blocat și nu este angajat într-o instrucțiune
wait().
Când nu sunt în rulare, thread-urile sunt în așteptare într-o coadă,
aranjată în funcție de priorități. Această coadă este gestionată de sistemul
de rulare Java. Prioritățile pot fi schimbate prin apelul instrucțiunii
Thread.setPriority cu un argument cuprins între Thread.MIN_PRIORITY și MAX_PRIORITY.
Instrucțiunea Thread .yield recapătă controlul pentru un thread de prioritate
egală cu celelalte. Dacă o metodă nu este marcată ca synchronized,
atunci poate fi executată imediat ce este apelată, chiar în timp ce altă
metodă sincronizată rulează. Calificativul de synchronized nu poate
fi transferat în subclase. O subclasă trebuie declarată explicit synchronized,
altfel ea va fi tratată ca una obișnuită.
Metodele declarate în interfețele Java nu pot fi înzestrate cu calificativul
de syncronized. După cum se știe, metodele din interfețe nu furnizează
informații cu privire la cod, ele sunt propriu-zis o semnătură a metodei
(specifică tipul argumentelor de intrare și tipul de date întors de metodă).
Aceste metode trebuie suprascrise de către programator. Un exemplu de metodă
interfață este metoda run din interfața Runnable.
Wait și Notification: Ca urmare a executării instrucțiunii wait():
thread-ul curent este suspendat
sistemul de rulare Java plasează thread-ul într-o coadă de așteptare internă
și inaccesibilă programatorului.
Ca urmare a executării instrucțiunii notify():
Din coada de așteptare internă este scos în mod arbitrar un thread.
Acest thread trebuie să obțină blocajul de sincronizare pentru obiectul
țintă, care întotdeauna va determina blocarea cel puțin pană thread-ul va
chema metoda notify .
Thread-ul este atunci reluat din punctul unde apare metoda wait.
Invocarea metodei notifyAll:
Invocarea metodei notifyAll lucrează în același mod ca și notify numai
că pașii de mai sus se aplica la toate thread-urile ce așteaptă în coada
de așteptare pentru obiectul țintă.
Două versiuni alternative ale metodei wait preia, argumente specificând
timpul maxim de așteptare în coadă. Dacă timpul de așteptare este depășit,
atunci metoda notify este invocată automat.
Dacă o instrucțiune interrupt apare în timpul execuției unei instrucțiuni
wait, același mecanism notify se aplică exceptând controlul întors către
clauza catch asociată cu invocarea lui wait.
Aplicație
Structura aplicației: Ca o ilustrare a noțiunilor prezentate mai
sus, este listat codul unei aplicații multithreading.
Interfața cu utilizatorul este dată de 3 butoane numite first, second,
third. În spațiul de deasupra lor, 3 thread-uri care rulează concurent
afișează numere consecutive de la 0 la 9. De observat că afișarea are un
caracter ciclic, după cifra 9 urmând cifra 0. Cifra de pornire nu este 0,
ci este generată printr-un generator Java de numere aleatoare și este diferită
în cazul fiecărui thread. Procesul de afișare este întrerupt prin apăsarea
butonului corespunzător fiecărui thread, caz în care este apelată metoda
thread.stop(). Reîmprospătarea periodică a ecranului și captarea acțiunii
asupra butoanelor sunt asigurate de un al patrulea fir de execuție din programul
principal. Acest thread se va opri când toate celelalte thread-uri sunt
oprite.
Ca și structură, programul definește o clasă (Machine) ce extinde clasa
Thread. Această clasă are următoarele atribute:
initial_value cifra inițială generată aleator, de la care se începe afișarea
cifrelor.
counter variabila ce va fi afișată și care ia valori de la 0 la 9, în
mod ciclic.
x_draw și y_draw coordonatele la care o instantiere a acestei clase va
începe să afișeze.
Metoda Thread.isAlive() testează starea firului de execuție care a apelat
metoda.
Considerații finale:
Pentru cei interesați câteva sugestii de îmbunătățire a aplicației. Ar
fi utilă înzestrarea aplicației cu un buton start care să repornească întregul
joc de la început. De asemenea, ar fi interesant dacă utilizatorul ar putea
să fixeze rapiditatea derulării cifrelor pe ecran pentru fiecare thread
în parte, între niște valori fixate de programator.
Bibliografie:
Doug Lea
Concurrent Programming
în Java- Design Principles and Patterns
Addison-Wesley 1996