Enrera
Mòdul 7
Iniciació a la programació en Java
  Pràctica
1
2
3
4
4
   
Exercicis
Exercicis
 
 
  Multitasca i Java: programar amb més d'un fil
   

Des de fa uns anys, els usuaris ens hem acostumat als sistemes operatius multitasca. Podem imprimir mentres llegim el correu electrònic, o podem passar un antivirus al mateix temps que escrivim un document. De fet, un ordinador domèstic només és capaç de fer una cosa cada vegada, però el sistema operatiu simula la capacitat de fer vàries coses simultàniament; així va repartint el temps d'utilització del processador entre les diferents aplicacions. Com que això són processos molt ràpids, l'usuari té la sensació que està fent diverses coses a la vegada.

Aquesta forma de treballar s'anomena multitasca i pot ser de dos tipus: apropiativa i cooperativa. Els sistemes operatius més moderns disposen de multitasca apropiativa. Els més antics, com Windows 3.1 i Mac 9, implementaven la multitasca cooperativa. Un sistema amb molt més perill perquè un programa podia monopolitzar tots els recursos i, al fallar, bloquejar el sistema operatiu.

Els programes multifil (multithreaded) es fonamenten en el mateix principi que els sistemes operatius multitasca: són programes que poden fer vàries coses a la vegada, separant les diferents feines en sectors (els fils), els quals van accedint alternativament als recursos que el sistema operatiu posa a disposició del programa. Tots els programes comercials importants són multifil (penseu, per exemple en els processadors de text que utilitzeu, o en el navegador, que carrega o descarrega diferents fitxers a la vegada). Un programa multifil dóna a l'usuari una impressió d'agilitat i qualitat impossible d'aconseguir d'altra manera.

L'inconvenient és que la programació multifil no és fàcil ni a Java ni a d'altres llenguatges. L'aproximació que en farem mitjançant aquestes pràctiques l'heu de considerar introductòria. Haureu de consultar documentació específica si us interessa aprofundir en aquest aspecte de la programació.

   
  La pilota que no ens deixa fer res i la que coopera:
   

Escriurem el programa de la pilota que bota, BolaNoFils.java:. Una bola es desplaça per la finestra del programa i va rebotant d'ample a ample durant una estona. El programa és una adaptació del programa de Horstmann i Cornell de la documentació de Sun.

Es tracta d'un programa codificat sense fils. Cada cop que l'executem, l'únic que podem fer amb ell és mirar el rebot de la bola fins al final. Posats a no poder, no podem ni interrompre el moviment de la pilota fins que no s'acabi el cicle que l'anima:

   

import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import java.util.*;
import javax.swing.*;

/**
* Programa que fa botar una pilota de banda a banda de la finestra
* Funciona sense fils (threads)
* ----------------------------------------------------------------
*/

public class BolaNoFils {

    public static int AMPLE=450;
    public static int ALT=450;

    public static void main(String[] args) {
        JFrame frame = new BolaFrame();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.show();
    }

}

/**
* Frame principal de l'aplicació
*/

class BolaFrame extends JFrame {

     private CanvasBola canvas;

     public BolaFrame() {
         setSize(BolaNoFils.AMPLE,BolaNoFils.ALT);
         setTitle("Bola que rebota");
         Container contentPane = getContentPane();
         canvas = new CanvasBola();
         contentPane.add(canvas,BorderLayout.CENTER);
         JPanel buttonPanel = new JPanel();
         addButton(buttonPanel,
                   "Comença",
                   new ActionListener() {
                       public void actionPerformed(ActionEvent evt) {
                           afegeixBola();
                       }
                   });
        addButton(buttonPanel,
                  "Tanca",
                  new ActionListener() {
                      public void actionPerformed(ActionEvent evt) {
                          System.exit(0);
                      }
                  });
        contentPane.add(buttonPanel,BorderLayout.SOUTH);
    }

    public void addButton(Container c, String titol,
                          ActionListener listener) {
         JButton boto = new JButton(titol);
         c.add(boto);
         boto.addActionListener(listener);
    }

    public void afegeixBola() {
            try {
                Bola b = new Bola(canvas);
                canvas.add(b);
                    for (int i=1; i<=1000; i++) {
                        b.mou();
                        Thread.sleep(5);
                    }
            } catch (InterruptedException e) {
            }
    }

}

/**
* Pinta la bola
*/
class CanvasBola extends JPanel {

    private ArrayList boles = new ArrayList();
    public void add(Bola b) { boles.add(b); }

    public void paintComponent(Graphics g) {
        super.paintComponent(g);
        Graphics2D g2 = (Graphics2D)g;

            for (int i=0; i<boles.size(); i++) {
                Bola b = (Bola)boles.get(i);
                b.draw(g2);
            }
    }

}

