Logo DIE

Hilos

Unidad de Apoyo para el Aprendizaje

Iniciar

Introducción


Imaginemos una aplicación departamental que deba realizar varias operaciones complejas. Sus funciones son descargar el catálogo de precios de los productos nuevos, realizar la contabilidad del día anterior y aplicar descuentos a productos existentes.

En un flujo normal, las tareas se realizarían de forma secuencial, es decir, las tareas se ejecutarán una después de la otra. Sin embargo, si la descarga de productos nuevos tarda demasiado, los descuentos no se podrían aplicar hasta que este proceso termine y, si se requiere un producto con descuento, éste no se podrá aplicar.

Lo ideal sería tener varios flujos de ejecución para poder realizar una tarea sin necesidad de esperar a las otras. Esto se puede lograr utilizando hilos, donde cada hilo representaría una tarea que se ejecuta de manera independiente.



Implementar el concepto de multitarea utilizando hilos en un lenguaje orientado a objetos.

Hilos (threads)


Un hilo es un código en ejecución dentro de un proceso, es decir, un subproceso. Mientras que un proceso es un programa en ejecución dentro de su propio espacio de direcciones, es decir, una aplicación que se ejecuta en el sistema operativo.

Un hilo no puede ejecutarse solo, requiere la supervisión de un proceso; por ello su ejecución está controlada por el contexto de un proceso donde se ejecuta.

Hilos en Java

La máquina virtual java (JVM) es capaz de manejar multihilos. Puede crear varios flujos de ejecución de manera simultánea, administrando los detalles como asignación de tiempos de ejecución o prioridades de los hilos, de forma similar a como administra un sistema operativo múltiples procesos.

Para el manejo de hilos Java tiene una interfaz y un conjunto de clases en su API:



Hilo



Tanto las clases como la interfaz son parte del paquete básico de Java (java.lang).



Ciclo de vida de un hilo

El campo de acción de un hilo está compuesto por la etapa runnable, que es cuando el hilo se está ejecutando.



Hilos

(s. a.) (s. f.). Hilos [imagen]. Tomada de https://mastermoviles.gitbook.io/introduccion-a-java-y-eclipse/hilos





Un hilo se encuentra en el estado new (nuevo) la primera vez que se crea y hasta que el método start es llamado. Los hilos en estado new ya han sido inicializados y están listos para empezar a trabajar; pero todavía no han sido notificados para que empiecen a realizar su trabajo.

Cuando se llama al método start de un hilo nuevo, el método run es invocado; en ese momento el hilo entra en el estado runnable y, por tanto, el hilo se encuentra en ejecución.

Cuando un hilo está detenido se dice que está en estado not running. Los hilos pueden pasar al estado not running por los métodos suspend, sleep y wait o por algún bloqueo de I/O.

Dependiendo de la manera en la que el hilo pasó al estado not running es como se puede regresar al estado runnable:



runnable

Un hilo entra en estado dead cuando ya no es un objeto necesario. Los hilos en estado dead no pueden ser resucitados, es decir, ejecutados de nuevo. Un hilo puede entrar en estado dead por dos causas:



Diagrama de bloques




Planificador


Java tiene un planificador (scheduler) que controla todos los hilos que se están ejecutando en todos sus programas y decide cuáles deben ejecutarse (dependiendo de su prioridad) y cuáles deben encontrarse preparados para su ejecución.



Prioridad

Un hilo tiene una prioridad (un valor entero entre 1 y 10), de modo que cuanto mayor es el valor, mayor es la prioridad.

El planificador determina qué hilo debe ejecutarse en función de la prioridad asignada a cada uno de ellos. Cuando se crea un hilo en Java, éste hereda la prioridad de su padre. Una vez creado el hilo es posible modificar su prioridad en cualquier momento utilizando el método setPriority.

El planificador ejecuta primero los hilos de prioridad superior y sólo cuando éstos terminan, puede ejecutar hilos de prioridad inferior. Si varios hilos tienen la misma prioridad, el planificador elige entre ellos de manera alternativa (forma de competición). Cuando pasa a ser “Ejecutable”, entonces el sistema elige a este nuevo hilo.

