Programación de fíos con Java

De MediaWiki
Ir a la navegación Ir a la búsqueda

Sumario

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.

A nivel pedagóxico, estes son os resultados de aprendizaxe e criterios de avaliación:

  • RA2. Desenvolve aplicacións compostas por varios fíos de execución, con análise e aplicación de librarías específicas da linguaxe de programación.
  • CA2.1. Identificáronse situacións en que resulte útil o uso de varios fíos nun programa.
  • CA2.2. Recoñecéronse os mecanismos para crear, iniciar e finalizar fíos.
  • CA2.3. Programáronse aplicacións que implementen varios fíos.
  • CA2.4. Identificáronse os posibles estados de execución dun fío e programáronse aplicacións que os xestionen.
  • CA2.5. Utilizáronse mecanismos para compartir información entre varios fíos dun mesmo proceso.
  • CA2.6. Desenvolvéronse programas formados por varios fíos sincronizados mediante técnicas específicas.
  • CA2.7. Estableceuse e controlouse a prioridade de cada fío de execución.
  • CA2.8. Depuráronse e documentáronse os programas desenvolvidos.

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 multifío 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.

Nota. Recordade que o paquete java.lang importase automáticamente; non é precioso importalo con import

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
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:

  1. Extendendo a clase thread.
  2. 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 da 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 xeral
    • 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 "Ola 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 oMeuFio = 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 '" + oMeuFio.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 fio1=new Saludo();
 9         //créase un obxecto Thread, o fío fio1
10         fio1.start();
11         //invoca a start() e pon en marcha o fío fio1
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 unha nova clase que implemente a Runnable
  • Redefinir (ou sombrear) na nova clase o método run() con código asociado ó fío (o que queremos que faga o fío)
  • Crear un obxecto da nova clase.
  • Crear un obxecto da clase thread pasando como argumento ó constructor o obxecto que ten o método run(). Este 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 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 fio1= new Thread(miRunnable);
11        //créase un obxecto Thread (o fío hilo1) pasando como argumento ó constructor un obxecto Saludo
12         fio1.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 
 5     Fio(String nome) {
 6         this.nome = nome;
 7     }
 8 
 9     @Override
10     public void run() {
11         System.out.printf("Ola, son o fío: %s.\n", this.nome);
12         System.out.printf("Fío %s terminado.\n", this.nome);
13     }
14 }
15 
16 public class LanzaFios {
17     public static void main(String[] args) {
18         Thread h1 = new Thread(new Fio("H1"));
19         Thread h2 = new Thread(new Fio("H2"));
20         h1.start();
21         h2.start();
22         System.out.println("Fio principal terminado.");
23     }
24 }

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 fío, 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 execució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 import java.util.Random;
 2 
 3 class Fio implements Runnable {
 4 
 5     private final String nome;
 6 
 7     Fio(String nome) {
 8         this.nome = nome;
 9     }
10 
11     @Override
12     public void run() {
13         System.out.printf("Ola, son o fio: %s.\n", this.nome);
14         Random r = new Random();
15 
16         for (int i = 0; i < 5; i++) {
17             int pausa = 10 + r.nextInt(500 - 10);
18             System.out.printf("Fio: %s fai pausa de %d ms.\n", this.nome, pausa);
19             try {
20                 Thread.sleep(pausa);
21             } catch (InterruptedException e) {
22                 System.out.printf("Fio %s interrumpido.\n", this.nome);
23             }
24         }
25         System.out.printf("Fio %s terminado.\n", this.nome);
26     }
27 }
 1 public class LanzaFiosYEsperaQueTerminen {
 2 
 3     public static void main(String[] args) {
 4         Thread h1 = new Thread(new Fio("H1"));
 5         Thread h2 = new Thread(new Fio("H2"));
 6         h1.start();
 7         h2.start();
 8         try {
 9             h1.join();
10             h2.join();
11         } catch (InterruptedException e) {
12             System.out.println("Fio principal interrumpido.");
13         }
14         System.out.println("Fio principal terminado.");
15     }
16 }

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. Devolve:

  • NEW
  • RUNNABLE
  • NO RUNNABLE
  • TERMINATED

Iniciar un fío

Cando se crea un novo fío ou thread mediante o método new(), non implica que o fío xa se poida executar.

Para iso debe estar no estado "Executable", e para conseguir ese estado é preciso iniciar ou arrancar o fío mediante o método start() da clase thread().

Nos exemplos anteriore tiñamos no código fio1.start(); que precisamente se encargaba de iniciar o fío representado polo obxecto thread hilo1.

O método start() realiza as seguintes tarefas:

  • Crea os recursos do sistema necesarios para executar o fío
  • Encárgase de chamar o seu método run() e o executa como un subproceso novo e independente

Cando se invoca a start() adoita dicirse que o fío está "correndo" (running), pero recorda que esto non significa que o fío esté executándose en todo momento, xa que un fío "Executable" pode estar "Preparado" ou "Executándose" segundo teña ou non asignado tempo de procesamento.

