Enrera
Mòdul 7
Iniciació a la programació en Java
  Pràctica
1
2
3
4
4
   
Exercicis
Exercicis
 
 
  Sincronització i bloqueig de fils
   
 

Els fils (threads) poden compartir objectes, accedir a la mateixa informació i intentar transformar-la. Aquesta és una situació perillosa: un fil pot estar intentant modificar un camp d'un objecte en el mateix moment que un altre fil. Imaginem dos fils que accedeixen simultàniament a un fitxer de disc on actualitzem informació del programa. Un fil pot estar escrivint dades just el mateix moment i posició del fitxer que l'altre fil que n'està llegint la informació. Pot haver-hi col·lisions i inconsistències amb molta facilitat.

Per tal d'evitar aquesta situació haurem de treballar, com ja s'ha vist, amb bloqueig de recursos, a través de la paraula reservada synchronized.

   

Què passa si no vigilem la sincronia?

   

Imaginem que administrem una gran base de dades i estem encarregats de fer el recompte dels usuaris que s'han anat connectant. Cada usuari té una terminal i cada terminal es representa a través d'un fil autònom. Escrivim un programa a través del qual, quan un terminal es connecta, s'incrementa un comptador a la base de dades. El programa es diu Sincro.java: escriviu-lo a JCreator, compileu-lo i executeu-lo:

   

public class Sincro {

    public static void main(String[] args) {
        Dades dades = new Dades();
        Terminal terminal1 = new Terminal("Terminal 1",dades);
        Terminal terminal2 = new Terminal("Terminal 2",dades);
        terminal1.start();
        terminal2.start();
    }
}

class Dades {

    public int comptador=0;

    public int getComptador(){
        comptador++;
        return comptador;
    }

}

class Terminal extends Thread {

    Dades dades;

    Terminal(String nom, Dades dades) {
        super(nom);
        this.dades=dades;
    }

    public void run() {
            for (int n=0; n<10000; n++) {
                suma();
            }
    }

    private void suma() {
        System.out.println(getName()+
                           " : "+
                           dades.getComptador()+
                           " persones");
    }

}

   
 

La consola ens anirà informant sobre quin terminal s'està connectant i quin és el número total de connexions a la base de dades. Busqueu en el llistat punts de transició entre el terminal 1 i el terminal 2: observareu que el recompte total de connexions no sempre quadra; en el canvi de fil, el terminal entrant pot anotar valors fora d'ordre, no sempre però sí prou sovint com per a què el mecanisme de suma no sigui fiable.

