Logo Studenta

Hilos-en-Java

¡Este material tiene más páginas!

Vista previa del material en texto

1 
 
 
Fuente: http://www.chuidiang.com/java/hilos/hilos_java.php 
 
Hilos en Java 
A veces necesitamos que nuestro programa Java realice varias cosas simultáneamente. 
Otras veces tiene que realizar una tarea muy pesada, por ejemplo, consultar en el listín 
telefónico todos los nombres de chica que tengan la letra n, que tarda mucho y no 
deseamos que todo se quede parado mientras se realiza dicha tarea. Para conseguir que 
Java haga varias cosas a la vez o que el programa no se quede parado mientras realiza una 
tarea compleja, tenemos los hilos (Threads). 
Crear un Hilo 
Crear un hilo en java es una tarea muy sencilla. Basta heredar de la clase Thread y definir 
el método run(). Luego se instancia esta clase y se llama al método start() para que 
arranque el hilo. Más o menos esto 
public MiHilo extends Thread 
{ 
 public void run() 
 { 
 // Aquí el código pesado que tarda mucho 
 } 
}; 
... 
MiHilo elHilo = new MiHilo(); 
elHilo.start(); 
System.out.println("Yo sigo a lo mio"); 
Listo. Hemos creado una clase MiHilo que hereda de Thread y con un método run(). En el 
método run() pondremos el código que queremos que se ejecute en un hilo separado. 
Luego instanciamos el hilo con un new MiHilo() y lo arrancamos con elHilo.start(). El 
System.out que hay detrás se ejecutará inmediatamente después del start(), haya 
terminado o no el código del hilo. 
Detener un hilo 
Suele ser una costumbre bastante habitual que dentro del método run() haya un bucle 
infinito, de forma que el método run() no termina nunca. Por ejemplo, supongamos un 
http://www.chuidiang.com/java/hilos/hilos_java.php
 
2 
 
chat. Cuando estás chateando, el programa que tienes entre tus manos está haciendo dos 
cosas simultáneamente. Por un lado, lee el teclado para enviar al servidor del chat todo lo 
que tú escribas. Por otro lado, está leyendo lo que llega del servidor del chat para 
escribirlo en tu pantalla. Una forma de hacer esto es "por turnos" 
while (true) 
{ 
 leeTeclado(); 
 enviaLoLeidoAlServidor(); 
 leeDelServidor(); 
 muestraEnPantallaLoLeidoDelServidor(); 
} 
Esta, desde luego, es una opción, pero sería bastante "cutre", tendríamos que hablar por 
turnos. Yo escribo algo, se le envía al servidor, el servidor me envía algo, se pinta en 
pantalla y me toca a mí otra vez. Si no escribo nada, tampoco recibo nada. Quizás sea 
buena opción para los que no son ágiles leyendo y escribiendo, pero no creo que le guste 
este mecanismo a la mayoría de la gente. 
Lo normal es hacer dos hilos, ambos en un bucle infinito, leyendo (del teclado o del 
servidor) y escribiendo (al servidor o a la pantalla). Por ejemplo, el del teclado puede ser 
así 
public void run() 
{ 
 while (true) 
 { 
 String texto = leeDelTeclado(); 
 enviaAlServidor(texto); 
 } 
} 
Esta opción es mejor, dos hilos con dos bucles infinitos, uno encargado del servidor y otro 
del teclado. 
Ahora viene la pregunta del millón. Si queremos detener este hilo, ¿qué hacemos?. Los 
Thread de java tienen muchos métodos para parar un hilo: destroy(), stop(), suspend() ... 
Pero, si nos paramos a mirar la API de Thread, nos llevaremos un chasco. Todos esos 
métodos son inseguros, están obsoletos, desaconsejados o las tres cosas juntas. 
¿Cómo paramos entonces el hilo? 
La mejor forma de hacerlo es implementar nosotros mismos un mecanismo de parar, que 
lo único que tiene que hacer es terminar el método run(), saliendo del bucle. 
http://www.chuidiang.com/java/sockets/hilos/socket_hilos.php
http://java.sun.com/j2se/1.5.0/docs/api/java/lang/Thread.html
 
3 
 
Un posible mecanismo es el siguiente 
public class MiHilo extends Thread 
{ 
 // boolean que pondremos a false cuando queramos parar el hilo 
 private boolean continuar = true; 
 
 // metodo para poner el boolean a false. 
 public void detenElHilo() 
 { 
 continuar=false; 
 } 
 
