|
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:
|