Per tal de provocar l'error però, hem d'utilitzar una cònsola ràpida: la pantalla negra no ho és prou i no ens serveix. Si utilitzem la cònsola de JCreator, donem temps suficient als fils per a no solapar-se. Aquesta seria una sortida en una consola ràpida (la de l'IDE "Eclipse"):

   
 
   
 

Aquí podem observar perfectament l'error: en un salt de Terminal, obtenim dues vegades el valor 17558. El terminal 2 ha variat el valor del comptador després de la lectura i abans de l'enregistrament del terminal 1.

Si protegim el mètode suma() amb la paraula reservada synchronized, aquest error no es produirà més. L'aplicació correrà una mica més lenta però l'ordre d'increment i impressió en pantalla del comptador queda garantit.

   
  Notificació d'incidències entre fils: wait() i notifyAll()
   

La sincronització de fils, tal com l'hem estudiada, soluciona el perill de la inconsistència de les dades però ens crea noves necessitats. Hem d'establir sistemes de comunicació que serveixin per anunciar, d'un fil als altres, quan s'han produït modificacions en recursos compartits.

Imaginem que estem programant la gestió d'entrades d'un museu. El museu té un aforament de 1000 persones i no podem deixar entrar cap persona fins que no surtin alguns dels visitants que ja són a l'interior. Utilitzarem un mètode sincronitzat d'accés que portarà el recompte del nombre de visitants actuals al museu, tot restant una plaça disponible cada cop que deixem passar una persona. Què passa si no hi ha places disponibles? Sembla raonable pensar que el programa hauria d'esperar a què es produís una vacant i fer l'assignació quan el comptador de places lliures fos superior a zero.

Però sense comunicació entre fils això no ho podem fer: Si el mètode d'accés es queda actiu esperant que algú surti, el programa es bloquejarà. Com que el mètode té bloquejat el comptador de visitants, cap altre fil pot actualitzar-lo i ens trobarem en un carreró sense sortida. Per tant, sense comunicació entre fils, hauríem d''intentar fer una entrada de visitant i, si no hi haguessin vacants, caldria sortir del mètode. Això ens obligaria a codificar algun mecanisme de reintent automàtic o deixar-ho tot plegat en mans de l'usuari del programa. Tot això no és pas un plantejament massa eficaç.

Per tal d'evitar aquestes situacions i d'altres similars, hi ha la parella de mètodes wait() - notifyAll() -també notify()-, ambdòs mètodes de la classe java.lang.Object (la classe mare de totes les classes), els quals faciliten la comunicació d'incidències entre diferents fils. El mètode wait() bloqueja els fils i els deixa esperant novetats, mentre que el mètode notifyAll() informa al fils que estan a l'espera de les novetats que s'han produït canvis en els objectes compartits. Immediatament després de la notificació, els fils que estaven esperant continuent treballant.

De fet, amb wait(), bloquejem el fil que treballa sobre un objecte sincronitzat i desbloquejem l'objecte sobre el qual estava treballant. El fil passa a una llista de fils bloquejats i no pot continuar treballant fins que un altre fil informi al gestor de fils que l'objecte sobre el qual havíem fet wait() està disponible.

En forma d'esquema podríem representar el procés així:

Fil 1:
  Troba l'objecte A en un estat no esperat o desitjat activa wait() desbloqueja els recursos sincronitzats passa a la llista de fils bloquejats queda a l'espera que un altre fil notifiqui actualitzacions sobre l'objecte A  
 
Fil 2:
  Treballa sobre l'objecte A actualitza els recursos sincronitzats desbloqueja els recursos sincronitzats notifica al gestor de fils que s'ha actualitzat l'objecte A amb notifyAll() el gestor de fils treu el Fil 1 de la llista de fils bloquejats i el passa a estat executable.  

Observeu que el treball amb wait() - notifyAll() l'heu de fer sempre simètricament. Si fem una aplicació sense notificació d'actualitzacions, els fils s'aniran bloquejant un a un i no hi haurà ningú que els tregui de la llista. Planifiqueu sempre qui i com s'han de fer les notificacions d'actualització.

Amb aquests instruments, el problema que teníem amb el programa del museu se simplifica: si tenim un mètode que anota les entrades de visitants i arriba un moment que no queden places vacants a l'interior de la sala, només ens cal cridar al mètode wait(). El mètode allibera els recursos bloquejats i es queda a l'espera que algú surti del museu. Quan un altre fil anoti la sortida d'un visitant, llançarà un notifyAll(), i el fil que estava esperant es posarà de nou en marxa.

Escriviu , compileu i executeu aquest programa, que ens il·lustra el problema del museu:

   
public class Louvre {

    public static void main (String[] args) {
        Museu m = new Museu();
        // Fil que simula el terminal que hi ha a l'entrada
        // del museu

        TerminalEntrades e = new TerminalEntrades(m);
        // Fil que simula el terminal que hi ha a la sortida
        // del museu

        TerminalSortides s = new TerminalSortides(m);
        e.start();
        s.start();
    }

}

// La classe museu disposa dels mètodes de gestió de vacants
// Els terminals han de crear objectes d'aquesta classe per a
// treballar

class Museu {

    // Simplifiquem l'aforament de la sala a 10 persones
    public static final int AFORAMENT = 10;
    private static int ocupades = 0;

    public Museu () {
    }

    // mètode sincronitzat per a les entrades al museu
    // Està protegit per InterruptedException que gestiona
    // els errors en el bloqueig de fils que pot provocar wait() i
    // notifyAll()

    public synchronized void entrar () throws InterruptedException {
        //Si no hi ha vacant ens quedem a l'espera de canvis
            while (ocupades>Museu.AFORAMENT-2) {
                System.out.println("... Esperant una vacant ... ");
                wait();
            }
        ocupades+=2; // Ocupem de 2 en 2 per crear cues d'espera
        System.out.println(" >> Entren dos visitants "+
                           ocupades+
                           " places ocupades");
    }

    // mètode sincronitzat per a les sortides del museu
    // cada cop que s'executa, notifica als altres fils que
    // ha actualitzat el número de places ocupades.

    public synchronized void sortir () throws InterruptedException {
        ocupades-=1;
        System.out.println(" << Surt un visitant "+
                           ocupades
                           +" places ocupades");
        notifyAll();
    }

}

class TerminalSortides extends Thread {

    private Museu m;

    public TerminalSortides (Museu m) {
        this.m=m;
    }

    public void run () {
            try {
                    for (int n=0; n<24; n++) { //Executa 24 sortides
                        m.sortir();
                        sleep(10);
                    }
            } catch(InterruptedException e) {
            }
    }

}

class TerminalEntrades extends Thread {

    private Museu m;

    public TerminalEntrades (Museu m) {
        this.m = m;
    }

    public void run () {
            for (int n=0;n<12;n++) {// Executa 12 processos d'entrada
                    try {
                        m.entrar();
                        sleep(10);
                    } catch (InterruptedException e) {
                    }
            }
    }

}

   
 

El programa activa dos fils, TerminalEntrades que gestiona les entrades al museu i TerminalSortides que informa dels visitants que surten. Ambdòs fils utilitzen com a instrument de treball la classe Museu, que és la que defineix els mètodes sincronitzats de gestió del comptador i el propi comptador de visitants. La classe Museu té el mètode entrar(), que deixa passar visitants si hi ha places, i sortir(), que anota les persones que surten i notifica als altres fils les actualitzacions que va fent. La classe principal de l'aplicació crea una instància de la classe auxiliar Museu i activa els dos fils, que simulen vàries accions d'entrada i sortida al museu.

La sortida en consola de l'aplicació ha de ser la següent:

   
 
   
   
   
 
Amunt