Algunas consideracións importantes:

  • Podes invocar directamente ó método run(), por exemplo poñer hilo1.run(); e se executará o código asociado a run() dentro do fío actual (como calquier outro método), pero non comenzará un novo fío como subproceso independente
  • Unha vez que se chama ó método start() dun fío, non podes volver a realizar outra chamada o mesmo método. Se o fas obterás unha excepción IllegalThreadStateException.
  • A orde na que inicies os fíos mediante start() non inflúe na orde de execución dos mesmos: a orde de execución dos fíos é non-determinísta (non se coñece aa secuencia na que serán executadas as instruccións do programa).

Exemplo de tres fíos que imprimen unha palabra 5 veces

O programa define tres fíos, construidos cada un deles polos procedementos xa vistos. Cada fío imprime unha palabra 5 veces. Observa que: * se executas varias veces o programa, a orde de execución dos fíos non é sempre a mesma

  • non influye en absoluto a orde en que se inician con start() (a orde de execución dos fíos é no-determinístico).
Nota. 
Pede que teñas que aumentar o número de iteracións (número de palabras que imprime cada fío) para apreciar as observacións indicadas anteriormente. 
 1 package ejecutarvarioshilos;
 2 
 3 /**
 4  *
 5  * @author JLV
 6  */
 7 public class Main {
 8 
 9    public static void main(String[] args) {
10         //creamos 2 fíos do tipo Hilo_Thread con 2 constructores
11         //diferentes
12         Thread hilo1 = new Hilo_Thread("Isabel");
13         Thread hilo2 = new Hilo_Thread();
14 
15         //creamos un fío Runnable nun paso
16         Thread hilo3 = new Thread(new Hilo_Runnable());
17 
18         //poñemos en marcha os 3 fíos
19         hilo1.start();
20         hilo2.start();
21         hilo3.start();
22     }
23 
24 }
 1 package ejecutarvarioshilos;
 2 
 3 /**
 4  *
 5  * @author JLV
 6  */
 7 public class Hilo_Thread extends Thread {
 8 //clase que extende a Thread con 2 constructores
 9 
10     String nombre = "Hilo_derviaThread";
11 
12     public Hilo_Thread(String nb) {
13         //constructor 1
14         nombre = nb;
15     }
16 
17     public Hilo_Thread() {
18         //constructor 2
19     }
20 
21     @Override
22     public void run() {
23         //redefinimos run() co código asociado ó fío
24         for (int i = 1; i <= 5; i++) {
25             System.out.println(nombre);
26         }
27     }
28 }
 1 package ejecutarvarioshilos;
 2 
 3 /**
 4  *
 5  * @author JLV
 6  */
 7 public class Hilo_Runnable implements Runnable {
 8     //clase que implementa Runnable
 9     public void run() {
10         //redefinimos run() co código asociado ó fío
11         for (int i = 1; i <= 5; i++) {
12             System.out.println("  Hilo_Runnable");
13         }
14     }
15 }

Deter temporalmente un fío

Estados fios.jpg

Qué significa que un fío deteuse temporalmente? Significa que o fío pasou o estado "Non Executable". E cómo puede pasar un fío a ese estado? Por algunha destas circunstancias:

  • O fío dormiuse. Invocouse ó método sleep() da clase thread indicando o tiempo que o fío permanecerá detido. Transcorrido ese tempo, o fío vólvese "Executable", en concreto pasa a "Preparado".
  • O fío está esperando. O fío detivo a súa execución mediante a chamada ó método wait(), e non se reanudará pasando a "Executable" (en concreto "Preparado") ata que se produza unha chamada ós método notify() ou notifyAll() por outros fíos (estos métodos están relacionados coa sincronización e comunicación de fíos).
  • O fío bloqueouse. O fío está pendente de que finalice unha operación de E/S nalgún dispositivo, ou á espera dalgún outro tipo de recurso; foi bloqueado polo sistema operativo. Cando finalice o bloqueo, volve ó estado "Executable", en concreto "Preparado".

Podes ver un esquema cos diferentes métodos que fan que un fío pase ó estado "Non Executable", así coma os que permiten saír dese estado e volver ó estado "Executable".

O método suspend() (actualmente en desuso ou deprecated) tamén permite deter temporalmente un fío, e nese caso se reanudaría mediante o método resume() (tamén en desuso); no debes utilizar estes métodos da clase thread xa que non son seguros e provocan moitos problemas.

Finalizar un fío

A forma natural de que morra ou finalice un fío é cando termina de executarse o seu método run(), pasando ó estado 'Morto'.

Unha vez que o fío morre non o puedes iniciar outra vez con start(). Se desexas realizar outra vez o traballo desempeñado polo fío terás que:

  • Crear un novo hilo con new().
  • Iniciar o fío con start().

