Tema 12: Programación multihilo Antonio J. Sierra Índice 1. Modelo de hilo en Java. 2. El hilo principal. 3. Creación de un hilo. 4. Creación de múltiples hilos. Prioridades. 5. Comunicación entre hilos. Sincronización. 6. Modelado UML para la programación multihilo. Clases activas. 1 Introducción • Un programa multihilo contiene dos o más partes que pueden ejecutarse de forma concurrente. • Cada parte de ese programa se llama hilo (Thread) y cada hilo establece un camino de ejecución independiente. • La concurrencia reúne varios hilos de ejecución. • Forma especializada de multitarea (multitasking). – basada en procesos – basada en hilos Multitarea basada en Procesos • Proceso es un programa que se está ejecutando. • Multitarea basada en procesos se puede decir que es la característica que le permite a la computadora ejecutar dos o más programas concurrentemente. • Un programa es la unidad de código más pequeña que el planificador puede seleccionar. • Los procesos son tareas pesadas que necesitan su propio espacio de direccionamiento. La comunicación entre procesos es más cara y limitada. • Es costoso el cambio de contexto de un proceso a otro. 2 Multitarea basada en hilos • El hilo es la unidad de código más pequeña que se puede seleccionar. • La multitarea basada en hilos requiere menos sobrecarga que la multitarea basada en procesos. • Los hilos son más ligeros, ya que comparten el mismo espacio de direcciones y comparten cooperativamente el mismo proceso pesado. • La comunicación entre hilos es ligera y el cambio de contexto de un hilo al siguiente es menos costoso. El modelo de hilo en Java • Java utiliza hilos para permitir que el entorno en su globalidad sea asíncrono. 3 Estados de los hilos • Un hilo puede estar ejecutándose. • Puede estar preparado para ejecutarse tan pronto como disponga de tiempo de CPU. • Si se está ejecutando puede suspenderse, lo que equivale a detener temporalmente su actividad. • El hilo suspendido puede reanudarse permitiendo que continúe su tarea allí donde la dejó. • Un hilo puede estar bloqueado cuando espera un recurso. • Un hilo puede detenerse, finalizando su ejecución de manera inmediata. Una vez detenido, un hilo no puede reanudarse. El ciclo de vida de un Thread En ejecución start Nuevo Hilo yield Ejecución Suspendido El método run termina Detenido 4 Prioridades de los hilos • El intérprete de Java utiliza prioridades para determinar cómo debe tratar cada hilo con respecto a los demás. • La prioridad de un hilo es un valor entero que asigna un orden de ejecución cuando los hilos estén preparados para ejecutarse o ejecutándose • La prioridad de un hilo se utiliza para decidir cuándo se pasa a ejecutar otro hilo. – Esto es lo que se conoce como cambio de contexto. Reglas para el cambio de contexto • Un hilo puede ceder voluntariamente el control. – Esto se hace por abandono explícito, al quedarse dormido o al bloquearse en espera de una E/S pendiente. En este caso, se examinan todos los hilos restantes y se selecciona para su asignación a la CPU aquél que, estando listo para su ejecución, tenga la prioridad más alta. • Un hilo puede ser desalojado por otro con prioridad más alta. – En este caso, un hilo de baja prioridad que no libera la CPU es desalojado por otro de mayor prioridad con independencia de lo que estuviese haciendo en ese instante. 5 Sincronización (I) • Los hilos permiten y potencian el comportamiento asíncrono de los programas, – forma de forzar el sincronismo donde sea necesario – Haciendo que coincidan en el tiempo dos o más hilos de ejecución • Java implementa una versión de modelo clásico de sincronización entre procesos, llamado monitor. • El monitor es un mecanismo de control que fue definido en primer lugar por C.A.R. Hoare y que puede entenderse como una pequeña caja en la que sólo cabe un hilo. – Una vez que un hilo entra en el monitor, los demás deben esperar a que éste salga. – Los monitores se utilizan para proteger un bien compartido y evitar que sea manipulado por más de un hilo simultáneamente. Sincronización (II) • Cada objeto tiene su propio monitor implícito en el que entra automáticamente cuando se llama a uno de los métodos sincronizados del objeto. • Una vez que un hilo está dentro de un método sincronizado, ningún otro hilo puede llamar a otro método sincronizado del mismo objeto. 6 El hilo principal class HiloActual { public static void main (String args[]) { Thread t = Thread.currentThread(); System.out.println("Hilo actual: " +t); //cambia el nombre del hilo t.setName("Mi hilo"); System.out.println("después del cambio de nombre: " +t); try { for (int n = 5; n>0 ; n--) { System.out.println(n); Thread.sleep(1000); } }catch (InterruptedException e){ System.out.println("Interrupcion del hilo principal"); } Hilo actual: Thread[main,5,main] } después del cambio de nombre: Thread[Mi hilo,5,main] 5 4 3 2 1 } Creación de un hilo • Dos opciones: – Implementando la interfaz Runnable. – Extendiendo la clase Thread. • La clase Thread define varios métodos que pueden sobrescribir las clases derivadas. • El único que tiene que ser sobrescrito es run(). – Este método es exactamente el mismo que es necesario para implementar la interfaz Runnable. • Implementar la interfaz permite utilizar herencia de cualquier otra clase diferente. 7 Implementando la interfaz Runnable • Si un objeto implementa la interfaz Runnable se puede usar para crear un hilo. • El comienzo del hilo (con start()) provoca que el método run() del hilo se pueda invocar de forma separada. Thread(Runnable objetoHilo, String nombreHilo) synchronized void start() public abstract void run() Ejemplo con Runnable (I) //Crea un segundo hilo. class NuevoHilo implements Runnable { Thread t; NuevoHilo() { //Crea un nuevo hilo t = new Thread(this,"Hilo hijo"); System.out.println("Hilo hijo: "+t); t.start(); //comienza el hilo } //Este es el punto de entrada del segundo hilo public void run() { try { for (int i = 5; i > 0; i--){ System.out.println("Hilo hijo: " +i); Thread.sleep(500); } }catch(InterruptedException e) { System.out.println("Interrupcion de hilo hijo"); } System.out.println("Sale del hilo hijo"); } } 8 Ejemplo con Runnable (II) class Hilos0 { public static void main(String args[]){ new NuevoHilo(); //crea un nuevo hilo try{ for (int i = 5; i>0 ; i--){ System.out.println("Hilo Principal: " +i); Thread.sleep(1000); } } catch(InterruptedException e) { System.out.println("Interrupcion del hilo principal"); } System.out.println("Sale del hilo principal."); } } Extendiendo la clase Thread • Crea una nueva clase que herede de la clase Thread y después crear una instancia de esa clase. • Esta nueva clase debe sobreescribir el método run(), que es el punto de entrada del nuevo hilo. • También debe llamar al método start() para que comience la ejecución del nuevo hilo. 9 Ejemplo con Thread (I) //Crea un hilo extendiendo la clase Thread. class NuevoHilo extends Thread { NuevoHilo() { //Crea un nuevo hilo super("Hilo demo"); System.out.println("Hilo hijo: "+this); start(); //comienza el hilo } //Este es el punto de entrada del segundo hilo public void run() { try { for (int i = 5; i > 0; i--){ System.out.println("Hilo hijo: " +i); Thread.sleep(500); } }catch(InterruptedException e) { System.out.println("Interrupcion de hilo hijo"); } System.out.println("Sale del hilo hijo"); } } Ejemplo con Thread (II) class Hilos1 { public static void main(String args[]){ new NuevoHilo();//crea un nuevo hilo try{ for (int i = 5; i>0 ; i--){ System.out.println("Hilo Principal: " +i); Thread.sleep(1000); } } catch(InterruptedException e) { System.out.println("Interrupcion del hilo principal"); } System.out.println("Sale del hilo principal."); } } 10 Creación de múltiples hilos • Hasta ahora sólo se han utilizado dos hilos: – el hilo principal – y un hilo hijo. • Se pueden generar tantos hilos como necesiten. Ejemplo, creación de varios hilos (I) //Creación de múltiples hilos class NuevoHilo implements Runnable { String nombre; Thread t; NuevoHilo(String NombreHilo) { //Crea un nuevo hilo nombre = NombreHilo; t = new Thread(this, nombre); System.out.println("Nuevo Hilo: " + t); t.start(); //comienza el hilo } //Este es el punto de entrada del hilo public void run() { try { for (int i = 5; i > 0; i--){ System.out.println(nombre + ": " +i); Thread.sleep(1000); } }catch(InterruptedException e) { System.out.println("Interrupcion del hilo "+ nombre); } System.out.println("Sale del hilo " + nombre); } } 11 Ejemplo creación de varios hilos (II) C:\jdk1.2.2\bin>java Hilos2 Nuevo Hilo: Thread[Uno,5,main] Nuevo Hilo: Thread[Dos,5,main] Nuevo Hilo: Thread[Tres,5,main] class Hilos2 { Uno: 5 public static void main(String args[]){ Dos: 5 new NuevoHilo("Uno"); Tres: 5 new NuevoHilo("Dos"); Uno: 4 new NuevoHilo("Tres"); Dos: 4 Tres: 4 try{ Uno: 3 Thread.sleep(10000); Dos: 3 } catch(InterruptedException e) { Tres: 3 System.out.println( Uno: 2 "Interrupcion del hilo principal"); Dos: 2 } Tres: 2 System.out.println( Uno: 1 "Sale del hilo principal."); Dos: 1 Tres: 1 } Sale del hilo Uno } Sale del hilo Dos Sale del hilo Tres Sale del hilo principal. Comunicación entre hilos • Una forma de determinar si un hilo ha terminado de ejecutarse es llamando al método de la clase Thread isAlive(): final boolean isAlive() throws InterruptedException – Devuelve true si el hilo al que se hace referencia está todavía ejecutándose. – Devuelve false en caso contrario. • El método join() se utiliza para esperar la finalización de un hilo. final void join() throws InterruptedException – Este método espera hasta que finalice el hilo sobre el que se llama. – Su nombre surge de la idea de que el hilo llamante espera hasta que el hilo especificado se reúne con él. – Hay otras formas de join() que permiten especificar el tiempo máximo que se quiere esperar la finalización de un hilo. final void join(long millis) throws InterruptedException final void join(long millis, int nanos) throws InterruptedException 12 Ejemplo de Comunicación entre hilos (I) //Uso del método join() para esperar la finalización de hilos class NuevoHilo implements Runnable { String nombre; Thread t; NuevoHilo(String NombreHilo) { //Crea un nuevo hilo nombre = NombreHilo; t = new Thread(this, nombre); System.out.println("Nuevo Hilo: " + t); t.start(); //comienza el hilo } //Este es el punto de entrada del hilo public void run() { try { for (int i = 5; i > 0; i--){ System.out.println(nombre + ": " +i); Thread.sleep(1000); } }catch(InterruptedException e) { System.out.println("Interrupcion del hilo "+ nombre); } System.out.println("Sale del hilo " + nombre); } } Ejemplo de Comunicación entre hilos (II) class Hilos3{ public static void main(String args[]){ NuevoHilo ob1 = new NuevoHilo("Uno"); NuevoHilo ob2 = new NuevoHilo("Dos"); NuevoHilo ob3 = new NuevoHilo("Tres"); System.out.println("El hilo Uno está vivo: "+ob1.t.isAlive()); System.out.println("El hilo Dos está vivo: "+ob2.t.isAlive()); System.out.println("El hilo Tres está vivo: "+ob3.t.isAlive()); //espera a que terminen los otros hilos try{ System.out.println("Espera finalización de otros hilos "); ob1.t.join(); ob2.t.join(); ob3.t.join(); } catch(InterruptedException e) { System.out.println("Interrupcion del hilo principal"); } System.out.println("El hilo Uno está vivo: "+ob1.t.isAlive()); System.out.println("El hilo Dos está vivo: "+ob2.t.isAlive()); System.out.println("El hilo Tres está vivo: "+ob3.t.isAlive()); System.out.println("Sale del hilo principal "); } } La salida de este programa es la siguiente: C:\jdk1.2.2\bin>java Hilos3 Nuevo Hilo: Thread[Uno,5,main] Nuevo Hilo: Thread[Dos,5,main] Nuevo Hilo: Thread[Tres,5,main] El hilo Uno estß vivo: true El hilo Dos estß vivo: true El hilo Tres estß vivo: true Espera finalizaci_n de otros hilos Uno: 4 Dos: 4 Tres: 4 Uno: 3 Dos: 3 Tres: 3 Uno: 2 Dos: 2 Tres: 2 Uno: 1 Dos: 1 Tres: 1 Sale del hilo Uno Sale del hilo Dos Sale del hilo Tres El hilo Uno estß vivo: false El hilo Dos estß vivo: false El hilo Tres estß vivo: false Sale del hilo principal 13 Suspensión y Reanudación de un hilo • Son dos métodos marcados como “Deprecated”: final void resume() final void suspend() • La clase Object proporciona los siguientes métodos: – void wait() – void wait(long timeout) – void wait(long timeout, int nanos) • Provoca que el hilo actual espere hasta que otro hilo invoque a notify() o notifyAll(). – void notify () • Despierta un solo hilo que estaba esparando en este monitor del objeto. – void notifyAll () • Despierta todos los hilos que estaban esperando en este monitor del objeto. Prioridad • El planificador de hilos utiliza las prioridades de los hilos para determinar cuándo debe permitir que se ejecute cada hilo. • Si dos hilos están preparados para ejecutarse se ejecutará el de mayor prioridad. • Prioridad es un valor entero (5 por defecto) comprendido entre Thread.MIN_PRIORITY y Thread.MAX_PRIORITY. • Se puede gestionar mediante los métodos: final void setPriority(int nivel) final int getPriority () 14 Sincronización • Cuando dos o más hilos necesitan acceder de manera simultánea a un recurso compartido, necesitan asegurarse de que sólo uno de ellos accede al mismo en un instante dado. El proceso mediante el cual se consigue esto se llama sincronización. • Un monitor es un objeto que se utiliza como cerrojo exclusivo, o mutex (mutually exclusive, mutuamente exclusivo). • Cuando un hilo adquiere un cerrojo, se dice que ha entrado en el monitor. Los restantes hilos que estuviesen intentando acceder al monitor bloqueado quedan en suspensión hasta que el primer hilo salga del monitor. Se dice que estos hilos están esperando al monitor. Un hilo que posea un monitor puede volver a acceder al mismo si así lo desea. La sincronización del código se puede realizar mediante la palabra clave synchronized (sincronizado). – Java proporciona un soporte único, a nivel de lenguaje, para la sincronización. – Sólo uno de los hilos puede ser el propietario del monitor en un instante dado. • • Ejemplo sin sincronización //Este programa no está sincronizado. class Llamada { void llama(String msg){ System.out.print("["+msg); try { Thread.sleep(1000); }catch(InterruptedException e){ System.out.println("Interrumpido"); } System.out.print("]"); } } class ElQueLlama implements Runnable { String msg; Llamada objetivo; Thread t; public ElQueLlama(Llamada objet, String s) { objetivo = objet; msg = s; t = new Thread(this); t.start(); } public void run() { objetivo.llama(msg); } } class Sincro0{ public static void main(String[] args){ Llamada objetivo ElQueLlama ob1 = ElQueLlama ob2 = ElQueLlama ob3 = "Sincronizado"); = new Llamada(); new ElQueLlama(objetivo, "Hola"); new ElQueLlama(objetivo, "Mundo"); new ElQueLlama(objetivo, //Espera a que los hilos terminen try { ob1.t.join(); ob2.t.join(); ob3.t.join(); }catch(InterruptedException e){ System.out.println("Interrumpido"); } } } La salida producida por este programa. [Hola[Mundo[Sincronizado]]] 15 Con Sincronización class Llamada { synchronized void llama(String msg){ //… } • Esto evitará que otros métodos puedan acceder a llama() mienta otro lo está utilizando. • La salida [Hola] [Mundo] [Sincronizado] • Otra forma es sincronizar un objeto en un conjunto de sentencias. synchronized (objeto){ //Sentencias que deben ir sincronizadas } Modelado UML para la programación multihilo. Clases activas. • Las clases activas son elementos estructurales de los bloques del construcción del modelo conceptual de UML. • Una clase activa es una clase cuyos objetos tienen uno o más procesos o hilos que constituyen flujos de control independientes pero concurrentes con otros flujos de control (con los que muy probablemente se deberán sincronizar). • Una clase activa es igual que una clase, excepto en que sus objetos representan elementos cuyo comportamiento es concurrente con otros elementos. 16 Modelado UML para la programación multihilo. Clases activas. GestorEventos supender() 17