Synchronized vs lock

De ChuWiki


Cuando varios hilos quieren acceder a un mismo recurso (una conexión, un fichero, una variable, ...) que no se puede ser utilizado por todos ellos a la vez, es necesario hacer que los hilos esperen unos por otros, de forma que cuando un hilo termine de usar el recurso, otro hilo que esté a la espera pase a usarlo. Para implementar esta espera, hay varias posibilidades, entre ellas, synchronized y lock.

Veamos de forma sencilla las diferencias en java entre usar synchronized y usar alguna clase que implemente Lock para sincronizar hilos.


synchronized[editar]

La forma más simple de hacer esto es usar bloques synchronized en java

synchronized (recurso) {
   // Uso del recurso 
}

La sintaxis es sencilla. synchronized y entre paréntesis alguna variable, siempre la misma, que puede ser el mismo recurso, que servirá para indicar si el recurso está o no bloqueado. La llamada syncrhonized(recurso) se quedará bloqueada si otro hilo está usando el recurso, y seguirá adelante cuando no haya nadie usando el recurso o este deje de usarse.

Inmediatamente después, entre {} se pone el código de utilización del recurso. Cuando termine de ejecutarse ese bloque, se liberará el recurso para que otro hilo que use synchronized(recurso) pueda usarlo.

Como se puede ver, es bastante sencillo e inmediato.

Lock[editar]

Java dispone de una interface Lock y varias clases que la implementan. Aquí usaremos para el ejemplo ReentrantLock. Su funcionamiento es muy similar al de synchronized, pero con más posibilidades. Su uso sería de la siguiente forma

// En algún sitio, se crea la variable de tipo Lock, una única para todos los hilos.
Lock aLock = new ReentrantLock();
...

A partir de aquí, los hilos que quieran usar el recurso, deberán llamar al método lock() de esta variable

aLock.lock();

// a partir de aquí, podemos usar el recurso.

Esta llamada se quedará bloqueada si otro hilo se nos ha adelantado. La ejecución seguirá si ningún otro hilo a bloqueado esta variable o cuando el que la tenga bloqueada lo libere. A partir de aquí, podemos usar el recurso.

A diferencia de synchronized, no estamos limitados a un bloque entre llaves justo detrás de synchronized(). Tenemos el recurso bloqueado todo el tiempo, hasta que llamemos a aLock.unlock()

Por ello, y para asegurarse que lo llamamos aunque haya fallos o salten excepciones, es bueno usar el recurso con un bloque try-catch y un finally en el que se libere el recurso

try {
   // uso del recurso
} catch (Exception e) {
   // Tratamiento de la excepcion
} finally {
   aLock.unlock();
}

Diferencias entre synchronized y lock[editar]

syncrhonized es más sencillo y directo, lock da más versatilidad[editar]

La primera diferencia ya debería estar clara, mientras que synchronized es más directo y limitado, Lock nos permite mantener bloqueado el recurso indefinidamente, hasta que llamemos a unLock. De hecho, podemos bloquear el recurso en un método y desbloquearlo en otro, cosa que no es posible con synchronized.

lock permite desbloquear hilos en orden de llegada, synchronized no garantiza el orden[editar]

Otra aspecto a tener en cuenta es que cuando hay varios hilos bloqueados en un synchronized(), al liberarse el recurso, cualquiera de los hilos puede empezar a usarlo, Java no establece ningún orden. Con un ReentrantLock pasa lo mismo ... salvo que en el constructor pasemos un boolean true

Lock aLock = new ReentrantLock(true);

si lo hacemos así, los hilos quedarán en una cola, siendo el primero de la misma el primero que llegue y será, por tanto, el primero que despertará cuando el recurso quede libre.

synchronized nos puede bloquear indefinidamente, lock permite fijar un tiempo máximo de bloqueo[editar]

Otra cosa más de lock, tiene un método tryLock() y otro tryLock(time,..) para intentar coger el lock y salir inmediatamente o pasado un tiempo, si no lo consiguen. Debemos verificar si estos métodos nos devuelve true o false, indicando si hemos conseguido bloquear o no el recurso. synchronized no tiene esta posibilidad, si entramos en un synchronized, podemos quedarnos bloqueados indefinidamente.

lock permite usar condiciones[editar]

Lock tiene además la posibilidad de crear "condiciones". A veces, un recurso no tiene por qué estar totalmente bloqueado para todo tipo de operaciones. Por ejemplo, imagina una lista que tiene un máximo de 10 elementos. Los hilos pueden añadir y retirar elementos de la lista, pero queremos que se queden bloqueados en las siguientes condiciones:

  • Un hilo se queda bloqueado si quiere retirar un elemento y la cola está vacía. Se queda bloqueado hasta que haya algún elemento en la cola.
  • Un hilo se queda bloqueado si quiere añadir un elemento a la cola y la cola ya está lleno. Se quedará bloqueado hasta que otro hilo retire un elemento.

Para implementar esto, creamos el lock global de la lista y dos condiciones, una para lleno, la otra para vacío

Lock aLock = new ReentrantLock();
Condition notFull = aLock.newCondition();
Condition notEmpty = aLock.newCondition();

Ahora, si queremos que un hilo espere hasta que la cola no esté llena, debemos poner algo como

aLock.lock();

while (aList.size() >= 10) {
   notFull.await();   // Espera hasta que alguien llame a notFull.signal()
}

Y el hilo que retire algo de la lista debería hacer algo como esto

aLock.lock();

aList.takeElement();
notFull.signal();

En general, una Condition no es más que un objeto al que podemos llamar a su método await() para quedarnos a la espera hasta que alguien llame a signal(). Qué objetos creamos, qué significan e implementar su lógica es cosa nuestra.