Hai alguna forma de comprobar se un fío non morreu? Non exactamente, pero podes utilizar o método isAlive() da clase thread para comprobar se un fío está vivo ou non. Un fío considérase que está vivo (alive) desde a chamada o seu método start() ata a súa morte. isAlive() devolve:

  • verdadero (true) se está vivo (en estado "executable" ou "non executable")
  • falso (false) se está morto (ou recén creado)

O método stop() da clase thread (deprecated) tamén finaliza un fío. Non debes utilizarlo porque é pouco seguro.

Exemplo que lanza fío secundario e comproba estado con isAlive() e getState()

O fío principal lanza un fío secundario que realiza unha conta atrás desde 10 ata 1. Desde o fío principal:

  • verificarase a morte do fío secundario mediante a función isAlive()
  • mediante o método getState() da clase thread vamos obtendo o estado do fío secundario
  • úsase tamén o método thread.join() que espera ata que o fío morre
 1 package PaquetePrincipal;
 2 
 3 import java.util.logging.Level;
 4 import java.util.logging.Logger;
 5 
 6 /******************************************************************************
 7  *
 8  * @author JLV
 9  */
10 public class Main {
11 
12     /**
13      * @param args the command line arguments
14      */
15     public static void main(String[] args) {
16         Hilo_Auxiliar hilo1 = new Hilo_Auxiliar();
17         //Crea un novo fío. O fío está en estado Novo (new)
18 
19         System.out.println("Hilo Auxiliar Nuevo: Estado=" + hilo1.getState()
20                 + ",¿Vivo? isAlive()=" + hilo1.isAlive());
21         //Obtemos o estado do thread hilo1 e se está vivo ou non
22 
23         hilo1.start();
24         //Inicia o thread hilo1 e pasa ´´o estado Executable
25 
26         System.out.println("Hilo Auxiliar Iniciado: Estado="
27                 + hilo1.getState()
28                 + ",¿Vivo? isAlive()=" + hilo1.isAlive() + "\n");
29        
30         try {
31             hilo1.join();
32             //espera a que o thread hilo1 morra
33         } catch (InterruptedException e) {
34             System.out.println(e);
35         }
36         System.out.println("\n Hilo Auxiliar Morto: Estado="
37                 + hilo1.getState()
38                 + ",¿Vivo? isAlive()=" + hilo1.isAlive());
39     }
40 }
 1 package PaquetePrincipal;
 2 /**
 3  * @author JLV
 4  */
 5 public class Hilo_Auxiliar extends Thread{
 6 //código del hilo
 7   @Override
 8   public void run(){
 9     for(int i=10;i>=1;i--)
10       System.out.print(i+",");
11   }
12 }

Dormir un fío con sleep

O método sleep() da clase thread recibe como argumento o tiempo que desexamos durmir o fío que o invoca. Cando transcorre o tempo especificado, o fío volve a estar "Executable" ("Preparado") para continuar executándose.

Hay dúas formas de chamar a este método:

sleep(long milisegundos)
Pasa como argumento un enteroque representa milisegundos
sleep(long milisegundos, int nanosegundos)
Agrega un segundo argumento entero (entre 1 e 999999), que representa un tempo extra en nanosegundos que se sumará o primeiro argumento

Calquer chamada a sleep() pode provocar unha excepción; o compilador obriganos a controlala mediante un bloque try-catch.

Exemplo de fío durmido para amosar marcador gráfico

Ilústrase a necesidade de durmir un fío na aplicación para que poida amosar como avanza un marcador gráfico desde 0 ata 20. Podrás comprobar que se non utilizamos un fío auxiliar e o durmimos, non poderemos apreciar como se vai incrementando o marcador.

PENDENTE

Xestión e planificación de fíos

O planificador de fíos de Java (Sheduler) utiliza un algoritmo de secuenciación de fíos denominado fixed priority scheduling que está baseado nun sistema de prioridades relativas; de xeito que o algoritmo secuencia a execución de fíos en base á prioridade de cada un deles.

O funcionamiento do algoritmo é o seguiente:

  • O fío elexido para executarse sempre é o fío "Executable" de prioridade máis alta
  • Se hai más dun fío coa mesma prioridade, a orde de execución manéxase mediante un algoritmo por turnos (round-rubin) baseado nunha cola circular FIFO (Primero en entrar, primero en saír)
  • Cando o fío que está "executándose" pasa ó estado de "Non Executable" ou "Morto", selecciónase outro fío para a súa execución
  • A execución dun fío interrúmpese se outro fío con prioridade máis alta se volve "Executable".

O feito de que un fío cunha prioridade máis alta interrumpa a outro denomínase "planificación apropiativa" (preemptive sheudling).

Mais hai que salientar que a responsabilidade de execución dos fíos é do Sistemas Operativos sobre o que corre a JVM,

Prioridade de fíos

Cada fío ten unha prioridad representada por un valor de tipo entero entre 1 e 10. A maior valor maior é a prioridade do dío.

