Introducere

De multe ori este nevoie ca o aplicaţie să poată să realizeze simultan mai multe activităţi (de exemplu o aplicaţie de tip server va trebui să fie capabilă să deservească simultan mai mulţi clienţi). Firele de execuţie reprezintă mecanismul prin care pot fi implementate în cadrul unui program secvenţe de cod ce se execută virtual în paralel.

Figura 1. Fire de execuţie

Trebuie făcută distincţie între fire de execuţie şi procese. Deşi ambele concepte implică execuţia în paralel a unor secvenţe de cod există o serie de deosebiri fundamentale între procese şi fire de execuţie. Procesele sunt entităţi independente ce se execută independent şi sunt gestionate de către nucleul sistemului de operare. Firele de execuţie sunt secvenţe ale unui program (proces) ce se execută aparent în paralel în cadrul unui singur proces.

Stările unui fir de execuţie

Un fir de execuţie se poate afla in una dintre următoarele 4 stări:

  • New – obiectul fir de execuţie a fost creat dar înca nu a fost startat.
  • Runnable – Firul se afla in starea in care poate fi rulat in momentul in care procesorul devine disponibil.
  • Dead – Calea normala prin care un fir se termina este prin ieşirea din metoda run(). Se poate forţa terminarea. firului apelând metoda stop() dar nu se recomanda folosirea sa, fiind o metoda “deprecated” in Java2.
  • Blocked – Firul de execuţie este blocat si nu poate fi rulat, chiar daca procesorul este disponibil.

Figura 2. Stările unui fir de execuţie

Construirea şi startarea firelor de execuţie

După cum se ştie punctul de start al unei aplicaţii Java este funcţia main. În momentul lansării în execuţie a aplicaţiei aceasta va conţine un fir de execuţie ce va executa această funcţie main. Aşadar o aplicaţie java conţine cel puţin un fir de execuţie.

Clasa java ce implementează mecanismele necesare pentru a iniţializa şi executa un fir de execuţie este clasa Thread.

Pentru a construi un fir de execuţie trebuie extinsă clasa Thread şi suprascrisă metoda run() din cadrul acestea. În cadrul metodei run() se va implementa secvenţa de instrucţiuni ce se va executa în momentul în care un fir de execuţie este startat.

Pentru a lansa în execuţie un fir se foloseşte metoda start() (moştenită de asemenea din cadrul clasei Thread). Această metodă este responsabilă cu iniţializarea tuturor mecanismelor necesare pentru a putea executa un fir de execuţie, după care va apela automat metoda run().

public class Counter extends Thread {
 
      Counter(String name){
            super(name);
      }
 
      public void run(){
            for(int i=0;i<20;i++){
                  System.out.println(getName() + " i = "+i);
                  try {
                        Thread.sleep((int)(Math.random() * 1000));
                  } catch (InterruptedException e) {
                        e.printStackTrace();
                  }
            }
            System.out.println(getName() + " job finalised.");
      }
 
      public static void main(String[] args) {
            Counter c1 = new Counter("counter1");
            Counter c2 = new Counter("counter2");
            Counter c3 = new Counter("counter3");
 
            c1.start();
            c2.start();
            c3.start();
      }
}

Metoda run() nu trebuie apelata in mod explicit de către programator pentru că aceasta va fi apelată în mod automat atunci când firul de execuţie este lansat în execuţie folosind metoda start().

Un fir de execuţie o data startat nu va mai putea fi startat înca odata. Daca se încearcă apelarea metodei start() pentru un fir deja pornit, se va genera o eroare.

Limbajul java nu permite moştenire multiplă. Aceasta înseamnă ca dacă avem o clasă care moşteneşte deja o clasă, şi dorim să o transformăm în fir de execuţie nu v-om putea folosi metoda prezentată anterior de extindere a clasei Thread. Pentru a putea rezolva astfel de situaţii java pune la dispoziţie o a doua metodă de construire a firelor de execuţie folosind interfaţa Runnable.

În continuare se va prezenta aplicaţia anterioară modificată pentru a folosi interfaţa Runnable în scopul construirii firelor de execuţie.

public class CounterRunnable implements Runnable {
 
      public void run(){
            Thread t = Thread.currentThread();
            for(int i=0;i<20;i++){
                  System.out.println(t.getName() + " i = "+i);
                  try {
                        Thread.sleep((int)(Math.random() * 1000));
                  } catch (InterruptedException e) {
                        e.printStackTrace();
                  }
            }
            System.out.println(t.getName() + " job finalised.");
      }
 
