sábado, 14 de julio de 2012

5.3.2. Ejemplo Synchronized


Prioridades

Un punto importante a conocer acerca de los hilos es el manejo de prioridades y cómo la máquina virtual selecciona a un determinado hilo para su ejecución.
Los hilos tienen prioridades, esta prioridad es relativa a otros hilos en Java. Cuando se dice que la prioridad es relativa se quiere decir que no se sabe exactamente el mapeo a las prioridades nativas de un determinado sistema operativo, lo único con lo que se cuenta es que las prioridades entre los hilos es relativa a otros hilos dentro de Java.
La máquina virtual simplemente selecciona al hilo de mayor prioridad que está en estado ejecutable y lo pone en ejecución, si es que existen dos o más hilos de igual prioridad, se selecciona uno en forma aleatoria, el hilo permanece en ejecución hasta que llama al método yield o pasa a estado no ejecutable o a estado detenido por algunas de las causas vistas anteriormente.
Otra cosa importante es que un hilo hereda automáticamente su prioridad del hilo que lo crea, en el ejemplo anterior los tres hilos tienen la misma prioridad y esta es igual a la prioridad del applet.
Se puede modificar la prioridad de un hilo con una llamada al método setPriorityque recibe como parámetro un entero, en estos momentos Java tiene definido 10 prioridades de 1 a 10, no es conveniente utilizarlas así en forma absoluta porque no se sabe que es lo que va a pasar posteriormente con esto de las prioridades, por lo pronto se pueden utilizar tres constantes que son, prioridad mínima,prioridad normal y prioridad máxima. Normalmente lo que se hace cuando se tiene una animación por ejemplo, es establecer la prioridad de esa animación a la prioridad mínima utilizando el método setPriority y mandándole como parámetro la constante MIN_PRIORITY de la clase Thread.
Si un hilo de alta prioridad se pone en ejecución y no tiene absolutamente ninguna operación de entrada-salida este hilo puede acaparar todo el CPU si es que no llama en algún momento al método yield o al método sleep, esto se conocen como hilos egoístas, así que es conveniente que cuando los hilos no tengan operaciones de entrada-salida ejecuten periódicamente yield o sleep para que no haya ningún acaparamiento del CPU por parte de un hilo egoísta.
Es conveniente saber que yield sólo cede el CPU a hilos de igual prioridad, sino existen hilos de igual prioridad al que se está ejecutando la llamada de yield es como si no existiera.
Resumiendo lo anterior se puede decir que:
  • Todos los hilos tienen una prioridad relativa a otros hilos.
  • La máquina virtual selecciona al hilo de mayor prioridad en estado ejecutable y lo pone a correr.
  • El hilo que está en ejecución hasta que: llama a yield() o hasta que pasa a estado no ejecutable (parado).
  • Un hilo hereda su prioridad del hilo que lo crea.
  • La prioridad de un hilo se puede modificar con setPriority(int). Utilizando las constantes disponibles MIN_PRIORITY, NORM_PRIORITY, MAX_PRIORITY
  • Si un hilo de alta prioridad se pone en ejecución y no tiene nada de I/O puede acaparar todo el CPU (hilos egoistas, selfish threads)
  • Es conveniente que el hilo ejecute yield() o sleep() a intervalos regulares para dar la oportunidad de que otros hilos puedan correr.
  • yield() sólo cede el CPU a hilos de igual prioridad.

Sincronización de Hilos

