Академический Документы
Профессиональный Документы
Культура Документы
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 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: detroy(), 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.
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;
Sincronización de hilos
... viene de hilos en java.
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 de datos)
y es posbible 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
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
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 filosfí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.
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.
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);
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 implemetar 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í
public class MiListaSincronizada
{
private LinkedList lista = new LinkedList();
public synchronized void addDato(Object dato)
{
lista.add(dato);
lista.notify();
}
Productor
El productor extenderá la clase Thread, y su código es el siguiente:
class Productor extends Thread {
private Tuberia tuberia;
private String alfabeto = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
p.start();
c.start();
}
}
Compilando y ejecutando esta aplicación, se podrá observar en modelo que se ha diseñado en pleno
funcionamiento.
Monitorización del Productor
Los programas productor/consumidor a menudo emplean monitorización remota, que permite al
consumidor observar el hilo del productor interaccionando con un usuario o con otra parte del
sistema. Por ejemplo, en una red, un grupo de hilos de ejecución productores podrían trabajar cada
uno en una workstation. Los productores imprimirían documentos, almacenando una entrada en un
registro (log). Un consumidor (o múltiples consumidores) podría procesar el registro y realizar
durante la noche un informe de la actividad de impresión del día anterior.
Otro ejemplo, a pequeña escala podría ser el uso de varias ventanas en una workstation. Una
ventana se puede usar para la entrada de información (el productor), y otra ventana reaccionaría a
esa información (el consumidor).
Peer, es un observador general del sistema.