      public static void main(String[] args) {
            CounterRunnable c1 = new CounterRunnable();
            CounterRunnable c2 = new CounterRunnable();
            CounterRunnable c3 = new CounterRunnable();
 
            Thread t1 = new Thread(c1,"conuter1");
            Thread t2 = new Thread(c2,"conuter2");
            Thread t3 = new Thread(c3,"conuter3");
 
            t1.start();
            t2.start();
            t3.start();
      }
}

Terminarea firelor de execuţie

O deosebita atenţie trebuie data modului in care un fir de execuţie se termina. În cadrul clasei Thread există definită metoda stop() ce poate fi folosită pentru terminarea unui fir, dar, nu este recomandată, de altfel, aceasta metoda este “deprecated” (nu mai este recomandată de către Sun pentru a fi utilizată). Calea recomandata de terminare a unui fir este prin revenirea normala ( return ) din run(). Aşadar in cadrul metodei run se recomandă adăugarea unor blocuri condiţinale care să determine terminarea firului de execuţie, respectiv ieşirea din metoda run.

Acest mecanism nu este util de fiecare data deoarece in momentul in care un fir este blocat ( datorita unor instrucţiuni wai(), sleep(), join(), operatii I/O ), acesta nu poate verifica nici o condiţie. Pentru a termina un fir care este blocat se utilizează metoda interrup(). Aceasta metoda determina aruncarea unei excepţii InterruptedException care trebuie prinsa.

Prioritatea firelor de execuţie

Prioritatea unui fir de execuţie spune planificatorului de execuţie a firelor cat de important este acesta, şi ce prioritate trebuie să i se acorde pentru executarea instrucţiunilor sale. Aceasta insemnă că dacă mai multe fire de execuţie sunt blocate in aşteptarea execuţiei, planificatorul îl va alege prima data pe cel cu prioritate mai mare. Aceasta nu înseamnă ca firele cu prioritate mai mica nu vor avea alocat timp din procesor.

Pentru a seta prioritatea unui fir se utilizează metoda setPriority(), iar pentru obţinerea priorităţii unui fir se utilizează metoda getPriority() (metodele fac parte din clasa Thread) . Firele pot primi priorităţi intre valorile MIN_PRIORITY si MAX_PRIORITY (constante definite în clasa Thread).

import java.awt.*;
import javax.swing.*;
 
public class MainFrame extends JFrame{
 
      public MainFrame(){
            setTitle("Thread priority test.");
            setDefaultCloseOperation(EXIT_ON_CLOSE);
            setLayout(new GridLayout(5,1));
            setSize(300,300);setVisible(true);
      }
 
      public void addNewThreadComponent(CounterX x){
            JPanel p = new JPanel();
            p.setLayout(new FlowLayout());     
            p.add(new JLabel(x.getName()));
            p.add(x.getProgressBar());
            add(p);
      }
 
      public static void main(String[] args) {
            MainFrame mf = new MainFrame();
            CounterX c1 = new CounterX(1000,1);
            mf.addNewThreadComponent(c1);
 
            CounterX c2 = new CounterX(1000,5);
            mf.addNewThreadComponent(c2);
 
            CounterX c3 = new CounterX(1000,10);
            mf.addNewThreadComponent(c3);
 
            c1.start();
            c2.start();
            c3.start();
      }
}
 
class CounterX extends Thread{
      int size;
      JProgressBar pBar;
 
      CounterX(int s, int priority){
            this.size = s;
            pBar = new JProgressBar(0,size);
            pBar.setStringPainted(true);
            this.setPriority(priority);
 
      }
 
      JComponent getProgressBar(){return pBar;}
 
      public void run(){
            for(int i=0;i<=size;i++){
                  try {Thread.sleep(1);
                  } catch (InterruptedException e) {
                        e.printStackTrace();
                  }
                  pBar.setValue(i);
            }
      }
}

Important: Programatorul nu trebuie sa se bazeze pe priorităţi in construirea programului, întrucât acesta poate da rezultate diferite pe sisteme diferite.

Metoda join()

Metoda join() aparţine clasei Thread si este utilizata pentru a determina un fir de execuţie să aştepte terminarea unui alt fir de execuţie.

class JoinTest extends Thread
{
      String n;
      Thread t;
      JoinTest(String n, Thread t){this.n = n;this.t=t;}
 
      public void run()
      {
            System.out.println("Firul "+n+" a intrat in metoda run()");
            try
            {                
                  if (t!=null) t.join();
                  System.out.println("Firul "+n+" executa operatie.");
                  Thread.sleep(3000);
                  System.out.println("Firul "+n+" a terminat operatia.");
            }
            catch(Exception e){e.printStackTrace();} 
 
      }
 
public static void main(String[] args)
{
      JoinTest w1 = new JoinTest("Proces 1",null);
      JoinTest w2 = new JoinTest("Proces 2",w1);
      w1.start();
      w2.start();
}
}