/**
* L'objecte bola
*/
class Bola {

    Random random = new Random();
    private Component canvas;
    private static final int XSIZE=15;
    private static final int YSIZE=15;
    private int x = 0;
    private int y = random.nextInt(BolaNoFils.ALT-50);
    private int dx = 2;

    public Bola(Component c) {
        canvas=c;
    }

    public void draw(Graphics2D g2) {
        g2.fill(new Ellipse2D.Double(x,y,XSIZE,YSIZE));
    }

    public void mou() {
        x += dx;
            if (x < 0) {
                x = 0;
                dx = -dx;
            }
            if (x + XSIZE >= canvas.getWidth()) {
                x = canvas.getWidth() - XSIZE;
                dx = -dx;
            }
        canvas.paint(canvas.getGraphics());
     }

}

   
  Ara donarem capacitats multifil al programa. Farem que tingui dos fils. Un d'ells, creat implícitament, que conté la interfície d'usuari i un altre, creat explícitament, que controla el bot de la pilota. Inclourem aquesta feina dins d'un objecte java.lang.Thread que s'activa amb el mètode run(). Cada cop que piquem sobre el botó "comença", el programa llança un nou fil (thread):
   
 

import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import java.util.*;
import javax.swing.*;


/**
* Programa que fa botar una pilota de banda a banda de la finestra
* Funciona amb fils (threads)
* ----------------------------------------------------------------
*/

public class BolaFils {

    public static int AMPLE=450;
    public static int ALT=450;

    public static void main(String[] args) {
        JFrame frame = new BolaFrame();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.show();
    }

}

/**
* Frame principal de l'aplicació
*/

class BolaFrame extends JFrame {

     private CanvasBola canvas;

     public BolaFrame() {
         setSize(BolaFils.AMPLE,BolaFils.ALT);
         setTitle("Bola que rebota amb fils");
         Container contentPane = getContentPane();
         canvas = new CanvasBola();
         contentPane.add(canvas,BorderLayout.CENTER);
         JPanel buttonPanel = new JPanel();
         addButton(buttonPanel,
                   "Comença",
                   new ActionListener() {
                       public void actionPerformed(ActionEvent evt) {
                         afegeixBola();
                       }
                   });
        addButton(buttonPanel,
                  "Tanca",
                  new ActionListener() {
                      public void actionPerformed(ActionEvent evt) {
                          System.exit(0);
                      }
                  });
        contentPane.add(buttonPanel,BorderLayout.SOUTH);
    }

    public void addButton(Container c,
                          String titol,
                          ActionListener listener) {
         JButton boto = new JButton(titol);
         c.add(boto);
         boto.addActionListener(listener);
    }

    public void afegeixBola() {
        Bola b = new Bola(canvas);
        canvas.add(b);

 
        FilBola fil = new FilBola(b);
        fil.start();
    }
}

// -------- FilBola és el fil que activem quan afegim una bola
class FilBola extends Thread {

    private Bola labola;

    public FilBola (Bola bola) {
        labola = bola;
    }

    //------------------ mètode que s'executa al llançar el fil ---
    public void run() {
            try {
                    for (int i=1; i<=1000; i++) {
                        labola.mou(); // Movem la bola
                        sleep(5); // Adormim el fil 5 mil·lisegons
                    }
            // Excepció imprescindible quan cridem wait() o sleep()
            } catch (InterruptedException e) {
            }
    }

}

 

class CanvasBola extends JPanel {

    private ArrayList boles = new ArrayList();

    public void add (Bola b) {
        boles.add(b);
    }

    public void paintComponent (Graphics g) {
        super.paintComponent(g);
        Graphics2D g2 = (Graphics2D)g;
            for (int i=0; i<boles.size(); i++) {
                Bola b = (Bola)boles.get(i);
                b.draw(g2);
            }
   }

}

class Bola {

    Random random = new Random();
    private Component canvas;
    private static final int XSIZE=15;
    private static final int YSIZE=15;
    private int x = 0;
    private int y = random.nextInt(BolaFils.ALT-50);
    private int dx = 2;

    public Bola (Component c) {
        canvas=c;
    }

    public void draw (Graphics2D g2) {
        g2.fill(new Ellipse2D.Double(x,y,XSIZE,YSIZE));
    }

    public void mou () {
        x += dx;
            if (x < 0) {
                x = 0;
                dx = -dx;
            }
            if (x + XSIZE >= canvas.getWidth()) {
                x = canvas.getWidth() - XSIZE;
                dx = -dx;
            }

 
        canvas.repaint(); // Treballant amb fils podem fer repaint()
 
    }

}
 