Por defecto, o fío principal (o que executa o seu método main()) é creado con prioridade 5.

O resto de fíos secundarios (creados desde o fío principal, ou desde calquier outro fío en funcionamento), herdan a prioridad que teña nese intre o seu fío pai.

Na clase thread defínense 3 constantes para manexar estas prioridades:

  • MAX_PRIORITY (= 10). Ë o valor que simboliza a máxima prioridade.
  • MIN_PRIORITY (=1). É o valor que simboliza a mínima prioridade.
  • NORM_PRIORITY (= 5). É o valor que simboliza a prioridade normal (a que ten por defecto o fío onde correo método main() ).

Pódese obter e modificar a prioridade dun fío mediante os métodos da clase thread:

  • getPriority(). Obten a prioridade dun fío.
  • setPriority(). Modifica a prioridade dun fío. Este método toma como argumento un entero entre 1 e 10, que indica a nova prioridade.

Java ten 10 niveles de prioridade que non teñen por qué coincidir cos do sistema operativo sobre o que está correndo. Porén o máis recomendable é utilizar no código só as constantes antes mencionadas.

Podemos conseguir aumentar o rendemento dunha aplicación multifío xestionando axeitadamente as prioridades dos diferentes fíos; por exemplo utilizando unha prioridade alta para tarefas de tempo crítico e unha baixa para outras tarefas menos importantes.

Declárase un fío que enche un vector con 20000 caracteres. Inícianse 15 fíos con prioridades diferentes:

  • 5 con prioridade máxima
  • 5 con prioridade normal
  • 5 con prioridade mínima

Ó executar o programa:

  • comprobarás que os fíos con prioridade máis alta tenden a finalizar antes.
  • observarás se usa tamén o método yield(), método que:
    • permite indicarlle ó hardware que está executando a tarefa que pode interrumpila e darle unha oportunidad a outro thread.
    • pero o procesador é libre de adherirse ou ignorar este comando (o seu comportamento e non determinista e depende da plataforma)...
    • polo que non é moi recomendable
 1 package PaquetePrincipal;
 2 
 3 /**
 4  * @author JLV
 5  */
 6 public class Programa {
 7     /**************************************************************************
 8      *
 9      * @param args the command line arguments
10      */
11     public static void main(String[] args) {
12        
13         int contador = 5;
14 
15         //vectores para fíos de distintas prioridades
16         Thread[] hiloMIN = new Thread[contador];
17         Thread[] hiloNORM = new Thread[contador];
18         Thread[] hiloMAX = new Thread[contador];
19 
20         //crea os fíos de prioridade mínima
21         for (int i = 0; i < contador; i++) {
22             hiloMIN[i] = new Hilo(Thread.MIN_PRIORITY);
23         }
24 
25         //crea os fíos de prioridade normal 
26         for (int i = 0; i < contador; i++) {
27             hiloNORM[i] = new Hilo();
28         }
29 
30         //crea os fíos de máxima prioridade
31         for (int i = 0; i < contador; i++) {
32             hiloMAX[i] = new Hilo(Thread.MAX_PRIORITY);
33         }
34 
35         System.out.println("Fíos en proceso, espera......\nOs de maior prioridade tenden a terminar antes...\n");
36 
37         //inicia os fíos
38         for (int i = 0; i < contador; i++) {
39             hiloMIN[i].start();
40             hiloNORM[i].start();
41             hiloMAX[i].start();
42         }
43     }
44 }
 1 package PaquetePrincipal;
 2 
 3 /******************************************************************************
 4  *
 5  * @author JLV
 6  */
 7 public class Hilo extends Thread {
 8 
 9     /**************************************************************************
10      * constructor por defecto
11      */
12     public Hilo() {
13         //herda a prioridade do fío pai
14     }
15 
16     /**************************************************************************
17      * constructor personalizado
18      */
19     public Hilo(int prioridad) {
20 
21         //establece a prioridade indicada
22         this.setPriority(prioridad);
23     }
24 
25     /**************************************************************************
26      * executa unha tarefa pesada
27      */
28     @Override
29     public void run() {
30 
31         //cadea
32         String strCadena = "";
33 
34         //agrega 30000 caracteres a unha cadea vacía
35         for (int i = 0; i < 20000; ++i) {
36             //imprime o valor na saída
37             strCadena += "A";
38             yield(); // suxire ó planificador Java que pode seleccionar outro fío
39         }
40 
41         System.out.println("Fío de prioridade " + this.getPriority() + " termina aora");
42     }
43 }

Fíos egoistas e programación expulsora

Un hilo denomínase egoísta (selfish thread) cando entra en execución e non cede voluntariamente o control para que poidan executarse outros fios. Para evitar esa situación non desexable, un fío debería executar os métodos durmir (sleep) ou ceder (yield) cando executa un buble largo, para asegurar que non monopoliza ó sistema.

