Tema 12 - Hilos en Java
Tema 12 - Hilos en Java
Tema 12 - Hilos en Java
Hilos en Java
Procesos e Hilos
En la programación concurrente, hay dos unidades básicas de ejecución: procesos
e hilos. En el lenguaje de programación Java, la programación concurrente está
más relacionada con los hilos. Sin embargo, los procesos también son
importantes.
Los hilos existen dentro de los procesos – cada proceso tiene al menos un hilo. Un
proceso con un solo hilo tiene las siguientes propiedades:
Figura 12.1
En un sistema multitarea que soporta multihilos, los hilos comparten los recursos
de un proceso, incluyendo la memoria y los archivos abiertos. Esto hace la
comunicación eficiente pero potencialmente problemática. En este caso cada
proceso puede tener uno o más hilos. Los diferentes procesos y sus hilo pueden
ejecutarse simultáneamente en forma real o aparente, dependiendo del número de
procesadores o núcleos.
Figura 12.2
Prueba.java
public class Prueba {
public static void main(String[] args) {
Hilo hilo = new Hilo();
hilo.run();
}
}
Figura 12.3
Para hacer que el método run() ejecute en paralelo con el método main() y
otros métodos de la clase Prueba, hay que modificar la clase Hilo para que sea
ejecutado por un nuevo hilo. Para ello nuestra clase Hilo deberá heredar de la
clase Thread. Como se muestra en el siguiente código:
Hilo.java
public class Hilo extends Thread {
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(I + ": Hola");
}
}
}
También debemos modificar a la clase que invoca a la clase Hilo para que en
lugar de invocar a su método run() invoque a su método start().
Prueba.java
Hilos en Java
hilo.start();
}
}
El método start() es quien crea otro hilo de ejecución. Este hilo, después de un
proceso de inicialización invoca al método run(). Este mismo hilo, cuando el
método run() completa se encarga de ños detalles de terminación del hilo. El
método start() del hilo original regresa inmediatamente, por lo que el método
run() ejecutará en el nuevo hilo al mismo tiempo que el método start()
regresa en el primer hilo como se muestra en la figura 12.4.
Figura 12.4
La clase Thread forma parte de la API de Java para hilos. Parte de esa API se
muestra en la figura 12.5.
Figura 12.4
Tabla 12.2 Métodos de la Clase Thread.
public Thread()
public Thread(Runnable target)
public Thread(Runnable target, String name)
public Thread(String name)
Crean nuevos hilos. El parámetro target denota el objeto del cual se ejecuta el método run().El
valor por omisión es null. El parámetro name establece el nombre del nuevo hilo. El valor
prestablecido es “Thread-“n, donde n es un entero consecutivo.
public void start()
Hace que este hilo inicie su ejecución; La máquina virtual de Java llama al método run() de este
hilo. Como resultado hay dos hilos ejecutándose concurrentemente. El hilo actual (que regresa de
la llamada al método start() y el otro hilo (que ejecuta su método run()).
Es ilegal iniciar un hilo más de una vez. En particular un hilo no puede reiniciarse una vez que
completa su ejecución.
Lanza:
IllegalThreadStateException – Si el hilo ya ha sido iniciado.
public void run()
Si este hilo fue construido usando un objeto Runnable diferente, entonces se invoca al método
run() del objeto. De otra forma, el método no hace nada y regresa.
Hilos en Java
HiloRunnable.java
public class HiloRunnable implements Runnable {
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(i + ": Hola desde la clase Hilo");
}
}
}
Una clase que implemente la interfaz debe implementar el método run(). Sin
embargo para crear un nuevo hilo no es suficiente con crear una instancia de la
clase que implementa la interfaz Runnable e invocar a su método start() ya
que la clase no hereda de la clase Thread y no tiene el método start(). En
lugar de ello crearemos un nuevo hilo y le pasaremos al constructor de la clase
Thread la instancia de la clase que implementa la interfaz Runnable, como se
muestra en el siguiente código:
PruebaHiloRunnable.java
public class PruebaHiloRunnable {
public static void main(String[] args) {
Runnable hiloRunnable = new Hilo();
Thread hilo = new Thread(hiloRunnable);
hilo.start();
}
}
Donde target es el objeto Runnable que le pasamos al constructor del hilo. Así
que el hilo empieza la ejecución de su método run() quien inmediatamente llama al
método run() del objeto Runnable.
Hace que el hilo, actualmente en ejecución se duerma (cese temporalmente su ejecución) por el
número de milisegundos (más el número de nanosegundos, en el caso del segundo método) dado
por el parámetro, sujeto a la precisión y exactitud de los temporalizadores y el programador
tareas. El hilo no pierde la propiedad de ninguno de sus monitores.
Checa si el hilo está vivo. Un hilo está vivo si ha sido iniciado y no ha muerto.
El siguiente código muestra el uso del método sleep() de la clase Thread para
implementar un hilo que ejecuta una tarea periódicamente. En este ejemplo, el
método suspende la ejecución del hilo por 1000 ms por lo que el ciclo despliega el
mensaje aproximadamente cada 1 s mientras se repita el ciclo.
Este código también ilustra la forma de parar un hilo. Para parar un hilo hay que
hacer que su método run() termine su ejecución y regrese. Para lograr eso en la
clase principal hacemos que la variable vivo tome el valor de falso, lo que hará que
el ciclo termine y termine también el método run(). También en la clase principal
le asignamos a la referencia al hilo, temporizador, el valor de null para que el
recolector de basura destruya al objeto y libere los recursos que el hilo tenía
asignados.
Temporizador.java
public class Temporizador extends Thread {
public volatile boolean vivo;
public Temporizador() {
vivo = true;
Hilos en Java
PruebaTemporizador.java
public class PruebaTemporizador {
public static void main(String[] args) {
Temporizador temporizador = new Temporizador();
temporizador.start();
try {
Thread.sleep(10000);
}
catch(InterruptedException ie) {
System.out.println(ie.getMessage());
}
temporizador.vivo = false;
temporizador = null;
}
Figura 12.5
PruebaTemporizador.java
public class PruebaTemporizador {
public static void main(String[] args) {
Temporizador temporizador = new Temporizador();
temporizador.start();
try {
Thread.sleep(10000);
}
catch(InterruptedException ie) {
System.out.println(ie.getMessage());
}
temporizador.vivo = false;
while (temporizador.isAlive()) {
try {
Thread.sleep(100);
} catch (InterruptedException ie) {
System.out.println(ie.getMessage());
}
}
temporizador = null;
}
}
Unión de Hilos
asignados al hilo. Hay otros métodos en la API de Java que son más adecuados
para esta tarea. A este acto de esperar se le conoce como una unión de hilos. Nos
estamos uniendo con el hilo creado previamente. Para unirnos con un hilo
podemos usar el método join(), tabla 12.4.
Espera a que este hilo muera cuando mucho el número de milisegundos (más el número de
nanosegundos, en el caso del segundo método) dado por el parámetro. En el primer método, un
valor del parámetro de 0 significa esperar por siempre.
El siguiente código ilustra el uso del método join() para esperar que un hilo
termine su ejecución:
PruebaTemporizador.java
public class PruebaTemporizador {
public static void main(String[] args) {
Temporizador temporizador = new Temporizador();
temporizador.start();
try {
Thread.sleep(10000);
}
catch(InterruptedException ie) {
System.out.println(ie.getMessage());
}
temporizador.vivo = false;
try {
temporizador.join();
} catch (InterruptedException ie) {
System.out.println(ie.getMessage());
}
Hilos en Java
temporizador = null;
}
}
Lanza:
SecurityException – Si el hilo actual no puede modificar el nombre de este hilo.
public static Thread currentThread()
Regresa:
El número de hilos colocados en el arreglo.
Lanza:
SecurityException – Si el hilo actual no puede modificar el nombre de este hilo.
public static int activeCount()
Si este hilo está bloqueado en una invocación de los métodos wait() de la clase Object o de
los métodos join() o sleep() de esta clase, entonces su estado interrumpido se limpia y
recibirá una excepción del tipo InterruptedException. De otra manera el estado interrumpido
se establecerá.
Lanza:
SecurityException – Si el hilo actual no puede modificar este hilo.
public static boolean interrupted()
Prueba si el hilo actual ha sido interrumpido. El método limpia el estado interrumpido del hilo. En
otras palabras, si este método se llama dos veces en consecutivas, la segunda llamada, regresará
falso (a menos que el hilo actual sea interrumpido de nuevo, después de que la primera llamada
haya limpiado su estado interrumpido y antes de que la segunda llamada lo inspeccione.
public boolean isInterrupted()
Prueba si el hilo actual ha sido interrumpido. El método no modifica el estado interrumpido del hilo.
public final int getPriority()
Hace que el hilo en ejecución actualmente, haga una pausa temporal y permita que otro hilo
ejecute.
Sincronización
Al compartir datos entre varios hilos puede ocurrir una situación llamada
condición de carrera entre dos hilos intentando acceder al mismo dato a más o
menos al mismo tiempo. Para ilustrar el problema veamos el siguiente problema:
if(c.deducir(cantidad))
entregar(cantidad);
imprimirRecibo();
}
Cuenta getCuenta() {
...
}
void imprimirRecibo() {
...
}
}
return false;
}
}
La implementación parece funcionar hasta que dos personas que tengan acceso a
la misma cuenta (cuentas mancomunadas). Un día un esposo y su esposa (por
separado) deciden vaciar la cuenta y por coincidencia lo intentan al mismo
tiempo. En este caso tenemos una condición de carrera: si los dos usuarios retiran
del banco al mismo tiempo, haciendo que los métodos se llamen al mismo tiempo,
es posible que los dos cajeros confirmen que la cuenta tiene suficiente dinero y se
los entregue a ambos usuarios. Lo que ocurre es que dos hilos acceden a la base
de datos de la cuenta al mismo tiempo.
Para tratar con este problema, el lenguaje Java provee de la palabra reservada
syncronized que impide que dos hilos ejecuten el mismo código al mismo
tiempo. Si modificamos el método deducir() de la clase cuenta de la
siguiente forma:
Hilos en Java
return false;
}
}
Método Sincronizado
sentencias
}
Bloque Sincronizado
syncronized(expresión) {
declaraciones
sentencias
}
Donde expresión debe ser una expresión que se evalué a un objeto, el objeto del
que se obtiene el candado.
Hilos en Java
sentencias
}
}
Esperar y Notificar
La clase Object tiene los métodos wait() y notify(). Esos métodos permiten
que un hilo libere el candado en un momento arbitrario, y espere a que otro hilo se
lo regrese antes de continuar. Estas actividades deben ocurrir dentro de bloques
sincronizados.
Despierta un solo hilo que está esperando por el monitor de este objeto. Si hay varios hilos
esperando por este objeto, uno de ellos es seleccionado para despertarse. La selección es
arbitraria y ocurre a la discreción de la implementación. Un hilo espera por el monitor del objeto al
llamar a alguno de sus métodos wait().
El hilo despertado no podrá proceder hasta que el hilo actual libere el candado sobre el objeto. El
hilo despertado competirá con cualquier otro hilo que este compitiendo activamente para
sincronizarse con este objeto; por ejemplo, el hilo despertado no tiene privilegios o desventajas
para ser el siguiente hilo en ponerle un candado al objeto.
Este método sólo debe llamarlo un hilo que sea el dueño del monitor del objeto. Un hilo se vuelve
el dueño del monitor del objeto en una de tres formas:
Sólo uno de los hilos a la vez puede ser el dueño del monitor del objeto.
Lanza:
IllegalMonitorStateException – Si el hilo actual no es el dueño del monitor del
objeto.
Hilos en Java
Despierta a todos los hilos que están esperando por el monitor del objeto. Un hilo espera por el
monitor del objeto al llamar a alguno de sus métodos wait().
Los hilos despertados no podrán proceder hasta que el hilo actual libere el candado sobre el
objeto. Los hilos despertados competirán con cualquier otro hilo que este compitiendo activamente
para sincronizarse con este objeto; por ejemplo, el hilo despertado no tiene privilegios o
desventajas para ser el siguiente hilo en ponerle un candado al objeto.
Este método sólo debe llamarlo un hilo que sea el dueño del monitor del objeto.
Lanza:
IllegalMonitorStateException – Si el hilo actual no es el dueño del monitor del
objeto.
public final void wait(long timeout) throws InterruptedException
Hace que el hilo actual espere hasta que otro hilo invoque al método notify() o al método
notifyAll() de este objeto, o la cantidad de milisegundos especificada por el parámetro haya
transcurrido.
Este método hace que el hilo actual se coloque en el estado de espera para este objeto y luego
libere cualquier derecho de sincronización en este objeto. El hilo deja de estar disponible para el
planificador de procesos y permanece dormido hasta que alguna de estas cuatro cosas suceden:
• Algún otro hilo invoca al método notify() para este objeto y el hilo es seleccionado
arbitrariamente para ser el hilo a despertar.
• Algún otro hilo invoca al método notifyAll() para este objeto.
• Algún otro hilo interrumpe al hilo.
• El tiempo en milisegundos especificado por el parámetro ha transcurrido (más o menos).
Si timeout es cero, el hilo espera hasta ser notificado.
El hilo es removido del estado de espera para este objeto y queda disponible para el planificador
de procesos. Luego compite con los otros hilos por el derecho de sincronizarse con el objeto. Una
vez que ha ganado el control del objeto, todos sus derechos de sincronización en el objeto se
restablecen al estado que tenían en el momento en que se invoco al método wait(). El hilo luego
regresa del método wait().
Lanza:
IllegalArgumentException – Si el valor de timeout es negativo.
IllegalMonitorStateException – Si el hilo actual no es el dueño del monitor del
objeto.
InterruptedException – Si cualquier hilo ha interrumpido el corriente hilo antes o
mientras el hilo actual está esperando por una notificación. El estado de interrumpido del
hilo actual se limpia cuando se lanza la excepción.
Hilos en Java
Hace que el hilo actual espere hasta que otro hilo invoque al método notify() o al método
notifyAll() de este objeto, u otro hilo interrumpa al hilo actual o la cantidad de tiempo dado
por sus parámetros haya transcurrido.
El método es similar al método wait con un solo parámetro, pero permite un control más fino sobre
la cantidad de tiempo a esperar por la notificación antes de continuar. El tiempo a esperar medido
en nanosegundos está dada por:
1000000*timeout+nanos
En todos los demás aspectos, este método es similar al método wait (long
timeout). En particular wait(0, 0) significa lo mismo que wait(0).
public final void wait() throws InterruptedException
Cuando el primer hilo logra obtener de nuevo el candado, continúa desde el punto
en que se quedó. Sin embargo, el hilo que estaba esperando puede no adquirir el
candado inmediatamente (o tal vez, nunca). Depende de cuando el segundo hilo
eventualmente libere el candado, y que hilo logre atraparlo enseguida. El primer
hilo no se despertará a menos que otro hilo llame al método notify() . Hay una
versión sobrecargada de wait(), que permite especificar un tiempo de
expiración. Si otro hilo no invoca al método notify() en el tiempo especificado,
el hilo se despierta automáticamente.
Para cada llamada a notify(), Java despierta uno de los métodos dormidos en
la llamada a wait(). Si hay varios métodos esperando, Java selecciona al primer
hilo en base a primero en entrar – primero en salir.
import java.util.Date;
@Override
public String toString() {
return "Tarea " + numTarea + ": " + duracion + ", "
+ timeStamp.toString();
}
}
GeneradorAleatorio.java
/*
* GeneradorAleatorio.java
*
* @author mdomitsu
*/
package cola;
do {
k++;
p *= Math.random();
} while (p > L);
return k - 1;
}
}
import java.util.ArrayList;
import java.util.List;
colaTareas.add(tarea);
notify();
}
while (colaTareas.size() == 0) {
wait();
}
Tarea tarea = colaTareas.get(0);
colaTareas.remove(tarea);
return tarea;
}
import cola.ColaTareas;
import cola.GeneradorAleatorio;
import cola.Tarea;
@Override
public void run() {
int i = 0;
try {
while (i < MAX_TAREAS) {
produceTarea(i);
colaTareas.ponTarea(tarea);
System.out.println("Productor: " + tarea + ", "
+ colaTareas.getTamano());
}
}
import cola.ColaTareas;
import cola.Tarea;
@Override
public void run() {
try {
while (true) {
Tarea tarea = consumeTarea();
sleep(1000 * tarea.getDuracion());
}
} catch (InterruptedException e) {}
}
tarea = colaTareas.obtenTarea();
System.out.println(getName() + ": " + tarea+ ", "
+ colaTareas.getTamano());
return tarea;
}
}
En la siguiente clase de prueba se simula el caso de que hay una solo estación de
atención para los clientes:
Hilos en Java
Prueba.java
/*
* Prueba.java
*
* @author mdomitsu
*/
package pruebas;
import cola.ColaTareas;
import hilos.Consumidor;
import hilos.Productor;
/**
*
* @author mdomitsu
*/
public class Prueba {
public static void main(String[] args) {
ColaTareas colaMensajes = new ColaTareas();
productor.start();
consumidor1.start();
}
}
Prueba.java
/*
* Prueba.java
*
* @author mdomitsu
*/
package pruebas;
import cola.ColaTareas;
import hilos.Consumidor;
import hilos.Productor;
/**
*
* @author mdomitsu
*/
public class Prueba {
public static void main(String[] args) {
ColaTareas colaMensajes = new ColaTareas();
productor.start();
consumidor1.start();
consumidor2.start();
}
}