Es común que varios procesos en ejecución manipulen a un recurso compartido. Si los hilos que acceden recursos compartidos simplemente lo leen, entonces no hay necesidad de evitar que este sea utilizado por más de un hilo a la vez. Sin embargo, cuando varios hilos comparten un objeto y este puede ser modificado por uno o más de los hilos pueden ocurrir resultados indeterminados; en algunas ocasiones producirá resultados correctos mientras que en otras los resultados serán incorrectos.
Para evitar esto, el recurso compartido se debe administrar de manera adecuada dándole a cada subproceso acceso exclusivo al código que manipule al objeto compartido. Cuando un hilo se encuentre accediendo al recurso los demás hilos que también desean acceder el recurso se deben mantener en espera. Cuando el hilo que está accediendo al recurso compartido termina, a uno de los procesos que están en espera se le permite continuar. A este proceso se le conoce como sincronización de hilos.
Qué pasa cuando diferentes hilos hacen acceso a recursos del programa, en este caso es necesario sincronizar este acceso para evitar inconsistencias en resultados, si dos hilos acceden al mismo recurso, el orden de ejecución es impredecible, depende de la máquina virtual además de otros factores como la carga del sistema en este momento y la ejecución de otros hilos. Java implementa un mecanismo de monitores, este concepto es el que se ve en algún curso de sistemas operativos; si un objeto se le asocia la palabra reservada sincronize automáticamente a este objeto se le asocia un candado o un lock.
Un monitor permite el acceso serializado y no simultáneo a un objeto, es decir controla que solamente un hilo esté accediendo a un determinado recurso al mismo tiempo para evitar caer en inconsistencias.
En otras palabras:
  • Es necesario sincronizar el acceso a recursos compartidos para evitar inconsistencias en los resultados.
  • Si dos hilos acceden a un mismo recurso, el orden de ejecución es impredecible, depende de la máquina virtual y otros factores del sistema (carga, ejecución de otros hilos, etc.)
  • Java implementa un mecanismo de monitores, si un objeto es identificado con synchronized se le asocia un candado (lock).
  • Un monitor permite el acceso serializado y no simultáneo a un objeto.  

Usando Synchronized

En el momento en que un objeto quiera acceder a otro objeto que está sincronizado se pone en una fila de espera hasta que se quite el candado, solamente un hilo puede estar dentro de un objeto sincronizado.
Cuando se utiliza la sincronización en objetos se puede hacer en una instrucción en un conjunto de instrucciones o en un método, la recomendación es que siempre se haga a nivel método.
Algo muy importante es que un objeto tiene solamente un candado si es que en un objeto que se quiere sincronizar, se sincronizan dos o más métodos se puede tener esos métodos sincronizados pero solamente existe un candado, ¿qué quiere decir esto?, que si algún otro objeto, en este caso un hilo llama a un método sincronizado automáticamente se pone el candado al objeto, es decir otro hilo no puede acceder a ese objeto por la llamada a otro método aunque sea diferente al que está sincronizado.
La manera de liberar el candado en un objeto es simplemente terminar la ejecución del método sincronizado, el hecho de que se utilice sincronización tiene un impacto importante en el rendimiento del programa, así es que la sincronización se debe mantener un mínimo y solamente utilizarla cuando se necesite. Por ejemplo, hay ciertas asignaciones de datos primitivos que no vale la pena sincronizar por que se que son atómicas, es decir se realizan en una sola instrucción del CPU.
Concluyendo:
  • Hilos que quieran acceder a un objeto sincronizado se ponen en fila de espera hasta que el objeto quede desocupado (unlock)
  • Sólo un hilo puede estar dentro de un objeto sincronizado.
  • Se recomienda utilizar synchronized a nivel método, pero puede sincronizar cualquier sección de código (sección crítica).
  • Cada objeto tiene un sólo candado, si un hilo entra a un método sincronizado, el objeto pone el candado a todos los métodos sincronizados, es decir ningún otro hilo puede ejecutar ningún método sincronizado del objeto, hasta que el hilo poseedor del candado lo libere.
  • La manera de liberar el candado en un objeto es cuando se termina la ejecución del un método sincronizado.
  • No vale la pena sincronizar asignaciones de algunos datos primitivos porque son atómicas.
  • Excesiva sincronización tiene un impacto relevante en el rendimiento

No hay comentarios:

Publicar un comentario en la entrada