Gestión de hilos en Java

1.- Introducción

Hay 2 tipos de programas según el flujo de ejecución:

  • Programa de flujo único: Las actividades o tareas que lleva a cabo una a continuación de la otra, de manera secuencial.
  • Programa de flujo múltiple: Coloca las actividades a realizar en diferentes flujos de ejecución.

La programación multihilo o multithreading son programas o aplicaciones de flujo múltiple.

2.- Conceptos sobre hilos

Un hilo (subproceso) es un flujo de control secuencial independiente dentro de un proceso.
Cuando se ejecuta un programa crea un hilo principal del cual salen más hilos.
Observaciones:
  • No puede existir un hilo sin proceso.
  • Un hilo no puede ejecutarse solo.
  • Un proceso puede tener varios hilos.

2.1.- Recursos compartidos por los hilos

Elementos de un hilo:
  • un identificador único,
  • un contador de programa propio,
  • un conjunto de registros,
  • una pila (variables locales).
Por otra parte, un hilo puede compartir con otros hilos del mismo proceso los siguientes recursos:
  • código,
  • datos (como variables globales),
  • otros recursos del sistema operativo, como los archivos abiertos y las señales.
Si un hilo corrompe la memoria, los demás hilos también lo sufrirán.

2.2.- Ventajas y uso de hilos

Ventajas de los hilos (procesos ligeros) VS proceso:
  • Consumen menos recursos en el lanzamiento y ejecución.
  • Se tarda menos tiempo en crear.
  • Cambio de contexto más rápido.
Se aconseja usar hilos en:
  • La aplicación maneja entradas de varios dispositivos de comunicación;
  • La aplicación debe poder realizar diferentes tareas a la vez;
  • Interesa diferenciar tareas con una prioridad variada.
  • La aplicación en un entorno multiprocesador.

3.1.- Utilidades de concurrencia del paquete java.lang

Dentro del paquete java.lang contiene:
  • clase Thread. Produce hilos funcionales para otras clases.
  • interfaz Runnable. Añade la funcionalidad de hilo a una clase simplemente implementando la interfaz.
  • clase ThreadDeath. Deriva de la clase Error, y permite manejar y notificar errores;
  • clase ThreadGroup. Maneja un grupo de hilos de modo conjunto.
  • clase Object. Proporciona unos cuantos métodos cruciales dentro de la arquitectura multihilo de Java. Estos métodos son wait, notify y notifyAll.

3.2.- Utilidades de concurrencia del paquete java.util.concurrent

El paquete java.util.concurrent posee clases que facilitan desarrollo de aplicaciones multihilo y aplicaciones complejas están en estos paquetes:

● java.util.concurrent:

  • clases de sincronización.
  • interfaces para separar la lógica de la ejecución
  • interfaces para gestionar colas de hilos

● java.util.concurrent.atomic. Incluye un conjunto de clases para ser usadas como variables atómicas en aplicaciones multihilo y con diferentes tipos de dato, por ejemplo AtomicInteger y AtomicLong;● java.util.concurrent.locks. Define una serie de clases como uso alternativo a la cláusula synchronized. En este paquete se encuentran algunas interfaces como por ejemplo Lock, ReadWriteLock.

4.- Creación de hilos

Para crear un hilo se usa java.lang.Thread. Se puede iniciar, detener o cancelar.

Se pueden implementar o definir de dos formas:
  • extendiendo (heredando de) la clase thread;
  • implementando la interfaz Runnable.
¿Cuándo usar Thread y Runnable?
  • extender la clase Thread es el más sencillo pero Java no permite la herencia múltiple.
  • implementar Runnable es más general y flexible.

4.1.- Creación de hilos extendiendo la clase Thread

Para definir y crear un hilo extendiendo la clase thread:
  • crear una nueva clase que herede (extends) de la clase Thread;
  • redefinir en la nueva clase el método run con el código asociado al hilo.
  • crear un objeto de la nueva clase Thread.
Para ponerlo en marcha o iniciarlo:
  • Invocar al método start del objeto Thread (el hilo que hemos creado).

4.2.- Creación de hilos mediante la interfaz Runnable

Para definir y crear hilos implementando la interfaz Runnable:

  • declarar una nueva clase que implementa Runnable;
  • redefinir en esa nueva clase el método run
  • crear un objeto de la nueva clase;
  • crear un objeto de tipo Thread pasando como argumento al constructor,

Para ponerlo en marcha:

  • invocar al método start del objeto Thread

5.- Estados de un hilo

El ciclo de vida de un hilo:

  • nuevo (NEW): se ha creado un nuevo hilo, pero aún no está disponible para su ejecución;
  • ejecutable (RUNNABLE): el hilo está preparado para ejecutarse.
  • no ejecutable o detenido (NO_RUNNABLE): el hilo podría estar ejecutándose, pero hay alguna actividad interna al propio hilo que se lo impide.
  • muerto o finalizado (TERMINATED): el hilo ha finalizado.