Algúns sistemas operativos (coma Windows) combaten estas actitudes cunha estratexia de planificación por división de tempos (time-slicing), que opera con fios de igual prioridade que compiten pola CPU. Nestas condicións o sistema operativo asigna tempos a cada fío e vai cedendo o control consecutivamente a todos os que compiten polo control da CPU, impedindo que un deles se apropie do sistema durante un intervalo de teempo prolongado. Este mecanismo é proporcionado polo sistema operativo, non por Java (que posibilita solución con sleep e yield).

Ollo cos fíos egoistas!

Esa solución da linguaxe coñécese como programación expulsora. Por exemplo a través do método yield() da clase java.lang.thread faise que un fío que está executándose pase a preparado para permitir que outros fíos poidan executarse. Pero é preciso ter en conta que:

  • o funcionamiento de yield() non está garantizado (e pode que despois de que pase a preparado, éste volva a ser elexido para executarse)
  • non se pode asumir que a execución dunha aplicación realizarase nun sistema operativo que implementa time-slicing
  • na aplicación debes incluir axeitadamente chamadas ó método yield(), (ou a sleep() e wait()) se o fío non se bloquea por unha entrada/saída

Podes ver un exemplo dun fío egoista; e como invocar a yield() dentro do método run() dun fío.

Sincronización e comunicación de fíos

