synchronized va a ser una palabra clave de Java que permite fabricar métodos o bloques de código de ejecución sincronizada.
Un bloque o método de ejecución sincronizada sólo puede ser ejecutado simultáneamente por más de un hilo. Eso significa que si tienes dos hilos que tratan de ejecutar la misma función a la vez, o la misma región que ha sido marcada previamente como synchronized, entonces no podrán hacerlo a la vez, y deberán ir uno a uno.
En esta lección os voy a explicar el funcionamiento de los métodos sincronizados, y voy a dejar los bloques sincronizados para la siguiente lección.
Monitor de un objeto y relación con la ejecución sincronizada
Los bloques y métodos sincronizados lo son con respecto a un monitor. Un monitor es un tipo específico de lock, una estructura concurrente que permite coordinar la ejecución de varios hilos para que se haga de forma ordenada y no simultánea. Se trata de una manera de impedir problemas como los de condición de carrera o corrupción de memoria que estudiamos en la lección anterior.
Aunque ya estudiaremos esto con más detalle en el futuro, un lock es un objeto que puede ser compartido de forma segura por varios hilos. De hecho, debería ser compartido por varios hilos. Una vez que todos los hilos tienen acceso al mismo lock, pueden invocar al menos dos operaciones en el mismo:
- Adquirir el lock. Solicitan que, de algún modo, el hilo sea responsable temporalmente del mismo. En ese momento, se dirá que el lock tiene dueño, y ese dueño es el hilo que ha conseguido llamar al método de adquisición.
- Liberar el lock. Se deshacen del lock, una vez que ya no lo necesitan.
La gracia con el sistema de locks es que están diseñados de tal manera que son thread-safe, es decir, seguros al ejecutarse en un entorno multihilo. Efectivamente, el acceso a un lock es atómico, lo que quiere decir que incluso en un escenario en el que múltiples procesadores en simultáneo traten de usar a la vez el método de adquisición del candado, sólamente uno de los hilos será quien gane. El acceso concurrente al estado de un candado sólo puede hacerlo un hilo tras otro.
Si se intenta capturar un lock que actualmente tiene propietario, esa adquisición no será exitosa, ya que un lock sólo puede ser adquirido si no tiene propietario en este momento. En caso contrario, debería esperarse hasta que el lock se libere, para poder ser este otro hilo quien lo adquiera.
Entonces, en este sentido, si tratas de hacer una adquisición de un lock que ahora mismo tiene propietario, pueden ocurrir dos cosas, según cómo esté diseñada la API del lock:
- El método de adquisición se puede bloquear. Pone tu hilo en espera, y automáticamente volverá a tratar de adquirirse cuando el lock se libere. De este modo, la ejecución de código en tu hilo simplemente se pausa hasta que el otro hilo libere el candado.
- El método de adquisición puede fallar instantáneamente. Se reporta mediante un valor lógico o excepción, que no ha sido posible adquirir el lock porque, previsiblemente, otro hilo ya lo tenía en este momento.
- O bien, una solución híbrida donde esperamos hasta un tiempo límite, y en caso de que superado ese límite no hayamos podido capturar el lock, se produce un fallo o se informa de algún modo de que la captura no ha sido exitosa.
Posiblemente tengas más preguntas sobre locks, pero las cubriremos en el futuro cuando pueda dedicarle una lección completa y así pueda explicar más sobre este tema.
En cualquier caso, ¿qué tiene que ver esto con la ejecución sincronizada de código, que es de lo que trata esta función? Porque todas las instancias de una clase, tienen incorporado un lock muy primitivo que puede ser adquirido y liberado. Ese lock se denomina monitor, por una cuestión histórica. Y, precisamente, synchronized es una de las formas que hay de interactuar con ese monitor.
Básicamente, cuando intentamos ejecutar una región de código sincronizada, como puede ser uno de estos métodos, el hilo automáticamente intentará adquirir ese monitor para que pueda iniciar la ejecución de la región sincronizada. Pero si múltiples hilos tratan de invocar la misma región de código sincronizada usando el mismo monitor, entonces el resto de hilos que no hayan llegado primero tendrán que esperar, y sólo podrán ir entrando en la región de uno en uno, a medida que un hilo va saliendo y liberando el monitor para que otro hilo lo pueda capturar.
Métodos sincronizados
Explicada la teoría detrás de la ejecución sincronizada de un método, vamos a ver cómo implementarla en Java. Se hace agregando la palabra clave synchronized a la definición de un método. Por ejemplo, imagina que tienes un recurso compartido denominado Timbre con un método llamado timbrar().
public void timbrar() {
System.out.print("Ding... ");
try {
Thread.sleep(2000);
System.out.println("Dong");
} catch (InterruptedException e) {
System.out.println("* El timbre de repente empieza a arder *");
}
}
Este timbre va a ser compartido por una serie de hilos que van a poder utilizar el método timbre() para poder tocarlo. Sin embargo, no queremos que en simultáneo varios hilos puedan hacerlo, sino que vayan de forma ordenada uno tras otro. Como deducimos por su código, ya que el timbre lleva un tiempo de usar, una cosa que puede ocurrir si no controlamos el acceso es que todos los hilos intenten tocar a la vez y la ejecución sea rara.
Ding... Ding... Ding... Dong
Ding... Dong
Dong
Dong
Para impedir esto, nos basta con ponerle la palabra clave synchronized al definir el método, tal que así:
- public void timbrar() {
+ public synchronized void timbrar() {
Con esto, si dos hilos tratan de invocar el método timbrar para la misma instancia de Timbre, tendrán que hacerlo de forma coordinada. No podrán hacerlo a la vez, sino que el primer hilos que llegue podrá invocar directamente al método, y el resto de hilos quedarán automáticamente bloqueados y a la espera hasta que el método pueda invocarse porque otro hilo haya terminado de invocarlo.
Métodos estáticos sincronizados
Una última particularidad menor sobre este tipo de métodos, es que los métodos estáticos también aceptan el uso de la palabra clave synchronized. Esto significa que una definición de método estática podría quedar como public static synchronized float miMetodo().
En caso de que se use synchronized a nivel de método estático, entonces se usa como monitor un monitor estático, asociado a la propia clase. Por ejemplo, sería el monitor de Timbre.class el que se use, y afectaría a todos los hilos que estén tratando de invocar un método sincronizado que esté marcado como estático dentro de esa clase.