Programación de fíos con Java
Sumario
- 1 Introducción
- 2 Utilidades de concurrencia
- 3 Creación de fíos en Java
- 4 A clase Thread
- 5 Xestión e planificación de fíos
- 6 Sincronización e comunicación de fíos
- 6.1 Información compartida entre fíos
- 6.2 Monitores. Métodos synchronized
- 6.3 Monitores. Segmentos de código synchronized
- 6.4 Comunicación entre hilos con métodos de java.lang.Object
- 6.5 O problema do interbloqueo (deadlock)
- 6.6 A clase Semaphore
- 6.7 A clase Exchanger
- 6.8 A clase CountDownLatch
- 6.9 A clase CyclicBarrier
- 7 Outras utilidades de concurrencia de java.util.concurrent
- 8 Depuración (debugging) e documentación de aplicacións multifío
- 9 Resumo
- 10 Créditos e referencias
Introducción
Comezaremos aprendendo a crear e lanzar novos fios (threads) dun proceso con Java, e facer que diferentes fíos dun mesmo proceso colaboren e se coordinen entre sí para facer unha tarefa. No resto deste capítulo veranse os mecanismos e as clases que proporciona a linguaxe Java para a programación de fíos e a súa sincronización.
Utilidades de concurrencia
Java da soporte ó concepto de fío desde a propia linguaxe:
- con algunhas clases e interfaces definidas no paquete java.lang
- con novas utilidades para desenvolver aplicacións multihilo e cun alto nivel de concurrencia do paquete java.util.concurrent
- con métodos específicos para a manipulación de fíos na clase Object
Paquete java.lang
Disponemos dunha interfaz e as siguientes clases para traballar con fíos:
- Clase thread
- Clase responsable de producir fíos funcionais para outras clases e que proporciona gran parte dos métodos utilizados para a súa xestión
- Interfaz Runnable
- Proporciona a capacidade de engadir a funcionalidade de fío a unha clase simplemente implementando a interfaz en lugar de derivándoa da clase thread.
- Clase ThreadDeath
- Clase de error que deriva da clase Error e que proporciona medios para manexar e notificar erros
- Clase ThreadGroup
- Utilízase para manexar un grupo de fíos de xeito conxunto de modo que se poida controlar a súa execución de forma eficiente
- Clase Object
- Non é estrictamente de apoio ós fíos pero proporciona métodos imprescindibles dentro da arquitectura multifío de Java:
- wait()
- notify()
- notifyAll()
Podes consultar nestes enlaces ([1],[2]) un resumo da clase thread cos métodos máis comúns para xestionar e controlar fíos.
Paquete java.util.concurrent
Inclúe unha serie de clases que facilitan enormemente o desenvolvemento de aplicacións multifío complexas, xa que están concebidas para utilizarse como bloques de deseño.
Estas utilidades están dentro dos seguintes paquetes:
- java.util.concurrent.
- Neste paquete están definidos os seguientes elementos:
- Clases de sincronización:
- Semaphore
- CountDownLatch
- CyclicBarrier
- Exchanger
- Interfaces para separar a lóxica da execución, coma por exemplo:
- Executor
- ExecutorService
- Callable
- Future
- Interfaces para xestionar colas de fíos:
- BlockingQueque
- LinkedBlokingQueque
- ArrayBlockingQueque
- SynchronousQueque
- PriorityBlockingQueque
- DelayQueque
- Clases de sincronización:
- java.util.concurrent.atomic
- Inclúe un conxunto de clases para ser usadas coma variables atómicas en aplicaciones multifío e con diferentes tipos de dato. Por exemplo:
- AtomicInteger
- AtomicLong.
- java.util.concurrent.locks
- Define unha serie de clases coma uso alternativo á cláusula sinchronized. Neste paquete atopanse algunhas interfaces coma:
- Lock
- ReadWriteLock.
Creación de fíos en Java
Un fío represéntase mediante unha instancia da clase java.lang.thread. Este obxecto thread emprégase para:
- iniciar
- deter
- cancelar
... a execución do fío de execución.
Os fíos ou threads podense implementar ou definir de dúas formas:
- Extendendo a clase thread.
- Mediante a interfaz Runnable .
En todo caso débese proporcionar unha definición do método run(), xa que este método é o que contén o código que executará o fío (o seu comportamento).
O procedemento de construcción dun fío é independente do seu uso (unha vez creado emprégase da misma forma). Entón, cando utilizar un ou otro procedemento?
- Extender aa clase thread
- é o procedemiento más sinxelo
- non sempre é posible. Se a clase xa herda dalguhna outra clase pai, non será posible herdar tambén da clase thread (Java non permite a hedanza múltiple), polo que haberá que recurrir o outro procedemento
- Implementar Runnable
- sempre é posible
- é o procedemento máis general
- e o procedemento máis flexible.
Exemplo. Na programación de applets, calquera deles ten que herdar da clase java.applet.Applet; e xa non poderá herdar de thread se se quere utilizar fíos. A solución será sempre implementando Runnable.
Cando a Máquina Virtual Java (JVM) arranca a execución dun programa, xa hai un fío executándose, denominado fío principal do programa, controlado polo método main(), que se executa cando comeza o programa e é o último fío que termina a súa execución. Cando este fío finaliza o programa termina.
Exemplo de fío que amosa información da execución
O seguinte exemplo amosa:
- que sempre hai un fío que executa o método main()
- por defecto este fío chámase main.
- para saber qué fío se está executando nun momento dado (o fío en curso) utilizamos o método currentThread() da clase thread
- que obtenemos o seu nome invocando o método getName() da clase thread
1 package PaquetePrincipal; 2 3 /**************************************************************************** 4 * Este programa identifica o fío que executa o método main() da típica aplicación de consola "¡Hola mundo!" 5 * Utilizanse para iso os métodos da clase Thread: currentThread() e getName() 6 */ 7 /** 8 * @author JLV 9 */ 10 public class Main { 11 public static void main(String[] args) { 12 13 //imprime "Ola mundo!" na saída 14 System.out.println("Ola mundo!\n"); 15 16 //obtén o fío onde se está executando este método mediante a función Thread.currentThread(), e o almacena na variable local miHilo 17 Thread miHilo = Thread.currentThread(); 18 19 //imprime o nome do fío na saída (función getName()) 20 System.out.println("Por defecto o fío que executa o método main() " + "do meu programa chámase '" + miHilo.getName() + "'\n"); 21 } 22 }
Exemplo de fío creado extendendo a clase Thread
Para definir e crear un fío extendendo a clase thread:
- Crear unha nova clase que herde da clase thread
- Redefinir na nova clase o método run() co código asociado ó fío as sentenzas que executará.
- Crear un obxecto da nova clase thread. Éste será realmente o fío.
Unha vez creado o fío, para poñelo en marcha ou inicialo:
- Invocar o método start() do obxecto thread (o fío que creamos)
O exemplo amosa como crear un fío extendendo a clase thread. O fío que se crea (obxecto thread hilo1) imprime unha mensaxe de saúdo. Para simplificar o exemplo incluiúse o método main() que inicia o programa na propia clase Saludo.
1 public class Saludo extends Thread { 2 //clase que extiende a Thread 3 public void run() { 4 // redefínese o método run() co código asociado ó fío 5 System.out.println("Saúdo desde un fío extendendo thread!"); 6 } 7 public static void main(String args[]) { 8 Saludo hilo1=new Saludo(); 9 //créase un obxecto Thread, o fío hilo1 10 hilo1.start(); 11 //invoca a start() e pon en marcha o fío hilo1 12 } 13 }
Exemplo de fío creado mediante a interfaz Runnable
Para definir e crear fíos implementando a interfaz Runnable seguiremos os seguintes pasos:
- Declarar una nueva clase que implemente a Runnable.
- Redefinir (o sombrear) en la nueva clase el método run() con el código asociado al hilo. Lo que queremos que haga el hilo.
- Crear un objeto de la nueva clase.
- Crear un objeto de la clase thread pasando como argumento al constructor, el objeto cuya clase tiene el método run(). Este será realmente el hilo.
Unha vez creado o fío para poñelo en marcha ou inicialo:
- Invocar o método start() do obxecto thread (o fío que creamos)
O exemplo amosa cómo crear un fío implementado Runnable. O fío que se crea (obxecto thread hilo1) imprime unha mensaxe de saúdo coma no caso anterior.
1 public class Saludo implements Runnable { 2 //clase que implementa a Runnable 3 public void run() { 4 //redefínese o método run() co código asociado ó fío 5 System.out.println("Saúdo desde un fío creado con Runnable!"); 6 } 7 public static void main(String args[]) { 8 Saludo miRunnable=new Saludo(); 9 //créase obxecto Saludo 10 Thread hilo1= new Thread(miRunnable); 11 //créase un obxecto Thread (o fío hilo1) pasando como argumento ó constructor un obxecto Saludo 12 hilo1.start(); 13 //invócase ó metodo start() do fío hilo1 14 } 15 }
Exemplo de programa que lanza varios fíos
Para executar un programa de Java, iníciase a máquina virtual de Java (JVM) cun único fío que executa o método main() dunha clase. Pero desde este fío podense crear máis fíos, a partir de calquera clase que implemente a interfaz Runnable. Nesta clase hai que programar o método run(), que se executa cando se lanza o fío.
Un fío xestionase con un obxecto da clase Thread. Despois de crealo podese lanzar co seu método start() . Con esto crease o fío e se executa o método start() .
O seguiente programa de exemplo lanza dous fios dun proceso. A funcionalidad dos fíos implementase nunha clase Fío, que implementa a interfaz Runnable. O lanzar o fío execútase o método run() , que escribe un identificador que se pasa no constructor.
1 package lanzahilos; 2 class Fio implements Runnable { 3 private final String nome; 4 Fio(String nome) { 5 this.nome = nome; 6 } 7 @Override 8 public void run() { 9 System.out.printf(“Ola, son o fío: %s.\n”, this.nome); 10 System.out.printf(“Fío %s terminado.\n”, this.nome); 11 } 12 } 13 public class LanzaFios { 14 public static void main(String[] args) { 15 Thread h1 = new Thread(new Fio(“H1”)); 16 Thread h2 = new Thread(new Fio(“H2”)); 17 h1.start(); 18 h2.start(); 19 System.out.println(“Fio principal terminado.”); 20 } 21 }
Se se executa o programa varias veces, pódese ver que en cada execución as instruccións dos tres fíos (o principal e os dous creados por él) intercalan a súa execución de diversas maneiras, e empiezan e rematan a súa execución en distintos órdenes (non determinista).
A clase Thread
A clase Thread proporciona a funcionalidad esencial para a creación, execución e xestión de fíos.
O cadro resume os principais métodos desta clase.
| Método | Funcionalidade |
|---|---|
| void run() | Execútase cando se lanza o fío. É o punto de entrada do fío, coma o método main() é o punto de entrada do proceso. |
| void start() | Lanza o fío. A JVM crea o fío e executa o seu método run() |
| static void sleep(long ms) static void sleep(long ms, long ns) |
Detén a execución do fío actualmente en execución durante un tempo, que se pode indicar en microsegundos ou nunha combinación de microsegundos e nanosegundos. |
| void join() void join(long ms) void join(long ms, long ns) |
Agarda a que termine o fío. Pódese indicar un tempo máximo de espera, ben en milisegundos, bien nunha combinación de milisegundos e nanosegundos. |
| public void interrupt() public boolean isInterrupted() public static boolean interrupted() |
O primero interrumpe a execución dun fío; o segundo verifica se foi interrumpido un fío; o terceiro (estático) verifica se foi interrumpida a execución do fío actual, e borra o estado de interrupción, de xeito que unha chamada posterior devolvería false , a menos que se volva a interrumpir. |
| boolean isAlive() | Comproba se o fío está vivo. Un fío está vivo cando foi iniciado e non rematou a súa execución. |
| int getPriority() void setPriority(int nuevaPrior) |
Podese asignar unha prioridade a un fío, e se pode obtener a prioridade dun fío. |
| static Thread currentThread() | Devolve un obxecto de clase Thread correspondente ó fío en execución actualmente. |
| long getId() | Devolve o identificador do fío |
| String getName() void setName(String nombre) |
Pódese asignar un nome a un fillo, e se pode recuperar o nome do fío. |
| Thread.State getState() | Devolve o estado do fío. |
| boolean isDaemon() void setDaemon(boolean on) |
Un fío pode ser de tipo daemon. A distinción é importante porque a JVM termina a súa ejecución cando non queda ningún fío activo ou cando só quedan fíos de tipo daemon. |
Exemplo de programa que lanza dous fíos e fai pausas con sleep
O seguinte programa lanza dous fíos. Cada un deles fai pausas de duración aleatoria de ente 10 e 500 ms, utilizando o método sleep da clase Thread.
O fio principal utiliza o método join para esperar a que terminen os dous fillos lanzados, polo que sempre terminará o último. Os dous métodos anteriores pausan a execución do fío, e durante ese periodo de tempo se podería interrumpir. Se esto sucede, lanzaríase unha InterruptedException, que se captura para amosar unha mensaxe.
1 package lanzahilosyesperaqueterminen; 2 import java.util.Random; 3 class Hilo implements Runnable { 4 private final String nombre; 5 Hilo(String nombre) { 6 this.nombre = nombre; 7 } 8 @Override 9 public void run() { 10 System.out.printf(“Hola, soy el hilo: %s.\n”, this.nombre); 11 Random r = new Random(); 12 for (int i = 0; i < 5; i++) { 13 int pausa = 10 + r.nextInt(500 - 10); 14 System.out.printf(“Hilo: %s hace pausa de %d ms.\n”,this.nombre, pausa); 15 try { 16 Thread.sleep(pausa); 17 } catch (InterruptedException e) { 18 System.out.printf(“Hilo %s interrumpido.\n”, this.nombre); 19 } 20 } 21 System.out.printf(“Hilo %s terminado.\n”, this.nombre); 22 } 23 }
1 public class LanzaHilosYEsperaQueTerminen { 2 public static void main(String[] args) { 3 Thread h1 = new Thread(new Hilo(“H1”)); 4 Thread h2 = new Thread(new Hilo(“H2”)); 5 h1.start(); 6 h2.start(); 7 try { 8 h1.join(); 9 h2.join(); 10 } catch (InterruptedException e) { 11 System.out.println(“Hilo principal interrumpido.”); 12 } 13 System.out.println(“Hilo principal terminado.”); 14 } 15 }
Estado dun fío
O método getState() da clase thread permite obter en calquer momento o estado no que se atopa un fío. Devuelve:
- NEW
- RUNNABLE
- NO RUNNABLE
- TERMINATED
Iniciar un fío
Deter temporalmente un fío
Finalizar un fío
Dormir un fío con sleep
Xestión e planificación de fíos
Prioridade de fíos
Fíos egoistas e programación expulsora
Sincronización e comunicación de fíos
Un programa pode lanzar múltiples fíos que colaboren entre sí para a realización dunha tarefa. Deben utilizarse mecanismos de sincronización para evitar problemas que se podan dar...
Información compartida entre fíos
Monitores. Métodos synchronized
Monitores. Segmentos de código synchronized
Comunicación entre hilos con métodos de java.lang.Object
O problema do interbloqueo (deadlock)
A clase Semaphore
A clase Exchanger
A clase CountDownLatch
A clase CyclicBarrier
Outras utilidades de concurrencia de java.util.concurrent
A interfaz Executor e os pools de fíos
Xestión de excepcións
Para xestionar as excepcións dunha aplicación multifío podes utilizar o método uncaughtExceptionHandler() da clase thread, que permite definir un manexador de excepcións.
Para crear un manejador de excepcións:
- Crear unha clase que implemente a interfaz thread.UncaughtExceptionHandler
- Implementar o método uncaughtException()
Por exemplo podemos crear un manexador de excepcións que utilizarán todos os fíos dunha mesma aplicación así:
- O manexador só amosará qué fío producíu a excepción e a pila de chamadas da excepción
1 public class ManejadorExcepciones implements Thread. 2 //manexador de excepcións para toda a aplicación 3 UncaughtExceptionHandler{ 4 //implementa o método uncaughtException() 5 public void uncaughtException(Thread t, Throwable e){ 6 System.out.printf("Thread que lanzou a excepción: %s \n", t.getName()); 7 //amosa en consola o fío que produce a exceción 8 e.printStackTrace(); 9 //amosa en consola a pila de chamadas 10 } 11 }
O proxecto completo:
- crearáse o anterior manexador de excepcións
- implementarase un fío que divide o número 100 por un número aleatorio comprendido entre 0 e 4, dando así a posibilidade de dividir por 0.
- créanse e inícianse cinco fíos que farán uso do manexador
Depuración (debugging) e documentación de aplicacións multifío
Podemos realizar seguementos da pila de Java tanto estáticos como dinámicos utilizando os seguintes métodos da clase thread:
- dumpStack(). Amosa unha traza da pila do fío (thread) en curso
- getAllStackTraces(). Amosa un Map de todos os fíos vivos na aplicación(Map é a interfaz hacia obxectos StackTraceElement que contén o nome do ficheiro, o número de liña e o nome da clase e o método da liña de código que se está executando)
- getStackTrace(). Devuelve o seguemento da pila dun fío nunha aplicación
Tanto getAllStackTraces() coma getStackTrace() permiten grabar os datos do seguemento de pila nun log.
Podes atopar un exemplo de cómo depurar aplicacións multifío desde o IDE de NetBeans e descargar ademáis as aplicacións de exemplo para practicar.
Para documentar as nosas aplicación de Java utilizaremos o xenerador JavaDoc, que de forma automática xenera a documentación da aplicación a partir do código fuente. Para isto este sistema consiste en incluir comentarios no código utilizando as etiquetas /** y */, que despois se procesarán para xenerar un conxunto de páxinas navegables HTML.
POdes atopar información máis detallada do uso de Javadoc nos recursos técnicos da web oficial de Oracle ou en moitos titoriales na web.
Resumo
- Os fios en Java represéntanse mediante obxectos da clase Thread.
- Podense crear fíos a partir de obxectos de calquera clase que implemente a interfaz Runnable. A propia clase Thread implementa esta interfaz.
- O proceso que realiza un fío impleméntase no método run . Iníciase a execución dun fío co método start, que a súa vez executa o método run.
- O método join suspende a execución do fío actual á espera de que termine a execución do fío para o que se invoca.
- A palabra clave synchronized fai que un fío so poida executar un bloque de código tras obter o bloqueo dun obxecto de bloqueo, que se libera unha vez executado o bloque.
- Se o obxecto de bloqueo está bloqueado, suspéndese a execución do fío á espera de conseguir o bloqueo do obxecto de bloqueo. Deste xeito conseguese que o bloque de código se execute en exclusión mutua con outros fíos.
- Pódese aplicar:
- para métodos non estáticos (nese caso, o obxecto de bloqueo é o obxecto para o que se executa o método, this )
- para métodos estáticos (nese caso, o obxecto de bloqueo é a clase a que pertence ó método)
- para bloques de código cualesquiera (nese caso, especifícase explícitamente o obxecto de bloqueo).
- O interbloqueo ou deadlock prodúce cuando dous ou máis fíos manteñen bloqueados un conxunto de obxectos de bloqueo, e a súa execución está suspendida á espera de conseguir bloquear cada un un novo obxecto de bloqueo, que non poden bloquear porque xa están bloqueados por outros fíos.
- Podese evitar o interbloqueo establecendo unha orde para os obxectos de bloqueo, e todos os hilos se bloquean segundo este orde.
- Si, una vez conseguido el bloqueo sobre un objeto de bloqueo obj en un bloque de código synchronized(obj) , un hilo comprueba que no se dan las condiciones necesarias para continuar con su ejecución, puede ejecutar wait() sobre el objeto de bloqueo con obj.wait() . Con ello, su ejecución queda suspendida, y en espera no activa, mientras que otro objeto no ejecute obj.notifyAll() u obj.notify() . notifyAll() reanuda todos los procesos a la espera en el objeto de bloqueo, mientras que notify() reanuda solo uno de ellos.
- Podense incluir nunha clase os mecanismos de sincronización apropiados para permitir o uso dos seus obxectos por distintos fíos de xeito concurrente. Desa maneira, non é preciso implementar neses fíos ningún mecanismo de sincronización para controlar o acceso concurrente ós objetos da clase. Se dice entonces que la clase es thread-safe o segura para su uso concurrente por parte de varios hilos. O paquete java.util.concurrent inclúe una variedad de clases thread-safe.
- En xeral, deben programarse os fios de xeito que se permita a súa interrupción por parte doutros fíos. Para iso deben xestionar de maneira apropiada a excepción InterruptedException, comprobar cada certo tiempo se foron interrumpidos con isInterrupted() ou interrupted() , e realizar as accións oportunas no seu caso.
- Os entornos de desarrollo adoitan permitir a depuración de aplicacións multifío e proporcionar ferramentas para a detección de interbloqueos.
- o paquete java.util.concurrent proporciona mecanismos de alto nivel para concurrencia.
Créditos e referencias
- Programación de servicios y procesos. Carlos Alberto Cortijo Bon ISBN: 9788413571041
- Programación multifío
- Introducción a la Programación Multihilo
- Creación de Hilos y Multihilos con Ejemplos
- Multitaréa e Hilos, fácil y muchas ventajas
- Multitarea e Hilos en Java con ejemplos
- Multithreading con ejemplos