Clase Thread


Es la clase en Java responsable de producir hilos funcionales para otras clases. Para añadir la funcionalidad de hilo a una clase sólo se hereda de ésta.



Hilo



Para añadir la funcionalidad deseada a cada hilo creado es necesario redefinir el método run. Este método es invocado cuando se inicia el hilo.

Un hilo se inicia mediante una llamada al método start de la clase Thread que, a su vez, hace una llamada al método run y finaliza cuando este método llega a su fin.

Ejemplo

public class ThreadClass extends Thread {
         public ThreadClass(String name) {
                super(name);
        }

        public void run() {
                 for(int i = 0 ; i < 5 ; i++) {
                        System.out.println("Iteration " + (i+1) + " from " + getName());
                 }
                 System.out.println("Ends " + getName());
        }
}

public class TestThreadClass {
        public static void main(String[] args) {
                new ThreadClass("First one").start();
                new ThreadClass("Second one").start();
                System.out.println("Ends thread main.");
        }
}

Como se puede observar, el método run en la clase ThreadClass contiene el bloque de ejecución del hilo. El método principal en la clase TestThreadClass crea dos objetos del tipo ThreadClass y manda llamar al método start (en cada caso). El método start inicia un nuevo hilo y manda llamar al método run.

Se observa que la ejecución de los tres hilos (el método principal y los hilos generados) es asíncrona. Si vuelves a ejecutar el programa es probable que las ejecuciones cambien.



Interfaz Runnable

La interfaz Runnable permite producir hilos funcionales para otras clases. Para añadir la funcionalidad de hilo a una clase por medio de Runnable sólo se necesita implementar la interfaz.

La interfaz Runnable proporciona una alternativa al uso de la clase Thread, para aquellos casos en los que no es posible hacer que la clase definida herede de la clase Thread (recuerda que en Java no existe herencia múltiple).

La interfaz Runnable define un método run, el cual puede ser ejecutado por un objeto Thread.

Ejemplo

public class RunnableClass implements Runnable {
        public void run() {
                for(int i = 0 ; i < 5 ; i++) {
                        System.out.println("Iterationn " + (i+1) + " de “ + Thread.currentThread().getName());
                                }
                                System.out.println("Termina el " + Thread.currentThread().getName());
        }
}


public class TestRunnableClass {
        public static void main(String[] args) {
                new Thread(new RunnableClass(), "First thread").start();
                new Thread(new RunnableClass(), "Second thread").start();
                System.out.println("Ends main thread.");
        }
}

Este programa realiza la misma tarea que el anterior. Nótese que para crear hilos utilizando Runnable es necesario crear una instancia de Thread pasando como parámetro la instancia de la clase que implementa Runnable.

La elección del método a utilizar para crear hilos (heredar de Thread o implementar Runnable) va a depender de las necesidades del programador.

Si se implementa la interfaz Runnable se realiza un mejor diseño orientado a objetos, permitiendo una alta cohesión. Si se hereda de la clase Thread, se genera código más simple.

Clase ThreadDeath

ThreadDeath deriva de la clase Error, la cual proporciona medios para manejar y notificar errores.

Cuando el método stop de un hilo es invocado, una instancia de ThreadDeath es lanzada por el hilo como un error, ya que el hilo se detiene de forma asíncrona. El hilo morirá cuando reciba realmente el error ThreadDeath.

Sólo se debe recoger el objeto ThreadDeath si se necesita realizar una limpieza específica para la ejecución asíncrona, lo cual es una situación bastante inusual. Si se recoge el objeto, debe ser relanzado para que el hilo en realidad muera.



Clase ThreadGroup

ThreadGroup se utiliza para manejar un grupo de hilos de manera simplificada (en conjunto). Esta clase proporciona una manera de controlar de modo eficiente la ejecución de una serie de hilos.

ThreadGroup proporciona métodos como stop, suspend y resume para controlar la ejecución del grupo (todos los hilos del grupo).

Los hilos de un grupo pueden, a su vez, contener otros grupos de hilos permitiendo una jerarquía anidada de hilos. Los hilos individuales tienen acceso al grupo, pero no al padre del grupo.

Ejemplo

public class GroupThread extends Thread {