 // Metodo del hilo 
 public void run() 
 { 
 // mientras continuar ... 
 while (continuar) 
 { 
 String texto = leeDelTeclado(); 
 enviaAlServidor(texto); 
 } 
 } 
} 
Simplemente hemos puesto en la clase un boolean para indicar si debemos seguir o no 
con el bucle infinito. Por defecto a true. Luego hemos añadido un método para cambiar el 
valor de ese boolean a false. Finalmente hemos cambiado la condición del bucle que 
antes era true y ahora es continuar. 
Para parar este hilo, es sencillo 
MiHilo elHilo = new MiHilo(); 
elHilo.start(); 
// Ya tenemos el hilo arrancado 
... 
// Ahora vamos a detenerlo 
elHilo.detenElHilo(); 
 
Sincronización de hilos 
Cuando en un programa tenemos varios hilos corriendo simultáneamente es posible que 
varios hilos intenten acceder a la vez a un mismo sitio (un fichero, una conexión, un array 
 
4 
 
de datos) y es posible que la operación de uno de ellos entorpezca la del otro. Para evitar 
estos problemas, hay que sincronizar los hilos. Por ejemplo, si un hilo con vocación de 
Cervantes escribe en fichero "El Quijote" y el otro con vocación de Shakespeare escribe 
"Hamlet", al final quedarán todas las letras entremezcladas. Hay que conseguir que uno 
escriba primero su Quijote y el otro, luego, su Hamlet. 
Sincronizar usando un objeto 
Imagina que escribimos en un fichero usando una variable fichero de tipo PrintWriter. 
Para escribir uno de los hilos hará esto 
fichero.println("En un lugar de la Mancha..."); 
Mientras que el otro hará esto 
fichero.println("... ser o no ser ..."); 
Si los dos hilos lo hacen a la vez, sin ningún tipo de sincronización, el fichero al final puede 
tener esto 
En un ... ser lugar de la o no Mancha ser ... 
Para evitarlo debemos sincronizar los hilos. Cuando un hilo escribe en el fichero, debe 
marcar de alguna manera que el fichero está ocupado. El otro hilo, al intentar escribir, lo 
verá ocupado y deberá esperar a que esté libre. En java esto se hace fácilmente. El código 
sería así 
synchronized (fichero) 
{ 
 fichero.println("En un lugar de la Mancha..."); 
} 
y el otro hilo 
synchronized (fichero) 
{ 
 fichero.println("... ser o no ser ..."); 
} 
Al poner synchronized(fichero) marcamos fichero como ocupado desde que se abren las 
llaves de después hasta que se cierran. Cuando el segundo hilo intenta también su 
 
5 
 
synchronized(fichero), se queda ahí bloqueado, en espera que de que el primero termine 
con fichero. Es decir, nuestro hilo Shakespeare se queda parado esperando en el 
synchronized(fichero) hasta que nuestro hilo Cervantes termine. 
synchronized comprueba si fichero está o no ocupado. Si está ocupado, se queda 
esperando hasta que esté libre. Si está libre o una vez que esté libre, lo marca como 
ocupado y sigue el código. 
Este mecanismo requiere colaboración entre los hilos. El que hace el código debe 
acordarse de poner synchronized siempre que vaya a usar fichero. Si no lo hace, el 
mecanismo no sirve de nada. 
Métodos sincronizados 
Otro mecanismo que ofrece java para sincronizar hilos es usar métodos sincronizados. 
Este mecanismo evita además que el que hace el código tenga que acordarse de poner 
synchronized. 
Imagina que encapsulamos fichero dentro de una clase y que ponemos un método 
synchronized para escribir, tal que así 
public class ControladorDelFichero 
{ 
 private PrintWriter fichero; 
 
 public ControladorFichero() 
 { 
 // Aqui abrimos el fichero y lo dejamos listo 
 // para escribir. 
 } 
 
 public synchronized void println(String cadena) 
 { 
 fichero.println(cadena); 
 } 
} 
Una vez hecho esto, nuestros hilos Cervantes y Shakespeare sólo tienen que hacer esto 
 
6 
 
ControladorFichero control = new ControladorFichero(); 
... 
// Hilo Cervantes 
control.println("En un lugar de la Mancha ..."); 
... 
// Hilo Shakespeare 
control.println("... ser o no ser ..."); 
Al ser el método println() synchronized,si algún hilo está dentro de él ejecutando el 
código, cualquier otro hilo que llame a ese método se quedará bloqueado en espera de 
que el primero termine. 
Este mecanismo es más mejor porque, siguiendo la filosofía de la orientación a objetos, 
encapsula más las cosas. El fichero requiere sincronización, pero ese conocimiento sólo lo 
tiene la clase ControladorFichero. Los hilos Cervantes y Shakespeare no saben nada del 
tema y simplemente se ocupan de escribir cuando les viene bien. Tampoco depende de la 
buena memoria del programador a la hora de poner el synchronized(fichero) de antes. 
Otros objetos que necesitan sincronización 
Hemos puesto de ejemplo un fichero, pero requieren sincronización en general cualquier 
entrada y salida de datos, como pueden ser ficheros, sockets o incluso conexiones con 
bases de datos. 
También pueden necesitar sincronización almacenes de datos en memoria, como 
LinkedList, ArrayList, etc. Imagina, por ejemplo, en una LinkedList que un hilo está 
intentando sacar por pantalla todos los datos 
LinkedList lista = new LinkedList(); 
... 
for (int i=0;i<lista.size(); i++) 
 System.out.println(lista.get(i)); 