El método getState de la clase Thread, permite obtener en cualquier momento el estado del hilo.

5.2.- Detener temporalmente un hilo

Un hilo detenido temporalmente es que ha pasado a estado «no ejecutable».
Circunstancias para pasar un hilo al estado «no ejecutable»:
  • el hilo se ha dormido. Se ha invocado al método sleep de la clase Thread, indicando el tiempo que el hilo permanecerá deteniendo
  • el hilo está esperando. El hilo ha detenido su ejecución mediante la llamada al método wait, y no se reanudará, pasará a «ejecutable» (en concreto «preparado») hasta que se produzca una llamada al método notify o notifyAll por otro hilo del mismo proceso.
  • el hilo se ha bloqueado. El hilo está pendiente de que finalice una operación de E/S en algún dispositivo.

5.3.- Finalizar un hilo

Un hilo termina de ejecutarse cuando su metodo run termina (manera natural). Tras morir no lo puedes iniciar otra vez con start. Si en tu programa deseas realizar otra vez el trabajo desempeñado por el hilo, tendrás que:
  • crear un nuevo hilo con new;
  • iniciar el hilo con start.
¿comprobar si un hilo no ha muerto?

método isAlive de la clase Thread para comprobar si un hilo está vivo o no, devuelve verdadero (true) o falso (false).

5.4.- Dormir un hilo con sleep

El método sleep permite introducir el tiempo que deseamos dormir el hilo que lo invoca. tras eso vuelve a estar «ejecutable» («preparado»)
Hay dos formas de llamar a este método:
  • Pasarle como argumento como parámetro: sleep (long milisegundos)
  • Indicar el tiempo extra que se sumará al segundo: sleep (long milisegundos, int nanosegundos)

6.- Gestión y planificación de hilos

La ejecución de hilos se puede realizar mediante:
  • paralelismo. En un sistema con múltiples CPU o núcleos,
  • pseudoparalelismo. Si no es posible el paralelismo, una CPU es responsable de ejecutar múltiples hilos.
El planificador de hilos de Java (Scheduler) utiliza un algoritmo de secuenciación

6.1.- Prioridad de hilos

Cada hilo tiene una prioridad (valor de tipo entero entre 1 y 10).
El hilo principal(main) siempre se crean con prioridad 5.
Los hilos secundarios heredan la prioridad que tenga en ese momento su hilo padre.
  • MAX_PRIORITY (= 10)
  • MIN_PRIORITY (=1)
  • NORM_PRIORITY (= 5). (la que tiene el hilo donde corre el método main())
Para obtener y modificar la prioridad de un hilo:
  • getPriority. Obtiene la prioridad de un hilo.
  • setPriority. Modifica la prioridad de un hilo.
Podemos conseguir aumentar el rendimiento de una aplicación multihilo gestionando adecuadamente las prioridades de los diferentes hilos.

Los hilos demonios son aquellos que se ejecutan en segundo plano.

Hilo egoísta: un hilo se ejecuta y no deja que otros hagan nada hasta que este termine. ● yield hace que un hilo que está «ejecutándose» pase a «preparado.

7.- Sincronización y comunicación de hilos

Al compartir recursos o información. Se presentan las siguientes situaciones:
  • Dos o más hilos compiten por obtener un mismo recurso.
  • Dos o más hilos colaboran para obtener un fin común
¿Cómo ejecutar los hilos de manera coordinada?
  • La sincronización es la capacidad de informar de la situación de un hilo a otro.
  • La comunicación es la capacidad de transmitir información desde un hilo a otro.
En Java la sincronización y comunicación de hilos se consigue mediante:
  • monitores. Se crean al marcar bloques de código con la palabra synchronized;
  • semáforos. indicador de condición que registra si un recurso está disponible o no.
  • Notificaciones. Permiten comunicar hilos mediante los métodos wait, notify y notifyAll de la clase java.lang.Object.

7.1.- Información compartida entre hilos

Las secciones críticas son aquellas secciones de código que no pueden ejecutarse concurrentemente, pues en ellas se encuentran los recursos o información que comparten diferentes hilos, y que por tanto pueden ser problemáticas.
«condición de carrera» = varios hilos acceden a la vez a un mismo recurso
La sincronización se consigue mediante:
  • exclusión mutua. Asegurar que un hilo tiene acceso a la sección crítica de forma exclusiva y por un tiempo finito.
  • por condición. Asegurar que un hilo no progrese hasta que se cumpla una determinada condición.

7.2.- Monitores. Métodos synchronized