Hai ocasións nas que distintos fios dun programa necesitan establecer algunha relación entre sí e compartir recursos ou información. Pode ocorrer que:

  • Dous ou máis fios compiten por obter un mesmo recurso (por exemplo queren escribir nun mesmo ficheiro ou acceder á mesma variable para modificala
  • Dous ou máis fios colaboran para obter un fin común e para iso precisan precisan comunicarse a través dalgún recurso (por exemplo un fío produce información que utilizará outro)

Nestas situacións será preciso que os fios se executen de xeito controlado e coordinada para evitar posibles interferencias (que poden levar a programas que se bloquean ou que intercambian datos de xeito equivocado).

Para conseguir que os fíos se executen dun xeito coordinado, utilízanse a sincronización e a comunicación de fios:

  • Sincronización. É a capacidade de informar da situación dun fío a outro. O obxectivo é establecer a secuencialidade correcta do programa
  • Comunicación. É a capacidade de transmitir información desde un fío a outro. O obxectivo é o intercambio de información entre fios para operar de forma coordinada.

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 poden dar.

En Java a sincronización e comunicación de fíos conséguese mediante:

Monitores
Créanse ó marcar bloques de código coa palabra synchronized.
Semáforos
Podemos implementar os nosos propios semáforos ou utilizar a clase Semaphore incluída no paquete java.util.concurrent.
Notificacións
Permiten comunicar fíos mediante os métodos da clase java.lang.Object.
wait()
notify()
notifyAll()

Java proporciona no paquete java.util.concurrent varias clases de sincronización que permiten a sincronización e comunicación entre diferentes fíos dunha aplicación multithreadring, coma son:

  • Semaphore
  • CountDownLatch
  • CyclicBarrier
  • Exchanger.

Información compartida entre fíos

As seccións críticas son aquelas seccións de código que non poden executarse concurrentemente, xa que nelas atópanse os recursos ou información que comparten os diferentes fios, e que por tanto poden ser problemáticas.

O clásico exemplo dos xardines ilustra o que pode acontecer cando varios fios actualizan unha mesma variable; pon de manifiesto o problema coñecido coma condición de carrera, que se produce cando varios fios acceden á vez a un mesmo recurso (por exemplo a unha variable) cambiando o seu valor e obteniendo desta forma un valor non esperado da mesma.

A forma de protexer as seccións críticas é mediante sincronización, que se acada mediante:

  • Exclusión mutua. Asegurar que un fio ten acceso á sección crítica de forma exclusiva e por un tiempo finito.
  • Por condición. Asegurar que un fío non progrese ata que se cumpla unha determinada condición.

A sincronización para o acceso a recursos compartidos en Java (e noutras linguaxes) basease no concepto de monitor.

Monitores

Un monitor é unha porción de código protexida por un mute ou lock.

Para crear un monitor en Java hai que marcar un bloque de código coa palabra synchronized, podendo ser ese bloque:

  • Un método completo
  • Un segmento de código

Engadir synchronized a un método fará que:

  • creemos un monitor asociado ó obxecto.
  • so un fío pode executar o método synchronized dese obxecto á vez
  • os fios que precisen acceder a ese método synchronized permanecerán bloqueados e en espera
  • cando o fío finaliza a execución do método synchronized, os fios en espera de poder executalo desbloquearanse; o planificador Java seleccionará a un deles para execución

Comunicación entre fíos con métodos de java.lang.Object

A comunicación entre fios podémola ver como un mecanismo de autosincronización, que consiste en lograr que un fío actúe só cando outro rematou certa actividade (e viceversa).

Java soporta comunicación entre fios mediante os seguientes métodos da clase java.lang.Object:

  • wait(). Deten o fío (pasa a "non executable"), e non se reanudará ata que outro fio notifique que aconteceu o esperado
  • wait(long tempo). Coma no caso anterior pero ahora o fío tamén pode reanudarse (pasar a "executable·) se concluiu o tempo indicaddo como parámetro
  • notify(). Notifica a un dos fios postos en espera para o mesmo obxecto, que xa pode continuar
  • notifyAll(). Notifica a todos os fios postos en espera para o mesmo objeto que xa poden continuar

O problema do interbloqueo (deadlock)

O problema do bloqueo mutuo, en las aplicaciones concurrentes, darase fundamentalmente cando un fío entra nun bloque synchronized, e a súa vez chama a outro bloque synchronized; ou ben ó utilizar clases de java.util.concurrent que levan implícita a exclusión mutua.

Exemplo de interbloqueo

O seguinte exemplo produce interbloqueo.

Observa que:

  • non finaliza a execución do programa
  • na barra de estado do IDE NetBeans aparece a indicación de programa bloqueado
  • haberá que finalizar manualmente o programa
 1 package PaquetePrincipal;
 2 /******************************************************************************
 3  * Programa exemplo de interbloqueo ou deadlock.
 4  * Supongamos doús fíos e que cada fío precisa privilexios exclusivos de escritura en dous archivos distintos. 
 5  * O hilo1 podería abrir o archivoA de forma exclusiva e o hilo2 facer o mesmo co archivoB.
 6  * Estando o hilo1 no archivoA necesita acceso exclusivo o archivoB e estando o hilo2 en archivoB necesita acceso exclusivo o archivoA.
 7  * Ambos fíos se obstaculizarán entre si e se bloquean indefinidamente. Se terá producido un interbloqueo.
 8  * 
 9  * @author JLV
10  */
11 class Main {
12 
13  //Se crean ficheros de ambos tipos, A y B
14   static FicheroA a = new FicheroA();
15    static FicheroB b = new FicheroB();
16 
17   public static void main(String args[]) {
18     //crea e inicia os fíos que executarán os métodos synchronized, e que provocarán o interbloqueo
19     Hilo1 hilo1 = new Hilo1(a, b);
20     Hilo2 hilo2 = new Hilo2(a, b);
21     hilo1.start();
22     hilo2.start();
23   }
24 }
 1 package PaquetePrincipal;
 2 
 3 /**
 4  *
 5  * @author JLV
 6  */
 7 public class Hilo1 extends Thread {
 8  //Declara dous obxectos di tipo de cada ficheiro
 9   FicheroA a;
10   FicheroB b;
11 
12   /*****************************************************************************
13   constructor
14    *
15    * @param a: ficheroA
16    * @param b: ficheroB
17    */
18   Hilo1(FicheroA a, FicheroB b) {
19     this.a = a;
20     this.b = b;
21   }
22 /*******************************************************************************
23  * Tarea do fío: acceder ó ficheiroA
24  */
25   @Override
26   public void run() {
27     a.metodoA(b);
28     //o fío accede ó ficheiroA
29   }
30 }
 1 package PaquetePrincipal;
 2 /**
 3  *
 4  * @author JLV
 5  */
 6 public class Hilo2 extends Thread {
 7 
 8   //Declara dous obxectos dos tipos de cada ficheiro
 9   FicheroA a;
10   FicheroB b;
11 
12   /****************************************************************************
13    * constructor
14    * 
15    * @param a: ficheroA
16    * @param b: ficheroB
17    */
18   Hilo2(FicheroA a, FicheroB b) {
19     this.a = a;
20     this.b = b;
21   }
22 
23   /******************************************************************************
24    * Tarea do fío: acceder ó ficheiroB
25    */
26   @Override
27   public void run() {
28 
29     b.metodoB(a);
30     //o fío accede ó ficheroB.
31   }
32 }
 1 package PaquetePrincipal;
 2 /**
 3  *
 4  * @author JLV
 5  */
 6 class FicheroA {
 7 
 8  /******************************************************************************
 9   *  métodoA: simula o acceso ó ficheroA con exclusión mutua. Durme ó fío e
10   * despois invoca ó método que lle permitirá intentar o acceso ó ficheroB
11   * @param b
12   */
13   public synchronized void metodoA(FicheroB b) {
14 
15     //imprime na saída o nome do fío que executa o método
16     String name = Thread.currentThread().getName();
17     System.out.println("Hilo " + name + " entra en ficheiro_A");
18 
19     try {
20       Thread.sleep(1000);
21     } catch (Exception e) {}
22 
23     System.out.println("Hilo " + name + " intentando acceder a ficheiro_B");
24     b.metodoB(this);
25   }
26 
27   public synchronized void syn2() {
28 
29     System.out.println("Dentro de A.syn2");
30   }
31 }
 1 package PaquetePrincipal;
 2 /**
 3  *
 4  * @author JLV
 5  */
 6 class FicheroB  {
 7 
 8   /****************************************************************************
 9    métodoB: simula o acceso ó ficheiroB con exclusión mutua. Durme o fío e
10    * despois invoca ó método que lle permitirá intentar o acceso ó ficheiroA
11    *
12    * @param a 
13    */
14 
15   public synchronized void metodoB(FicheroA a) {
16 
17     //imprime na salida o nome do fío que executa o método
18     String name = Thread.currentThread().getName();
19     System.out.println("Hilo " + name + " entra en ficheiro_B");
20 
21     try {
22       //o durme durante 1 segundo
23       Thread.sleep(1000);
24     } catch (Exception e) {}
25     
26     //imprime na saída o intento de chamada ó método metodoA() do obxecto a
27     System.out.println("Hilo " + name + " intentando entrar en ficheiro_A");
28     a.metodoA(this);
29   }
30 }

A clase Semaphore

Os semáforos controlan o acceso a recursos compartidos

A clase Semaphore do paquete java.util.concurrent permite definir un semáforo para controlar o acceso a un recurso compartido.

Para crear e usar un objeto Semaphore faremos:

  • Indicarlle ó constructor Semaphore (int permisos) o total de permisos que se poden dar para acceder ó mesmo tiempo ó recurso compartido (isto é o número de fios que poden acceder simultaneamente ó recurso)
  • Indicarlle ó semáforo mediante o método acquire() que queremos acceder al recurso; ou ben mediante acquire(int permisosAdquirir) cantos permisos se queren consumir o mesmo tempo
  • Indicarlle ó semáforo mediante el método release() que libere o permiso; ou ben mediante release(int permisosLiberar) cantos permisos se queren liberar ó mesmo tempo
  • Hai outro constructor Semaphore (int permisos, boolean justo) que mediante o parámetro justo permite garantir que o primeiro fio en invocar adquire() será o primeiro en adquirir un permiso cando sexa liberado (garantindo así a orde de adquisición de permisos segundo a orde na que se solicitan)

Dependendo do uso de Semaphore estes metodos se invocarán nun punto ou outro:

  • Se se usa para proteger secciones críticas: a chamada aos métodos acquire() e release() farase desde o recurso compartido ou sección crítica, e o número de permisos pasado ó constructor será 1.
  • Se se usa para comunicar fios, neste caso un fío invocará o método acquire() e outro fío invocará o método release() para así traballar de xeito coordinado. O número de permisos pasado ó constructor coincidirá co número máximo de fiosh bloqueados na cola ou lista de espera para adquirir un permiso

A clase Exchanger

A clase Exchanger do paquete java.util.concurrent, establece un punto de sincronización onde se intercambian obxectos entre dous fios.

A clase Exchanger<V> é xenérica (significa que tenrás que especificar en <V> o tipo de obxecto a compartir entre os fios.

A clase CountDownLatch

Permite facer o mesmo que poderíamos facer cos métodos wait e notify dunha forma máis sinxela e con menos código.

A clase CyclicBarrier

Para arrancar varios fíos á vez ou para agardar a que todos eles terminen.

Esta clase instánciase pasándolle no constructor cantos fíos debe sincronizar. Os fíos deben chamar ó método await() de CyclicBarrier e se quedarán ahí detidos. CyclicBarrier liberaraos cando teña tantos fíos á espera como se lle indicara no constructor.

Outras utilidades de concurrencia de java.util.concurrent

A interfaz Executor e os pools de fíos

As aplicacións tipo servidor teñen que atender multiples peticións concurrentes de usuario, que son tarefas que deben ser procesadas o antes posible: o uso de fíos é moi convinte para estas aplicaciones. Con todo se executamos cada tarefa nun fío distinto, podense chegar a crear tantos fíos que o incremento de recursos utilizados pode comprometer a estabilidade do sistema. A solución a este problema son os pools de fios (thread pools).

Isto é basicamente un contedor dentro do cal se crean e se inician un número limitado de fíos, para executar todas as tarefas dunha lista.

O más habitual para declarar un pool é facelo coma un obxecto do tipo ExecutorService utilizando algún dos seguientes métodos da clase estática Executors:

  • newFixedThreadPool(int numeroFios): crea un pool co número de fíos indicado. Ditos fíos son reutilizados cíclicamente ata rematar coas tarefas da cola ou lista
  • newCachedThreadPool(): crea un pool que vai creando fíos conforme se van precisando pero que pode reutilizar os xa concluidos para non ter que crear demasiados. Os fíos que levan moito tempo inactivos son terminados automáticamente polo pool
  • newSingleThreadExecutor(): crea un pool dun só fío. A vantaxe que ofrece este esquema é que se ocorre unha excepción durante a execución dunha tarea, non se deten a execución das seguintes
  • newScheduledExecutor(): crea un pool que vai a executar tarefas programadas cada certo tiempo, xa sexa unha sola vez ou de xeito repetitivo. É parecido a un obxecto Timer, pero coa diferenza de que pode ter varios threads que irán realizando as tarefas programadas conforme se desocupen.

Os obxectos de tipo ExecutorService implementan a interfaz Executor. Esta interfaz define o método execute(Runnable), ó que hai que chamar unha vez por cada tarefa que deba ser executada polo pool (a tarefa pásase como argumento do método).

A interface ExecutorService proporciona unha serie de métodos para o control da execución das tarefas; entre eles o método shutdown(), para indicarlle ó pool que os fíos non se van a reutilizar para novas tarefas e deben morrer cando finalicen o seu traballo.

Poder ver un exemplo dunha clase que implementa Runnable que xenera e imprime 10 números aleatorios; crearase un pool de dous fíos capaz de realizar trinta desas tarefas.

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 manexador 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

Seguemento da pila

Copiar información da pila desde o IDE netbeans en modo debug

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.

Outra opción e copiar a información da pila no IDE. Por exemplo, en netbeans en modo debug co botón dereito do rato podemos escoller Copy Stack.

Podes atopar máis información acerca do seguemento da pila en titoriais en inglés e en castelán

Depuración no IDE

Coa programación de fíos, pode ser de gran axuda executar en modo debugging ou depuración do código. Con este modo, nos sitios onde indiquemos un breakpoint ou punto de control, deterase a execución, para que podamos revisar que está acontecendo.

Cabe continuar a execución, ou ir executando mediante steps (pasos), de xeito que vamos vendo liña a liña de código como se está executando o noso programa.

Cada IDE ten as súas peculiaridades sobre como executar o programa co depurador. Por exemplo, en Netbeans: Debuggeando deadlock.png

E por exemplo, pódense fixar breakpoits pulsando co botón dereito na liña de código no que o queremos insertar: Debuggeando deadlock breakpoint.png

Unha vez detida a execución nun breakpoint, pódense ver todos os fíos representados por unha icona de roda dentada. na que:

  • os de cor verde representan fíos en execución
  • os de cor vermella representan fíos parados no breakpoint

O fío no que se parou a execución no breakpoint está suspendido, e aparece cunha cor distinta a esquerda e con fondo verde. Este é o fío actual e pódese executar paso a paso (step) como diciamos.

Pódese cambiar a outro fío suspendido para executalo paso a paso pulsando sobre él co botón dereito e escollendo Make current.

Detección de interbloqueos

No IDE

Na execución do código, podemos chegar a unha situación de interbloqueo. Coas opcións de deperuración de IDE, cando lle damos a Pause na execución, o IDE pode axudar indicando que se producíu una situación de interbloqueo:

Debuggeando deadlock detected.png

Con JConsole

JConsole (Java™ Monitoring and Management Console) é unha ferramenta gráfica que permite ó usuario supervisar e xestionar o comportamento das aplicacións Java.

JConsole conecta con aplicacións que se executan no mesmo equipo ou en equipo remoto. Cando se conecta a unha aplicación Java, amosa información sobre a aplicación. Entre outros:

  • uso da memoria
  • fíos en execución
  • clases cargadas
  • ...

Estes datos permítenlle supervisar o comportamento da aplicación e da JVM. A información é útil á hora de entender os problemas de rendimento, os de uso da memoria, e no caso da programación de fíos... os interbloqueos.

Para arrincar JConsele, a súa ubicación é:

%JAVA_HOME%/bin/jconsole.exe

Na listaxe de procesos, teremos que indicar co que queremos conectar:

JConsole con deadlock.PNG

Unha vez conectado, podemos ver a información que comentabamos. Para o noso interese, a lapela threads facilitaranos información útil:

JConsole con deadlock 2.PNG

Se pulsamos no botón Detect Deadlock, amosaranos a información relevante en caso de existir un:

JConsole con deadlock 3.PNG

Exemplos

De depuración

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.

De documentación

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 /** e */, 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
      • Extendendo (herdanza) a clase thread (salvo que esa clase xa herde doutra
      • 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 calesquera (nese caso, especifícase explícitamente o obxecto de bloqueo).
  • O interbloqueo ou deadlock prodúcese cando 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.
  • Se unha vez conseguido o bloqueo sobre un obxecto de bloqueo obj nun bloque de código synchronized(obj) , un fío comproba que non se dan as condicións necesarias para continuar coa súa execución, pode executar wait() sobre o obxecto de bloqueo con obj.wait().
    • Con iso a súa execución queda suspendida, e en espera non activa, mientres que outro obxecto non execute obj.notifyAll() u obj.notify().
      • notifyAll() reanuda todos os procesos á espera no obxecto de bloqueo
      • notify() reanuda só un deles
  • 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.
  • As contornas de desenvolvemento (IDE) 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

Thread dump

JConsole

Problema de productor/consumidor

Problema de los filosofos

A clase Semaphore

A clase Exchanger

A clase CyclicBarrier

A clase CountDownLatch

A interfaz Executor e os pools de fíos

Outras referencias

Outros exemplos