Estupendo y maravilloso pero ... ¿qué pasa si mientras se escriben estos datos otro hilo 
borra uno de los elementos?. Imagina que lista.size() nos ha devuelto 3 y justo antes de 
intentar escribir el elemento 2 (el último) viene otro hilo y borra cualquiera de los 
elementos de la lista. Cuando intentemos el lista.get(2) nos saltará una excepción porque 
la lista ya no tiene tantos elementos. 
 
7 
 
La solución es sincronizar la lista mientras la estamos usando 
LinkedList lista = new LinkedList(); 
... 
synchronized (lista) 
{ 
 for (int i=0;i<lista.size(); i++) 
 System.out.println(lista.get(i)); 
} 
además, este tipo de sincronización es la que se requiere para mantener "ocupado" el 
objeto lista mientras hacemos varias llamadas a sus métodos (size() y get()), no queda 
más remedio que hacerla así. Por supuesto, el que borra también debe preocuparse del 
synchronized. 
Esperando datos: wait() y notify() 
A veces nos interesa que un hilo se quede bloqueado a la espera de que ocurra algún 
evento, como la llegada de un dato para tratar o que el usuario termine de escribir algo en 
una interface de usuario. Todos los objetos java tienen el método wait() que deja 
bloqueado al hilo que lo llama y el método notify(), que desbloquea a los hilos bloqueados 
por wait(). Vamos a ver cómo usarlo en un modelo productor/consumidor. 
Bloquear un hilo 
Antes de nada, que quede claro que las llamadas a wait() lanzan excepciones que hay que 
capturar. Todas las llamadas que pongamos aquí deberían estar en un bloque try-catch, 
así 
try 
{ 
 // llamada a wait() 
} 
catch (Exception e) 
{ 
 .... 
} 
 
8 
 
pero para no liar mucho el código y mucho más importante, no auto darme más trabajo 
de escribir de la cuenta, no voy a poner todo esto cada vez. Cuando hagas código, habrá 
que ponerlo. 
Vamos ahora a lo que vamos... 
Para que un hilo se bloquee basta con que llame al método wait() de cualquier objeto. Sin 
embargo, es necesario que dicho hilo haya marcado ese objeto como ocupado por medio 
de un synchronized. Si no se hace así, saltará una excepción de que "el hilo no es 
propietario del monitor" o algo así. 
Imaginemos que nuestro hilo quiere retirar datos de una lista y si no hay datos, quiere 
esperar a que los haya. El hilo puede hacer algo como esto 
synchronized(lista); 
{ 
 if (lista.size()==0) 
 lista.wait(); 
 
 dato = lista.get(0); 
 lista.remove(0); 
} 
En primer lugar hemos hecho el synchronized(lista) para "apropiarnos" del objeto lista. 
Luego, si no hay datos, hacemos el lista.wait(). Una vez que nos metemos en el wait(), el 
objeto lista queda marcado como "desocupado", de forma que otros hilos pueden usarlo. 
Cuando despertemos y salgamos del wait(), volverá a marcarse como "ocupado." 
Nuestro hilo se desbloquerá y saldrá del wait() cuando alguien llame a lista.notify(). Si el 
hilo que mete datos en la lista llama luego a lista.notify(), cuando salgamos del wait() 
tendremos datos disponibles en la lista, así que únicamente tenemos que leerlos (y 
borrarlos para no volver a tratarlos la siguiente vez). Existe otra posibilidad de que el hilo 
se salga del wait() sin que haya datos disponibles, pero la veremos más adelante. 
Notificar a los hilos que están en espera 
Hemos dicho que el hilo que mete datos en la lista tiene que llamar a lista.notify(). Para 
esto también es necesario apropiarnos del objeto lista con un synchronized. El código del 
hilo que mete datos en la lista quedará así 
synchronized(lista) 
{ 
 lista.add(dato); 
 
9 
 
 lista.notify(); 
} 
Listo, una vez que hagamos esto, el hilo que estaba bloqueado en el wait() despertará, 
saldrá del wait() y seguirá su código leyendo el primer dato de la lista. 
wait() y notify() como cola de espera 
wait() y notify() funcionan como una lista de espera. Si varios hilos van llamando a wait() 
quedan bloqueados y en una lista de espera, de forma que el primero que llamó a wait() 
es el primero de la lista y el último es el útlimo. 
Cada llamada a notify() despierta al primer hilo en la lista de espera, pero no al resto, que 
siguen dormidos. Necesitamos por tanto hacer tantos notify() como hilos hayan hecho 
wait() para ir despertándolos a todos de uno en uno. 
Si hacemos varios notify() antes de que haya hilos en espera, quedan marcados todos esos 
notify(), de forma que los siguientes hilos que hagan wait() no se quedaran bloqueados. 
En resumen, wait() y notify() funcionan como un contador. Cada wait() mira el contador y 
si es cero o menos se queda bloqueado. Cuando se desbloquea decrementa el contador. 
Cada notify() incrementa el contador y si se hace 0 o positivo, despierta al primer hilo de 
la cola. 
Un símil para entenderlo mejor. Una mesa en la que hay gente que pone caramelos y 
gente que los recoge. La gente son los hilos. Los que van a coger caramelos (hacen wait()) 
se ponen en una cola delante de la mesa, cogen un caramelo y se van. Si no hay 
caramelos, esperan que los haya y forman una cola. Otras personas ponen un caramelo en 
la mesa (hacen notify()). El número de caramelos en la mesa es el contador que 
mencionabamos. 
Modelo Productor/Consumidor 
Nuevamente y como comentamos en sincronizar hilos, es buena costumbre de 
orientación a objetos "ocultar" el tema de la sincronización a los hilos, de forma que no 
dependamos de que el programador se acuerde de implementar su hilo correctamente 
(llamada a synchronized y llamada a wait() y notify()). 
Para ello, es práctica habitual meter la lista de datos dentro de una clase y poner dos 
métodos synchronized para añadir y recoger datos, con el wait() y el notify() dentro. 
El código para esta clase que hace todo esto puede ser así 
http://www.chuidiang.com/java/hilos/sincronizar_hilos_java.php
 