Un monitor es un objeto que implementa acceso bajo exclusión mutua a todos sus métodos, y provee sincronización. En Java, un monitor es una porción de código protegida por un mutex o lock. Para crear un monitor en Java, hay que marcar un bloque de código con la palabra synchronized, pudiendo ser ese bloque:
  • un método completo,
  • cualquier segmento de código.
Un objeto con métodos synchronized proporciona un cerrojo único ¿Qué bloques interesa marcar como synchronized? Precisamente los que se correspondan con secciones críticas y contengan el código o datos que comparten los hilos.
Los monitores son reentrantes por que pueden adquirir el mismo cerrojo varias veces, es decir, cuando un hilo no se excluye a sí mismo.

7.3.- Monitores. Segmentos de código synchronized

synchronize= poner las llamadas a los métodos que se quieren sincronizar dentro de segmentos sincronizados
Funcionamiento:
  • Se le indica al método el objeto que se quiere sincronizar.
  • Llamada al método que se quiere sincronizar.
  • El hilo que entra en el segmento declarado synchronized se hará con el monitor del objeto, si está libre, o se bloqueará en espera de que quede libre.El monitor se libera al salir el hilo del segmento de código synchronized.
  • Sólo un hilo puede ejecutar el segmento synchronized a la vez.
Debes tener en cuenta que:
  • La adquisición y liberación de monitores genera una sobrecarga.
  • Siempre que sea posible, por legibilidad del código, es mejor sincronizar métodos completos.
  • Al declarar bloques synchronized puede aparecer un nuevo problema, denominado interbloqueo (lo veremos más adelante).

7.4.- Comunicación entre hilos con métodos de java.lang.Object

Métodos para la comunicación entre hilos:

  • wait(). Detiene el hilo (pasa a «no ejecutable»), el cual no se reanudará hasta que otro hilo notifique que ha ocurrido lo esperado.
  • wait(long tiempo). Como el caso anterior pero pasando el tiempo por parámetro.
  • notify(). Notifica a uno de los hilos puestos en espera para el mismo objeto, que ya puede continuar.
  • notifyAll(). Notifica a todos los hilos

La llamada a estos métodos se realiza dentro de bloques synchronized.

7.5.- El problema del interbloqueo (deadlock)

El interbloqueo = uno a más hilos, se bloquean o esperan indefinidamente. A dicha situación se llega:
  • Porque cada hilo espera a que le llegue un aviso de otro hilo que nunca le llega.
  • Porque todos los hilos, de forma circular, esperan para acceder a un recurso.
Otro problema, menos frecuente, es la inanición = un hilo no puede tener acceso regular a los recursos compartidos y no puede avanzar..

7.6.- La clase Semaphore

La clase Semaphore del paquete java.util.concurrent, permite definir un semáforo para controlar el acceso a un recurso compartido.
Para crear y usar un objeto Semaphore haremos lo siguiente:
  • Indicar al constructor Semaphore (int permisos) se le pasa el número de hilos que pueden acceder a la vez al recurso.
  • Indicar al semáforo mediante el método acquire(), que queremos acceder al recurso, o bien mediante acquire(int permisosAdquirir) cuántos permisos se quieren consumir al mismo tiempo.
  • Indicar al semáforo mediante el método release(), que libere el permiso, o bien mediante release(int permisosLiberar), cuantos permisos se quieren liberar al mismo tiempo.
  • Hay otro constructor Semaphore (int permisos, boolean justo) que mediante el parámetro justo permite garantizar que el primer hilo en invocar adquire() será el primero en adquirir un permiso cuando sea liberado.
Usos de Semaphore.
  • Si se usa para proteger secciones críticas.
  • Si se usa para comunicar hilos.

7.7.- La clase Exchanger

.

La clase Exchanger, del paquete java.util.concurrent, establece un punto de
sincronización donde se intercambian objetos entre dos hilos.

Existen dos métodos definidos en esta clase:

● exchange(V x).
● exchange(V x, long timeout, TimeUnit unit).
Ambos métodos exchange() permiten intercambiar objetos entre dos hilos.

El funcionamiento:

● Dos hilos (hiloA e hiloB) intercambiarán objetos del mismo tipo, objetoA y objetoB.
● El hiloA invocará a exchange(objetoA) y el hiloB invocará a exchange(objetoB).
● El hilo que procese su llamada a exchange(objeto) en primer lugar, se bloqueará y quedará a la espera del segundo. Cuando eso ocurra y se acabe el bloqueo se producirá el intercambio de objetos.


7.8.- Las clase CountDownLatch.