        public GroupThread(ThreadGroup group, String name){
                super(group, name);
        }

         public void run(){
                for (int i = 0 ; i < 10 ; i++){
                        System.out.print("\t" + getName() + "\t");
                }
        }

        public static void listarHilos(ThreadGroup group) {
                int number;
                Thread [] list;
                number = group.activeCount();
                list = new Thread[number];
                group.enumerate(list);
                System.out.println("\nHilos activos en el grupo= “ + number);
                for (int i = 0 ; i < number ; i++) {
                        System.out.println("Hilo: " + list[i].getName());
                }
        }
}

public class TestGroupThread {
        public static void main(String[] args) {
                ThreadGroup grupo = new ThreadGroup("Grupo de hilos.");
                GroupThread [] hilos = new GroupThread[5];

                for (int cont=0 ; cont<hilos.length ; cont++) {
                        hilos[cont] = new GroupThread(grupo, "Hilo " + (cont+1));
                }

                for (int cont=0 ; cont<hilos.length ; cont++) {
                        hilos[cont].start();
                }

                GroupThread.listarHilos(grupo);
        }
}

Método yield()

El método estático yield tiene como propósito regresar al hilo que está en ejecución al estado runnable para permitir que los otros hilos puedan entrar en ejecución, provocando un uso equitativo de recursos del procesador entre los hilos en ejecución (en teoría).



Método join()

El método join permite que un hilo se una al final de otro hilo, es decir, detiene su ejecución hasta que el otro hilo termine.

Comencemos del supuesto de que un hilo B no puede terminar su ejecución hasta que el hilo A haya terminado su tarea; entonces B se debe unir (join) a A y, por tanto, B no se va a ejecutar hasta que A termine.

Ejemplo

public class ThreadJoin extends Thread {
        private String name;
        private int sleepTime;
        private Thread waitsFor;

        public ThreadJoin(){}

        public ThreadJoin(String name, int sleepTime, Thread waitsFor) {
                this.name = name;
                this.sleepTime = sleepTime;
                this.waitsFor = waitsFor;
        }
public void run() {
                System.out.print("[" + name + " ");

                try {
                        Thread.sleep(sleepTime);
                } catch(InterruptedException ie) { }

                System.out.print(name + "? ");

                if (!(waitsFor == null))
                        try {
                                waitsFor.join();
                        } catch(InterruptedException ie) { }

                System.out.println(name + "] ");
        }
}


public class TestThreadJoin {
        public static void main (String [] args) {
                 Thread t1 = new                  ThreadJoin("1", 1000, null);
                 Thread t2 = new ThreadJoin("2", 4000, t1);
                 Thread t3 = new ThreadJoin("3", 600, t2);
                 Thread t4 = new ThreadJoin("4", 500, t3);
                 t1.start();
                 t2.start();
                 t3.start();
                 t4.start();
        }
}



ícono

Actividad. Caja asíncrona de supermercado

Los hilos (threads) permiten ejecutar un bloque de código de manera asíncrona a la aplicación principal, lo que mejora la eficiencia con procesos que pueden ejecutarse de una manera no secuencial, ya que lanzan múltiples hilos que se ejecutan de manera concurrente.

ícono

Autoevaluación. Estados y métodos de hilos en Java

Los sistemas asíncronos son muy útiles en aplicaciones robustas, crear o consultar elementos en background es una práctica muy utilizada para economizar tiempos. En Java se pueden crear procesos asíncronos utilizando hilos; por ello es importante conocer los estados por los que puede pasar un hilo y los métodos principales que maneja Java.


Fuentes de información

Dean, J. y Dean, R. (2009). Introducción a la programación con Java. McGraw-Hill.

Martín, A. (2008). Programador certificado Java 2 (2.a ed.). Alfaomega Grupo Editor.

Sierra, K. y Bates, B. (2008). SCJP Sun Certified Programmer for Java 6 Study Guide. McGraw-Hill.


Cómo citar


Solano, J. A. (2020). Hilos. Unidades de Apoyo para el Aprendizaje. CUAIEED/Facultad de Ingeniería-UNAM. (Vínculo)