10 
 
public class MiListaSincronizada 
{ 
 private LinkedList lista = new LinkedList(); 
 
 public synchronized void addDato(Object dato) 
 { 
 lista.add(dato); 
 lista.notify(); 
 } 
 
 public synchronized Object getDato() 
 { 
 if (lista.size()==0) 
 wait(); 
 Object dato = lista.get(0); 
 lista.remove(0); 
 return dato; 
 } 
} 
Listo, nuestros hilos ya no deben preocuparse de nada. El hilo que espera por los datos 
hace esto 
Object dato = listaSincronizada.getDato(); 
y eso se quedará bloqueado hasta que haya algún dato disponible. Mientras, el hilo que 
guarda datos sólo tiene que hacer esto otro 
listaSincronizada.addDato(dato); 
Interrumpir un hilo 
Comentamos antes que es posible que un hilo salga del wait() sin necesidad de que nadie 
haga notify(). Esta situación se da cuando se produce algún tipo de interrupción. En el 
caso de java es fácil provocar una interrupción llamandoal método interrupt() del hilo. 
Por ejemplo, si el hiloLector está bloqueado en un wait() esperando un dato, podemos 
interrumpirle con 
hiloLector.interrupt(); 
El hiloLector saldrá del wait() y se encontrará con que no hay datos en la lista. Sabrá que 
alguien le ha interrumpido y hará lo que tenga que hacer en ese caso. 
 
11 
 
Por ejemplo, imagina que tenemos un hilo lectorSocket pendiente de un socket (una 
conexión con otro programa en otro ordenador a través de red) que lee datos que llegan 
del otro programa y los mete en la listaSincronizada. 
Imagina ahora un hilo lectorDatos que está leyendo esos datos de la listaSincronizada y 
tratándolos. 
¿Qué pasa si el socket se cierra?. Imagina que nuestro programa decide cerrar la conexión 
(socket) con el otro programa en red porque se han enfadado y ya no piensan hablarse 
nunca más. Una vez cerrada la conexión, el hilo lectorSocket puede interrumpir al hilo 
lectorDatos. Este, al ver que ha salido del wait() y que no hay datos disponibles, puede 
suponer que se ha cerrado la conexión y terminar. 
El código del hilo lectorDatos puede ser así 
while (true) 
{ 
 if (listaSincronizada.size() == 0) 
 wait(); 
 
 // Debemos comprobar que efectivamente hay datos. 
 if (listaSincronizada.size() > 0) 
 { 
 // Hay datos, los tratamos 
 Object dato=listaSincronizada.get(0); 
 listaSincronizada.remove(0); 
 // tratar el dato. 
 } 
 else 
 { 
 // No hay, datos se debe haber cerrado la conexion 
 // así que nos salimos. 
 return; 
 } 
} 
y el hilo lectorSocket, cuando cierra la conexión, debe hacer 
socket.close(); 
lectorDatos.interrupt(); 
 
http://www.chuidiang.com/java/sockets/socket.php

Continuar navegando