Descarga la aplicación para disfrutar aún más
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
Compartir