Mecanismes de sincronització
Monitors (intrinsic lock)
A Java, la sincronització es fa mitjançant monitors. Un monitor és un objecte qualsevol que pot tenir un únic fil propietari. Qualsevol fil pot demanar la propietat d'un monitor i a canvi accedir a una zona crítica de codi restringida. Si ja hi ha propietari, cal que s'esperi fins que ho deixi de ser.
Per demanar la propietat d'un monitor i l'accés a la zona crítica de codi, podem utilitzar la paraula reservada "synchronized". En funció d'on ho fem, l'objecte monitor canvia:
- Mètodes d'instància: L'objecte monitor és la instància. Per tant, només un fil per cada instància.
- Mètodes de classe: L'objecte monitor és la classe. Per tant, només un fil per cada classe.
- Blocs de codi: s'ha d'indicar l'objecte monitor dins dels parèntesis. Qualsevol objecte pot ser monitor (p. ex.
new Object()
), tot i que habitualment fem que el monitor sigui el mateix objecte sobre el qual volem exercir control d'accés.
Un exemple de mètodes d'instància:
public class SynchronizedCounter {
private int c = 0;
public synchronized void increment() {
c++;
}
public synchronized void decrement() {
c--;
}
public synchronized int value() {
return c;
}
}
Conseqüències:
- Primer, no és possible que dos fils puguin cridar simultàniament a dos mètodes sincronitzats. Les subseqüents crides se suspenen fins que el primer fil acabi amb l'objecte.
- Segon, quan un mètode sincronitzat acaba, estableix una relació happens-before: les crides subseqüents tindran visibles els canvis fets.
Important: dins d'un bloc sincronitzat, cal fer la feina mínima possible: llegir les dades i si cal, transformar-les.
Un exemple de blocs de codi:
public class MsLunch {
private long c1 = 0;
private long c2 = 0;
private Object lock1 = new Object();
private Object lock2 = new Object();
public void inc1() {
synchronized(lock1) {
c1++;
}
}
public void inc2() {
synchronized(lock2) {
c2++;
}
}
}
Aquest mètode permet tenir un gra més fi: pot haver-hi un fil a la zona de codi de lock1 i un altre a la de lock2.
Wait / Notify (guarded lock)
Imaginem que volem esperar fins que es compleixi una condició:
public void waitForHappiness() {
// Simple method, wastes CPU: never do that!
while (!hapiness) {}
System.out.println("Happiness achived!");
}
Això ho podem fer entre fils mitjançant el mètode clàssic de comunicació wait i notify, que permet:
- Esperar fins que una condició que implica dades compartides sigui certa, i
- notificar a altres fils que les dades compartides han canviat, probablement activant una condició per la que esperen altres fils.
Els mètodes són:
- wait(): quan es crida, el fil actual espera fins que un altre fil cridi notify() o notifyall() sobre aquest monitor.
- notify(): desperta un fil qualsevol de tots els que estiguin esperant a aquest monitor.
- notifyAll(): desperta tots els fils que estiguin esperant a aquest monitor.
Els mètodes wait() i notify s'han de cridar des de dins d'un bloc sincronitzat per a l'objecte monitor.
A més, tal i com es comenta a Object, el mètode wait() ha de ser a dins d'un bucle esperant per una condició:
// in one thread:
synchronized (monitor) {
while (!condition) {
monitor.wait();
}
}
// in the other thread:
synchronized (monitor) {
monitor.notify();
}
En el nostre cas:
synchronized (monitor) {
while (!happiness) {
monitor.wait();
}
}
...
synchronized (monitor) {
happiness = true;
monitor.notify();
}
Funcionament del wait / notify
- El fill 1 obté el monitor i comprova si la condició és true per acabar el loop.
- Com que és false, fa wait() i deixa el monitor.
- El fill 2 obté el monitor, canvia la condició a false i fa notify().
- El fill 1 es desperta i passa a 'ready', demanant pel monitor.
- Quan l'obté, veu que la condició és true i acaba el loop.
Reentrant Lock
L'ús de synchronized és molt senzill i suficient en molts escenaris. Però hi ha una implementació, ReentrantLock, que permet les següents característiques addicionals:
- Intent de bloqueig: permet provar de bloquejar un lock sense haver d'esperar.
- Locks justos: permeten que els fils s'acabin executant en l'ordre en que han demanat el lock.
- Locks condicionals: permeten que un fil esperi fins que una condició es compleixi.
- Locks amb interrupció: permeten que un fil esperi fins que una condició es compleixi, però que es pugui interrompre.
Aquest podria ser el nostre fil per a l'exemple del comptador:
static class MyRunnable implements Runnable {
SharedObject so;
Lock lock;
MyRunnable(SharedObject so, Lock lock) {
this.so = so;
this.lock = lock;
}
void increment() {
try {
lock.lock();
so.counter ++;
} finally {
lock.unlock();
}
}
@Override
public void run() {
for (int i=0; i<1_000_000; i++) {
increment();
}
}
}
Com es pot veure, el mètode increment() fa servir un lock per a sincronitzar l'accés a la variable compartida. Això és el mateix que fer servir un mètode sincronitzat, però amb la diferència que podem fer servir el lock en un bloc de codi que pot llançar una excepció.