Sincronización en java

La sincronización nace de la necesidad de evitar que dos o más threads traten de acceder a los mismos recursos al mismo tiempo. Así, por ejemplo, si un thread tratara de escribir en un fichero, y otro thread estuviera al mismo tiempo tratando de borrar dicho fichero, se produciría una situación no deseada. Otra situación en la que hay que sincronizar threads se produce cuando un thread debe esperar a que estén preparados los datos que le debe suministrar el otro thread. Para solucionar estos tipos de problemas es importante poder sincronizar los distintos threads.

Las secciones de código de un programa que acceden a un mismo recurso (un mismo objeto de una clase, un fichero del disco, etc.) desde dos threads distintos se denominan secciones críticas (critical sections). Para sincronizar dos o más threads, hay que utilizar el modificador synchronized en aquellos métodos del objeto-recurso con los que puedan producirse situaciones conflictivas. De esta forma, Java bloquea (asocia un bloqueo o lock) con el recurso sincronizado. Por ejemplo:

public synchronized void metodoSincronizado() {
…// accediendo por ejemplo a las variables de un objeto

}

La sincronización previene las interferencias solamente sobre un tipo de recurso: la memoria reservada para un objeto. Cuando se prevea que unas determinadas variables de una clase pueden tener problemas de sincronización, se deberán declarar como private (o protected). De esta forma sólo estarán accesibles a través de métodos de la clase, que deberán estar sincronizados.

Es muy importante tener en cuenta que si se sincronizan algunos métodos de un objeto pero otros no, el programa puede no funcionar correctamente. La razón es que los métodos no sincronizados pueden acceder libremente a las variables miembro, ignorando el bloqueo del objeto. Sólo los métodos sincronizados comprueban si un objeto está bloqueado. Por lo tanto, todos los métodos que accedan a un recurso compartido deben ser declarados synchronized.

De esta forma, si algún método accede a un determinado recurso, Java bloquea dicho recurso, de forma que el resto de threads no puedan acceder al mismo hasta que el primero en acceder termine de realizar su tarea. Bloquear un recurso u objeto significa que sobre ese objeto no pueden actuar simultáneamente dos métodos sincronizados.

Existen dos niveles de bloqueo de un recurso. El primero es a nivel de objetos, mientras que el segundo es a nivel de clases. El primero se consigue declarando todos los métodos de una clase como synchronized. Cuando se ejecuta un método synchronized sobre un objeto concreto, el sistema bloquea dicho objeto, de forma que si otro thread intenta ejecutar algún método sincronizado de ese objeto, este segundo método se mantendrá a la espera hasta que finalice el anterior (y desbloquee por lo tanto el objeto). Si existen varios objetos de una misma clase, como los bloqueos se producen a nivel de objeto, es posible tener distintos threads ejecutando métodos sobre diversos objetos de una misma clase.

El bloqueo de recursos a nivel de clases se corresponde con los métodos de clase o static, y por lo tanto con las variables de clase o static. Si lo que se desea es conseguir que un método bloquee simultáneamente una clase entera, es decir todos los objetos creados de una clase, es necesario declarar este método como synchronized static. Durante la ejecución de un método declarado de esta segunda forma ningún método sincronizado tendrá acceso a ningún objeto de la clase bloqueada.

La sincronización puede ser problemática y generar errores. Un thread podría bloquear un determinado recurso de forma indefinida, impidiendo que el resto de threads accedieran al mismo. Para evitar esto último, habrá que utilizar la sincronización sólo donde sea estrictamente necesario.

Es necesario tener presente que si dentro un método sincronizado se utiliza el método sleep() de la clase Thread, el objeto bloqueado permanecerá en ese estado durante el tiempo indicado en el argumento de dicho método. Esto implica que otros threads no podrán acceder a ese objeto durante ese tiempo, aunque en realidad no exista peligro de simultaneidad ya que durante ese tiempo el thread que mantiene bloqueado el objeto no realizará cambios.

Para evitarlo es conveniente sustituir sleep() por el método wait() de la clase java.lang.Object heredado automáticamente por todas lasclases. Cuando se llama al método wait() (siempre debe hacerse desde un método o bloque synchronized) se libera el bloqueo del objeto y por lo tanto es posible continuar utilizando eseobjeto a través de métodos sincronizados. El método wait() detiene el thread hasta que se llame al método notify() o notifyAll() del objeto, o finalice el tiempo indicado como argumento del método wait(). El método unObjeto.notify() lanza una señal indicando al sistema que puede activar uno delos threads que se encuentren bloqueados esperando para acceder al objeto unObjeto. El método notifyAll() lanza una señal a todos los threads que están esperando la liberación del objeto.

Los métodos notify() y notifyAll() deben ser llamados desde el thread que tiene bloqueado el objeto para activar el resto de threads que están esperando la liberación de un objeto. Un thread se convierte en propietario del bloqueo de un objeto ejecutando un método sincronizado del objeto. Los bloqueos de tipo clase, se consiguen ejecutando un método de clase sincronizado (synchronized static). Véanse las dos funciones siguientes, de las que put() inserta un dato y get() lo recoge:

public synchronized int get() { while (available == false) {
try {

// Espera a que put() asigne el valor y lo comunique con notify() wait();
} catch (InterruptedException e) { }
}
available = false;

notifica que el valor ha sido leído notifyAll();

devuelve el valor

return contents;
}

public synchronized void put(int value) { while (available == true) {
try {

// Espera a que get() lea el valor disponible antes de darle otro wait();
} catch (InterruptedException e) { }
}

ofrece un nuevo valor y lo declara disponible contents = value;

available = true;

notifica que el valor ha sido cambiado notifyAll();

}

El bucle while de la función get() continúa ejecutándose (avalaible == false) hasta que el método put() haya suministrado un nuevo valor y lo indique con avalaible = true. En cada iteración del while la función wait() hace que el hilo que ejecuta el método get() se detenga hasta que se produzca un mensaje de que algo ha sido cambiado (en este caso con el método notifAll() ejecutado por put()). El método put() funciona de forma similar.

Existe también la posibilidad de sincronizar una parte del código de un método sin necesidad de mantener bloqueado el objeto desde el comienzo hasta el final del método. Para ello se utiliza la palabra clave syncronized indicando entre paréntesis el objeto que se desea sincronizar (synchronized(objetoASincronizar)). Por ejemplo si se desea sincronizar el propio thread en una parte del método run(), el código podría ser:

public void run() { while(true) {

syncronized(this) { // El objeto a sincronizar es el propio thread
… // Código sincronizado
}
try {
sleep(500); // Se detiene el thread durante 0.5 segundos pero el objeto

es accesible por otros threads al no estar sincronizado } catch(InterruptedException e) {}

}
}

Un thread puede llamar a un método sincronizado de un objeto para el cual ya posee el bloqueo, volviendo a adquirir el bloqueo. Por ejemplo:

public class VolverAAdquirir { public synchronized void a() {
b();
System.out.println(«Estoy en a()»);
}

public synchronized void b() { System.out.println(«Estoy en b()»);
}
}

El anterior ejemplo obtendrá como resultado:

Estoy en b()
Estoy en a()

debido a que se ha podido acceder al objeto con el método b() al ser el thread que ejecuta el método a() “propietario” con anterioridad del bloqueo del objeto.

La sincronización es un proceso que lleva bastante tiempo a la CPU, luego se debe minimizar su uso, ya que el programa será más lento cuanta más sincronización incorpore.

Fuente: Aprenda Java como si estuviera en primero de la Universidad de Navarra