Programarea multithreading
în limbajul Java

O noțiune fundamentală în înțelegerea corectă a thread-urilor este noțiunea de interfață. Cea mai folosită interfață din Java este Runnable. Deoarece thread-urile implementează această interfață, am considerat necesară o introducere a acestei noțiuni.

Interfețe

Noțiuni introductive: În Java, o interfață încapsulează un set coerent de servicii și atribute, fără a atribui aceste funcționalități la obiecte particulare sau la un cod particular. O interfață are membrii de tip constant și metode abstracte. Acest tip (interfață) nu are o implementare ci doar o „semnătură“. Într-o declarație de tip interface, se specifică tipul de date întors și parametri de intrare pentru fiecare metodă a interfeței. O clasă poate fi declarată ca o implementare directă a unei sau mai multor interfețe, însemnând că toate metodele abstracte ale interfeței trebuie implementate.

Exemplu:

class Point{int x,y;}
 	  interface Point {
 	  void move(int dx, int dy);
 	  }  	 

În exemplu de mai sus, va apărea o eroare de compilare deoarece o clasă și o interfață nu pot avea același nume.

Inițializarea câmpurilor din interfețe: Toți membrii interfeței sunt implicit de tip public.

Fiecare câmp din corpul interfeței trebuie să aibă o expresie de inițializare, care nu trebuie să fie o expresie constantă (adică declarată cu cuvântul cheie final). Variabila de inițializare este evaluată și asignată exact o dată, când interfața este inițializată.

O eroare de compilare apare dacă o expresie de inițializare pentru un câmp conține o referință la un nume de câmp care este declarat mai târziu. Astfel:

interfaceTest{
 	  float f=j;
 	  int j=1;
 	  int k=k+1; 
 	  } 

provoacă două erori de compilare deoarece j este referit la inițializarea lui f, înainte ca j să fie declarat și deoarece inițializarea lui k se referă la însuși k.

Câmpuri moștenite în mod ambiguu: O interfață poate extinde altă interfață predefinită, sau definită de programator.

Exemplu:

interface BaseColors{
 	  int RED=1,GREEN=2,
 	  BLUE=4;
 	  }  	 
interface RainbowColors extends BaseColors{
 	  int YELLOW=3,ORANGE=
 	  5,INDIGO=6,VIOLET=7;
 	  } 	 
interface PrintColors extends BaseColors{
 	  int YELLOW=8,CYAN=
 	  16,MAGENTA=32;
 	  } 	 
interface LotsOfColors extends RainbowColors, PrintColors{
 	  int FUCHSIA=17, 
 	  VERMILION=43, 
 	  CHARTREUSE=RED+90;
 	  } 	 

Interfața LotsOfColors moștenește 2 câmpuri numite YELLOW. Acest lucru este corect atâta timp cât interfața nu conține o referință la câmpul YELLOW.

Chiar dacă interfața PrintColors ar avea pentru YELLOW valoarea 3 în loc de 8, o referință la câmpul YELLOW, din interfața LotsOfColors, tot ar fi fost considerată ambiguă.

Declarațiile metodelor abstracte: Fiecare metodă declarată în corpul unei interfețe este implicit de tipul abstract. Pentru compatibilitate cu versiunile mai vechi de Java, este permisă dar nu este recomandată specificarea cuvântului cheie abstract pentru metodele din interiorul unei interfețe.

Fiecare metodă declarată, în corpul unei interfețe, este implicit de tipul public. Este permisă dar nu este recomandată specificarea cuvântului cheie public pentru metodele din interiorul unei interfețe.

Dacă o metodă declarată într-o interfață este declarată de tipul native sau synchronized, atunci va apărea eroare la compilare, deoarece aceste cuvinte cheie descriu proprietățile unei implementări decât proprietățile interfeței. Oricum o metodă declarată într-o interfață poate fi implementată ca o metodă de tip native sau synchronized.

Dacă o metodă dintr-o interfață este declarată de tip final, atunci va apărea o eroare de compilare. Oricum orice metodă a interfeței poate fi implementată ca o metodă de tip final.

Interfață de tip Runnable:

Una din cele mai folosite interfețe din Java este interfața Runnable. Este foarte importantă deoarece este implementată de clasa Thread (fir de execuție, în sensul pe care îl are și în C++)

Interfața Runnable asigură un protocol comun obiectelor care doresc să-și execute codul, atâta timp cât sunt active. Un obiect este activ dacă a fost lansat în execuție și nu a fost oprit. Inima unui obiect, care implementează clasa Runnable, este metoda run(). Această metodă trebuie suprascrisă de obiectul de tip Runnable. De altfel, este singura metodă a interfeței Runnable.

Interfața se numește Runnable și nu Running, deoarece ea nu se execută chiar tot timpul. Pe marea majoritate a mașinilor se află un singur procesor care este ocupat și cu alte sarcini. Doar o parte din timpul de lucru al procesorului este alocată obiectului de tip Runnable. Exemplu:

import java.io.*; 	 
// clasa Simple afiseaza message de number ori
 	  class Simple implements Runnable{
 	  protected String message;
 	  protected int number; 	 
 // constructorul clasei
 	  public Simple (String m, int n){
 	  message = m;
 	  number=n;
 	  } 	 
 public void run(){
 	  int i=0;
 	  while(i++<number){
 	  System.out.println(mes
 	  sage);
 	  }
 	  }
 	  } 	 
public class prima_ora{
 	  public static void main(String args[]){
 	  Simple ob1=new Simple("par",10);
 	  Simple ob2=new Simple("impar",9);
 	  ob1.run();
 	  ob2.run();
 	  }
 	  }

Rezultatul execuției programului precedent va fi afișarea mesajului par de 10 ori și a mesajului impar de 9 ori.

Se poate atribui un obiect altui obiect. Exemplu:

import java.io.*; 	 
// clasa Simple afisează message de number ori
 	  class Simple implements Runnable{ 	 
 protected String message;
 	  protected int number; 	 
 // constructorul clasei
 	  public Simple (String m, int n){
 	  message = m;
 	  number=n;
 	  } 	 
 public void run(){
 	  int i=0;
 	  while(i++<number){
 	  System.out.println(mes
 	  sage);
 	  }
 	  }
 	  }
 	  public class atribuire{
 	  public static void main(String args[]){
 	  Simple ob1=new Simple("par",4);
 	  Simple ob2=new Simple("impar",3);
 	  ob1.run();
 	  ob2.run();
 	  ob1=ob2;
 	  System.out.println("după atribuire");
 	  ob1.run();
 	  System.out.println("OK");
 	  ob2.run();
 	  }
 	  } 	 

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


BYTE România - noiembrie 1997


(C) Copyright Computer Press Agora