1. Hilos
Al arrancar un programa Java, este se ejecuta por defecto sobre un único hilo de ejecución, es decir, por ejemplo, cuando se programa un evento listener en un botón cuya ejecución resultase muy pesada, tardando un tiempo prolongado en procesarse, provocaba que la ventana quedase bloqueada
no pudiendo recibir más eventos hasta que un evento listener se terminase de ejecutar. Para estos casos, es necesario ejecutar el evento en otro hilo en paralelo al hilo principal.
Java posee mecanismos para ejecutar ciertas clases abriendo un nuevo hilo (thread). A la hora de crear hilos, tenemos que tener presente que en el sistema operativo solo existirá un único proceso de ejecución (el programa java.exe que lanzó la aplicación en el caso de SO Windows), por lo tanto,
todos los threads abiertos sobre este proceso compartirán ciertos recursos asignados por el sistema operativo, por ejemplo, la cantidad de memoria atribuida al proceso tendrá que ser administrada para todos los hilos. Para tener claro el concepto de thread, podemos imaginar la propiedad de
auto salvado de la mayoría de procesadores de texto. Esta funcionalidad se ejecuta en segundo plano según se va introduciendo texto en el documento, es decir la operación de auto salvado no bloquea al usuario, permitiéndoles que siga escribiendo mientras el archivo se está guardando.
2. Clase Thread e Interfaz Runnable
Un thread o un hilo de ejecución en Java, generalmente se abrirá a través de la clase java.lang.thread, que a su vez implementa la interfaz java.lang.runnable.
2.1 ¿Qué es un Thread?
Los principales métodos del objeto Thread son:
La clase Thread también dispone de una serie de métodos estáticos:
Entre los métodos de las dos tablas anteriores, no se ha hecho referencia a dos funciones de vital importancia en el manejo de hilos. Estos son los métodos wait() y notify(), que derivan de la superclase Object. El método wait dejará el hilo actual en un estado de espera, hasta que se invoque al método notify que volverá a activar el hilo.
2.2 Ciclo de vida de un thread
Como vemos en la figura anterior un thread puede tener los
siguientes estados:
- Ready: El thread está listo para ser ejecutado.
- Running: El thread está siendo ejecutado.
- Waiting: El thread está esperando a que ocurra un evento para continuar con su ejecución.
- Sleeping: El thread se encuentra detenido.
- Blocked: El thread está esperando a que se liberen los objetos necesarios para poder continuar.
- Dead: Finalización de un thread.
2.3 Creando un thread
Existen dos formas básicas para implementar un thread, tal y como se refleja en la figura.
La primera técnica que veremos para crear un nuevo hilo de ejecución será extendiendo la propia clase Thread.
Clase que extiende de Thread.
En el ejemplo anterior hemos extendido la clase Thread sobrescribiendo el método run que será el que se ejecute en el nuevo hilo cuando se invoque al método start de la clase, tal y como lo hace la main del listado. En ambas clases hemos llamado al método sleep, para que los hilos se queden dormidos durante un corto período de tiempo y permitirnos ver mejor como las dos clases se ejecutan en threads distintos, de ahí la razón que al invocar a la clase main, podamos observar cómo se entremezclan en la consola de salida, los System.out de la clase Hilos y ThreadExample, esto quiere decir que se están ejecutando en paralelo.
La otra opción que tenemos a la hora de crear threads es implementar la interfaz Runnable, que al igual que en el caso anterior, su método principal tendrá el nombre run (). Para ejecutar la implementación de Runnable como un nuevo hilo, pasaremos una instancia de la misma al constructor de la clase Thread.
Por ejemplo:
new Thread (new
MiClaseQueImplemetaRunnable()).start().
Así pues, podemos cambiar los listados para construir el thread
implementando a la interfaz Runnable.
Clase que implementa Runnable.
Clase que se ejecuta en un thread principal y abre uno nuevo.
2.4 Sincronización de métodos
Cuando trabajamos con múltiples hilos, tal vez necesitamos que un método no pueda ser ejecutado al mismo tiempo por varios threads, por ejemplo, tal vez tengamos una función que escribe en un fichero, resultando imposible que varios hilos intenten a la vez, escribir en él. Para estos casos existe el modificador synchronized, que utilizaremos en la definición del método que queremos que sea sincronizado entre distintos hilos. Si un thread intenta acceder a un método sincronizado que está siendo ejecutado por otro hilo, se quedará bloqueado hasta que el primer hilo termine de ejecutarlo.
Esto puede ser peligroso ya que puede ocurrir que un método sincronizado invoque de forma incorrecta a otro que también lo es, dejando los hilos bloqueados. Para ver cómo funcionan los métodos sincronizados utilizaremos los ejemplos anteriores, pero en vez de escribir trazas a través de la instrucción System.out, lo haremos a través de un método de una nueva clase EscribeTrazas:
Clase EscribeTrazas
La clase anterior dispone de dos métodos estáticos que escriben 100 veces el texto pasado como parámetro. La única diferencia entre ellos es que uno es sincronizado. Comenzaremos primero modificando a clase Hilos y RunnableExample para invocar al método no sincronizado.
Clase RunnableExample invocando a EscribeTrazas.
Creando un nuevo hilo e invocando a EscribeTrazas.
Si ejecutamos el ejemplo anterior, obtendremos las trazas entremezcladas, ya que tanto el hilo de la main y el de RunnableExample, escriben a través de un método no sincronizado.
Si cambiamos y utilizamos el método escribeSyn en lugar de escribe, la salida de las trazas cambiará, escribiendo las 100 líneas del primer hilo que lo ha ejecutado y a continuación las 100 del otro.
Finalmente, añadir que la interfaz Runnable es considerada como una interfaz funcional, por lo tanto se pueden aplicar expresiones Lambda. Por ejemplo, podríamos simplificar los programas de la siguiente manera.
new Thread (()-> EscribeTrazas.escribeSyn("Ejecutando
Thread")).start();
EscribeTrazas.escribeSyn("Main Thread:");
No hay comentarios:
Publicar un comentario