Analitzem el programa:

  • Com que la interfície d'usuari segueix un fil diferent del que hem creat (és el fil o thread per defecte: sempre hi ha, com a mínim, aquest), quan cliquem sobre el botó "tanca", el programa ens obeeix instantàniament i, a més, podem activar tantes boles com vulguem tot picant sobre el botó "comença".

  • El que hem fet és posar el codi de moviment de la bola dins el mètode run() del fil. El mètode run() és el que s'encarrega de fer les accions del fil.

  • Observeu que no hem cridat el mètode run() directament. En primer lloc hem creat un objecte Thread (fil = new FilBola()), i aquest ha estat el responsable de sol·licitar el mètode run() a través de fil.start().Sempre hem de procedir d'aquesta forma:

    • declarar el fil thread=new Thread();

    • inicialitzarlo amb thread.start().

    • preveure les accions amb el mètode run(), que serà cridat a l'inicialitzar-se el fil.

  • La crida a sleep(5) és molt important. Quan cridem a wait() o a sleep() estem comunicant a la resta de fils de l'aplicació que tenen l'oportunitat d'actuar. En cas contrari, el fil actual monopolitzaria els recursos del programa.

  • Si no tenim l'oportunitat de derivar els nostres objectes de la classe Thread perquè ja són fills d'una altra classe, haurem de fer que implementin la interfície Runnable. Dins el programa que hem escrit, tot i que no és necessari, podem fer aquesta adaptació de la classe Bola i eliminar la classe FilBola:

    class Bola implements Runnable{

        private Thread fil;

        ...
        public void start() {
                if (fil==null) {
                    fil = new Thread(this,"bola");
                    fil.start();
                }
        }

        public void run() {
                try {
                        for (int i=1; i<=1000; i++) {
                            mou();
    // Movem la bola
                            Thread.sleep(5);
    // Adormim el fil 5
                                             
    // mil·lisegons
                        }
                } catch (InterruptedException e) {
                }

        }
    }

    Ara, per tal de llençar un
    nou fil en el mètode afegeixBola(), hem de fer:

    public void afegeixBola() {
        Bola b = new Bola(canvas);
        canvas.add(b);
        b.start();
    }

    i, d
    esprés de crear l'
    objecte Bola i afegir-lo al canvas cridem el mètode start() de la classe Bola.
  Estats d'un fil:
   

Un fil pot estar en aquests quatre estats possibles:

  • Nou: quan un fil l'hem instanciat amb new Bola() però no s'està executant (encara no hem fet start()).

  • Executable: quan un fil ha rebut la instrucció start() està en estat executable. Que s'estigui executant o no depèn de si el sistema operatiu li ha donat pas o no.

  • Bloquejat: quan un fil està adormit -sleep()- o aturat -wait()-, quan està esperant acabar una operació d'entrada-sortida o quan intenta accedir a objectes bloquejats per altres fils. El fil surt d'aquest estat quan es desperta després de l'aturada de l'sleep(), quan un altre fil invoca notify() sobre el fil bloquejat per wait(), o quan s'acaba el procés d'entrada-sortida que el bloquejava.

  • Mort: quan un fil acaba amb les tasques de run(), o quan salta alguna excepció que el mètode run() no pot interceptar.
   
  Grups de fils (thread groups):
   

A la vida real els programes poden tenir molts fils, tants que en ocasions és útil agrupar-los en paquets que puguem obrir i tancar junts. La creació de grups de fils segueix aquestes passes:

ThreadGroup grupdefils = new ThreadGroup("nomqueposemdelgrup");
Thread fil1 = new Thread(grupdefils, "fil1");
Thread fil2 = new Thread(grupdefils, "fil2");

Amb aixó ja podem fer algunes operacions sobre tots els fils del grup, per exemple, aturar-los tots:

grupdefils.interrupt();
   
  Prioritats:
   
Encara que no ho definim explícitament, cada fil té la seva prioritat, expressada en un nombre que Java li assigna, entre 1 i 10. Si un fil té una prioritat més alta que un altre i els dos estan en espera d'execució, el planificador de fils posarà en marxa sempre el de prioritat més alta. Si els dos tenen la mateixa prioritat, el planificador actuarà seguint el seu propi criteri.

Decidir la prioritat d'un fil és força senzill: immediatament després d'instanciar-lo, executem el mètode setPriority():

Thread unfil = new Thread("unfil");
unfil.setPriority(Thread.MAX_PRIORITY);
Thread unfil2 = new Thread("unaltrefil");
unfil.setPriority(Thread.MIN_PRIORITY);
Thread unfil3 = new Thread("untercerfil");
unfil.setPriority(Thread.NORM_PRIORITY);

En qualsevol cas, no ens hem de refiar; l'assignació de prioritats no és una característica estrictament multiplataforma. Cada sistema operatiu gestiona les prioritats de manera diferent: Linux no farà massa cas de les prioritats que definim, Windows 2000 les simplificarà en una escala més restringida...

   
   
   
 
Amunt