La clase CountDownLatch del paquete java.util.concurrent es una utilidad de sincronización que permite que uno o más threads esperen hasta que otros threads finalicen su trabajo.
El funcionamiento esquemático de CountDownLatch o «cuenta atrás de cierre» es el siguiente:
● Implementa un punto de espera que denominaremos «puerta de cierre»
● Los hilos que deben finalizar su trabajo se controlan mediante un contador que llamaremos «cuenta atrás».
● Cuando la «cuenta atrás» llega a cero se reanudará el trabajo del hilo o hilos
interrumpidos y puestos en espera.
● No se puede reiniciar. Si fuera necesario reiniciar la «cuenta atrás» habrá que pensar en utilizar la clase CyclicBarrier.

Los aspectos más importantes al usar la clase CountDownLatch son los siguientes:

● Al constructor countDownLatch(int cuenta) se le indica, mediante el parámetro «cuenta», el total de hilos que deben completar su trabajo,
● método await() esperará en la «puerta de cierre» hasta que la «cuenta atrás» tome el valor cero. También se puede utilizar el método await(long tiempoespera, TimeUnit unit), para indicar que la espera será hasta que la
cuenta atrás llegue a cero o bien se sobrepase el tiempo
● La «cuenta atrás» se irá decrementando mediante la invocación del método countDown()
● No se puede reinicar
● El método getCount() obtiene el valor actual de la «cuenta atrás»


7.9.- La clase CyclicBarrier.

La clase CyclicBarrier permite que uno o más threads se esperen hasta que todos ellos finalicen su trabajo.

El funcionamiento:

● Implementa un punto de espera que llamaremos «barrera», donde cierto número de hilos esperan a que todos ellos finalicen su trabajo.
● Finalizado el trabajo de estos hilos, se dispara la ejecución de una determinada acción o bien el hilo interrumpido continúa su trabajo.
● se puede reiniciar.

Los aspectos más importantes:

● Indicar al constructor CyclicBarrier(int hilosAcceden) el total de hilos que van a usar la barrera mediante el parámetro hilosAcceden.
● La barrera se dispara cuando llega el último hilo.
● Cuando se dispara la barrera, dependiendo del constructor, lanzará o no una acción, y entonces se liberan los hilos de la barrera.
● El método principal de esta clase es await()


8.- Aplicaciones multihilo.

propiedades aplicación multihilo:
Seguridad. La aplicación no llegará a un estado inconsistente por un mal uso de los recursos compartidos.
Viveza. La aplicación no se bloqueará o provocará que un hilo no se pueda ejecutar.

La corrección de la aplicación se mide:

● Corrección parcial. Se cumple la propiedad de seguridad.
● Corrección total. Se cumplen las propiedades de seguridad y viveza

Tener en cuenta los siguientes aspectos:

● La situación de los hilos en la aplicación:
○ Independientes. No será necesario sincronizar y/o comunicar los hilos.
○ Colaborando y/o compitiendo. Será necesario sincronizar y/o comunicar los hilos.
● Gestionar las prioridades
● No todos los Sistemas Operativos implementan time-slicing.
● La ejecución de hilos es no-determinística.

Ventajas:

● Facilitar la programación.
● Mayor rendimiento.
● Mayor fiabilidad.
● Menor mantenimiento.
● Mayor productividad.


8.2.- La interfaz Executor y los pools de hilos.

Un pool de hilos (thread pools) es básicamente un contenedor dentro del cual se crean y se inician un número limitado de hilos, para ejecutar todas las tareas de una lista.
Para declarar un pool, lo más habitual es hacerlo como un objeto del tipo ExecutorService utilizando alguno de los siguientes métodos de la clase estática

Executors:

● newFixedThreadPool(int numeroHilos): crea un pool con el número de hilos indicado.
● newCachedThreadPool(): crea un pool que va creando hilos conforme se van necesitando.
● newSingleThreadExecutor(): crea un pool de un solo hilo.
● newScheduledExecutor() : crea un pool que va a ejecutar tareas programadas cada cierto tiempo, ya sea una sola vez o de manera repetitiva.
La interface ExecutorService proporciona una serie de métodos para el control de la
ejecución de las tareas, entre ellos el método shutdown(), para indicarle al pool que
los hilos no se van a reutilizar y deben morir


8.3.- Gestión de excepciones.

Para gestionar las excepciones de una aplicación multihilo puedes utilizar el método uncaughtExceptionHandler() de la clase thread.

Para crear un manejador de excepciones:

● Crear una clase que implemente la interfaz thread.UncaughtExceptionHandler.
● Implementar el método uncaughtException().

8.4.- Depuración y documentación. Métodos de la clase thread:

● dumpStack(). Muestra una traza de la pila del hilo (thread) en curso.
● getAllStackTraces(). Devuelve un Map de todos los hilos vivos en la aplicación.
● getStackTrace(). Devuelve el seguimiento de la pila de un hilo
Tanto getAllStackTraces() como getStackTrace() permiten grabar los datos del
seguimiento de pila en un log.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.