Sincronizarea firelor

Unul dintre aspectele cele mai importante de care trebuie ţinut cont atunci când se lucrează cu fire de execuţie este modul in care acestea accesează resursele comune. Astfel in momentul in care doua sau mai multe fire împart o resursa comuna acestea trebuie sa î-şi sincronizeze accesul la acea resursa.

Un exemplu de astfel de situaţie este cel al firelor consumator\producator. Un fir introduce date intr-un buffer, iar cel de al doilea citeşte datele puse de primul fir in buffer. Evident cele doua fire nu trebuie sa acceseze simultan bufferul si trebuie sa aibă un acces sincronizat la acesta.

Prevenirea coliziunii in java se face utilizând metodele sincronizate sau blocurile sincronizate. O metoda este sincronizata atunci când are in fata cuvântul cheie synchronized.

                synchronized void f() { /* ... */ }
                synchronized void g(){ /* ... */ }

Un bloc sincronizat este definit sub forma:

                synchronized(obiect){...}

Numai un singur fir poate accesa la un moment dat o metoda sincronizata a unui obiect. Fiecare obiect are inclus (automat) un monitor sau un zăvor. In momentul in care o metoda sincronizata a unui obiect este apelata, monitorul este achiziţionat de către firul care a accesat metoda. Atâta timp cat metoda sincronizata este in execuţie nici o alta metoda sincronizata nu va mai putea fi apelata de către un alt fir.

Important: In momentul in care un fir încearcă sa acceseze o metoda sincronizata a unui obiect care are monitorul acaparat de către un alt fir, acesta se blochează in aşteptarea eliberării monitorului.

public class TestSincronizare {
public static void main(String[] args) {
    Punct p = new Punct();
    FirSet fs1 = new FirSet(p);
    FirGet fg1 = new FirGet(p);
 
    fs1.start();
    fg1.start();
}
}
 
class FirGet extends Thread {
    Punct p;
 
    public FirGet(Punct p){
        this.p = p;
    }
 
    public void run(){
        int i=0;
        int a,b;
        while(++i<15){         
           // synchronized(p){
            a= p.getX();          
            try {
                sleep(50);
            } catch (InterruptedException e) {  
                e.printStackTrace();
            }         
            b = p.getY();
           // }
            System.out.println("Am citit: ["+a+","+b+"]");
        }
    }
}//.class
 
 
class FirSet extends Thread {
    Punct p;
    public FirSet(Punct p){
        this.p = p;
    } 
    public void run(){
        int i =0;
        while(++i<15){
            int a = (int)Math.round(10*Math.random()+10);
            int b = (int)Math.round(10*Math.random()+10);
 
            //synchronized(p){
            p.setXY(a,b);
            // }
 
            try {
                sleep(10);
            } catch (InterruptedException e) {
 
                e.printStackTrace();
            }
            System.out.println("Am scris: ["+a+","+b+"]");
        }
    }
}//.class
 
class Punct {
    int x,y;
    public void setXY(int a,int b){
        x = a;y = b;
    }  
    public int getX(){return x;}
    public int getY(){return y;}   
}

Interblocaje (deadlocks)

Folosirea blocurilor sincronizatea în mod greşit poate duce la situaţii de interblocaje (eng. deadlock) între firele de execuţie. Astfel de situaţii apar atunci când două fire sunt blocate, fiecare aşteptând unul după celălalt eliberarea unui monitor.

Aplicaţia următoare exemplifică apariţia situaţiei de interblocaj între două fire ce folosesc blocuri sincronizate.

public class Deadlock {
 
    public static void main(String[] args) {
        final Robot alphonse = new Robot("Alphonse");
        final Robot gaston = new Robot("Gaston");
        new Thread(new Runnable() {
            public void run() { alphonse.proceseazaPiesa(gaston); }
        }).start();
        new Thread(new Runnable() {
            public void run() { gaston.proceseazaPiesa(alphonse); }
        }).start();
    }
}
 
class Robot {
    private final String name;
    Piesa piesa;
    public Robot(String name) {
        this.name = name;
        this.piesa = new Piesa();
    }
    public String getName() {
        return this.name;
    }
    public synchronized void proceseazaPiesa(Robot r) {
        System.out.println(name+" proceseaza piesa ");
        piesa.procesare();
        r.primestePiesa(this);
    }
    public synchronized void primestePiesa(Robot r) {
        System.out.println(r.getName()+ " a transmis piesa catre "+name);
        this.piesa = r.getPiesa();
    }
 
      private Piesa getPiesa() {
            return piesa;
      }
}
 
class Piesa{
      public void procesare(){
            System.out.println("Piesa se proceseaza");
            try {
                  Thread.sleep(100);
            } catch (InterruptedException e) {
                  e.printStackTrace();
            }
            }
}

Sincronizarea firelor utilizând metodele wait(), notify() si notifyAll()

Metodele wait() si notify() sunt utilizate pentru a bloca şi debloca firele de execuţie. Aceste două metode fac parte din clasa Object. Motivul acestei poziţionări este că acestea manipulează monitoarele obiectelor, si, la rândul lor, monitoarele se găsesc la nivelul oricărui obiect.

Metodele wait si notify pot fi apelate doar din interiorul unor blocuri sau metode sincronizate. Ceea ce înseamnă ca aceste metode, pentru un obiect data, pot fi apelate doar de către deţinătorul monitorului obiectului.

In momentul in care un fir este blocat prin apelarea metodei wait() monitorul deţinut de respectivul fir este eliberat.

Metoda wait(milis) poate fi utilizata in locul metodei Thread.sleep(milis), avantajul fiind ca firul poate fi deblocat înainte de expirarea timpului de aşteptare (ceea ce in cazul sleep() nu este posibil).

Important:Nu utilizaţi metodele stop() si suspend() deoarece folosirea acestora in cadrul unor blocuri sincronizate duce la deadlok întrucât monitorul nu este eliberat.

public class Test {
    public static void main(String[] args){
        Buffer b = new Buffer();
            Producer pro = new Producer(b);
            Consumer c = new Consumer(b);
            Consumer c2 = new Consumer(b);
            //Lanseaza cele 3 fire de executie. Se observa ca cele 3 fire de executie
            // folosesc in comun obiectul b de tip Buffer. Exista un fir pro ce este
            // responsabil cu adaugarea de elemente in buffer si doua obiecte
            // responsabile cu extragerea elementelor din buffer.
            pro.start();
            c.start();
            c2.start();
    }
}
 
/**
 * Aceasta este o clasa de tip fir de executie. In cadrul unei bucle infinite sunt
 * generate numere de tip double si sunt adaugate in cadrul unui obiect de tip Buffer
 * apeland metoda put. Aduagare elementelor se face la intervale de 1 secunda.
 *
 */
class Producer implements Runnable
{
      private Buffer bf;
      private Thread thread;
      Producer(Buffer bf){this.bf=bf;}
 
      public void start()
      {
            if (thread==null)
            {
                  thread = new Thread(this);
                  thread.start();
            }
      }
 
      public void run()
      {
            while (true)
            {
                  bf.push(Math.random());
                  System.out.println("Am scris.");
                  try
                  {Thread.sleep(1000);}catch(Exception e){}
            }
      }
}
 
/**
 * Aceasta este o clasa de tip fir de executie. Intr-o bucla infinita sunt citite elemente
 * din cadrul unui obiect de tip Buffer.
 */
 
class Consumer extends Thread
{
      private Buffer bf;
      Consumer(Buffer bf){this.bf=bf;}
 
      public void run()
      {
            while (true)
            {
                  System.out.println("Am citit : "+this+" >> "+bf.get());
            }
      }
}
 
class Buffer
{
      /*
       * Vector folosit pentru a inmagazina obiecte de tip Double.
       */
      ArrayList content = new ArrayList();
 
      /**
       * Prin intermediul acestei metode sunt adaugate elemente in containerul content.
       * Se observa ca aceasta metoda este sincronizata. Metoda fa fi apelata de firele
       * de executie de tip Producer.
       *
       * Dupa adaugarea unui element in container se apeleaza metoda notify() aceasta asigura
       * trezirea unui fir de executie ce a fost blocat prin apelul functiei wait().
       * @param d
       */
      synchronized void push(double d)
      {
            content.add(new Double(d));
            notify();
      }
 
      /**
       * Aceasta metoda este folosita pentru a extrage elemente din cadrul containerului
       * content. Se observa ca aceasta metoda este sincronizata.
       * Daca containerul este  gol se apeleaza metoda wait(). Aceasta va bloca firul
       * de executie apelant pana in momentul in care un fir de executie producator
       * va adauga in container un element si va apela metoda notify() (vezi metoda put(...))
       *
       * @return
       */
      synchronized double get()
      {
            double d=-1;
            try
            {
                  while(content.size()==0) wait();
                  d = (((Double)content.get(0))).doubleValue();
                  content.remove(0);
            }catch(Exception e){e.printStackTrace();}
            return d;
      }
}