Sólo Programadores Banner 468x60
Este mes en Sólo Programadores
Contenido del CD-ROM
Índice temático
Suscripción

JAVA

Programación en Java usando threads (y III)

Para efectuar determinadas acciones sobre un conjunto de threads de forma simultánea se pueden crear grupos de hilos, de manera que no hay necesidad de tener que llamar a un método por cada uno de ellos. Gracias a los threads, se pueden desarrollar applets de forma sencilla con unos resultados espectaculares.

Cuando una aplicación utiliza multihilos siempre surge la necesidad de realizar ciertas operaciones que afecten a todos los hilos en ejecución. Es más, en determinadas condiciones también es necesario agrupar estos threads según ciertas características, como pueden ser su prioridad de ejecución, de forma que determinados cambios afecten a los hilos de según que grupos.

GRUPOS DE THREADS

En Java, todos los threads pertenecen a un determinado grupo, el cual puede puede ser el grupo por defecto determinado por el sistema o un grupo que el programador haya definido de forma explícita. Durante el proceso de creación el thread se añade a un grupo y no puede cambiar durante su vida a otro diferente. Si se crean más de un thread sin especificar su grupo estos serán añadidos al grupo del sistema.

Todos los grupos de threads a su vez pertenecen a otro grupo de threads. Un nuevo grupo puede pertenecer a otro según se defina en el constructor del grupo de threads o si no se especifica nada, se le incluirá como un grupo hijo dentro del grupo del sistema.

La razón de la existencia de los grupos de threads para cada grupo, es decir, el motivo por el que un grupo siempre debe estar incluido en otro es de difícil explicación. La mayoría de las publicaciones explican que esta característica viene dada por razones de seguridad, ya que los threads de un grupo pueden modificar a otros threads en otro grupo, incluyendo a los del grupo padre. Pero los threads que pertenecen a un grupo no pueden modificar a otros que se encuentren en un grupo con el que no mantienen relación, ya sea por ser otro grupo padre, o por no ser un grupo hijo directo.

MÉTODOS PARA LOS GRUPOS DE THREADS

Los métodos utilizables para los grupos de threads se pueden clasificar en función de:

- Si actúa sobre los threads que pertenecen a un grupo.

- Si la acción recae sobre los threads que pertenecen al grupo actual y a los grupos relacionados.

En el primer caso tenemos por ejemplo los métodos setDaemon() y setMaxPriority() que no modifican las características del grupo, sino la de los sus threads. En cambio los siguiente métodos:

resume()
stop()
suspend()

Son los que actúan sobre todos los threads del grupo y de sus grupos descendientes.

Además tenemos que destacar que los siguientes métodos permiten obtener los atributos de un grupo de threads:

 getMaxPriority()
getDaemon()
getName()
getParent()
list() 

De estos métodos, getName() devuelve el nombre de un determinado thread para poder realizar determinadas acciones sobre uno particular. El método getParent() devuelve el grupo padre al que pertenece el grupo del thread actual. El método list() es un método especial pues permite visualizar las características de un grupo de threads, por lo que se usa principalmente en la depuración de código.

EJEMPLOS PRÁCTICOS

Una vez que conocemos los fundamentos de los grupos de threads vamos a desarrollar unos ejemplos para poner en práctica la teoría. En el ejemplo del Listado 1 vamos a ver como los threads de un determinado grupo "hijo" pueden acceder a los métodos de los threads del grupo padre.

Ejecutamos el código y obtenemos a continuación:

java.lang.ThreadGroup
[name=x,maxpri=10]
Thread[hilo1,5,x]
java.lang.ThreadGroup
[name=y,maxpri=10]
java.lang.ThreadGroup
[name=z,maxpri=10]
Thread[hilo2,5,z]
el hilo : hilo1 ejecuta f()
el hilo : hilo2 ejecutaf()
java.lang.ThreadGroup
[name=x,maxpri=10]
Thread[hilo1,1,x]
java.lang.ThreadGroup
[name=y,maxpri=10]
java.lang.ThreadGroup
[name=z,maxpri=10]
Thread[hilo2,1,z] 

En el código se observa que en la clase acceso se definen los grupos de threads. En la salida del programa se aprecia que el grupo <X>es un grupo que pertenece al sistema, por lo que hereda las características del mismo. Como vemos en la primera línea dicho grupo contendrá hilos con una prioridad máxima de valor 10. Los siguiente grupos, tales como el <Y>que será un subgrupo de <X> y a su vez, <z> que lo será de <y>.

Los dos threads que se crean pertenecerán respectivamente a los grupos <y> y <z>En la clase de usaThread1 definimos los métodos para los threads del grupo <y>y a su vez, en la clase usaThread2 para los de <z>, que heredan los métodos del grupo de threads <y>

En la ejecución del método run() de usaThread2 se crean los hilos del grupo y se visualizan sus características. A continuación se crea una tabla con todos los threads que están en ejecución con ayuda del método activeCount() que devuelve el número de hilos activos.

Mediante el método enumerate() se copian en la tabla especificada el conjunto de los threads de un determinado grupo y su primer nivel de subgrupos. Existe un método sobrecargado de éste que permite incluir a todos los grupos "hijos" del grupo en cuestión de forma recursiva.

Ya que tenemos los threads en una tabla, les cambiamos la prioridad y hacemos que llamen a la función f() de la clase usa Thread1, independientemente de si los threads pertenecen al grupo padre o hijo.

Con este ejemplo que hemos expuesto ha quedado reflejada la característica que indica que los grupos de threads hijos pueden acceder a los métodos del grupo padre. Pero la característica más importante del grupo de threads es la facilidad del control de los mismos en forma de conjunto, cuando se pretende que una determinada acción afecte a todos los threads.

Con el ejemplo del Listado 2 se demuestra lo sencillo que resulta manipular las propiedades de los grupos de threads, así como la facilidad con la que todos los threads de un grupo realizan una misma función mediante una simple llamada a un método.

De la ejecución del código obtenemos el siguiente resultado:

java.lang.ThreadGroup[name=main,maxpri=10]

 Thread[main,5,main]

java.lang.ThreadGroup[name=main,maxpri=9]

Thread[main,5,main]

java.lang.ThreadGroup[name=main,maxpri=9]

Thread[main,6,main]

java.lang.ThreadGroup[name=Grupo 1,maxpri=9]

java.lang.ThreadGroup[name=Grupo 1,maxpri=9]

Thread[hilo1_g1,7,Grupo 1]

java.lang.ThreadGroup[name=Grupo 1,maxpri=9] 

Thread[hilo1_g1,7,Grupo 1]

Thread[hilo2_g1,9,Grupo 1]

java.lang.ThreadGroup[name=Grupo 1,maxpri=5]

Thread[hilo1_g1,7,Grupo 1]

Thread[hilo2_g1,9,Grupo 1] 

java.lang.ThreadGroup[name=Hijo de Grupo 1,maxpri=5] 

java.lang.ThreadGroup[name=Hijo de Grupo 1,maxpri=5] 

Thread[hilo0_h1,5,Hijo de Grupo 1] 

Thread[hilo1_h1,5,Hijo de Grupo 1]

Thread[hilo2_h1,5,Hijo de Grupo 1]

Thread[hilo3_h1,5,Hijo de Grupo 1]
Todos los thread van a morir ..

 

Después de observar esta segunda ejecución analizaremos los resultados obtenidos.

De la ejecución de la parte (1) lo que obtenemos son los threads del grupo del sistema, por lo que también se obtienen en consecuencia sus características principales.

En la secuencia (2) se cambia la prioridad del grupo "sistema", afecta a la prioridad máxima que pueden obtener los threads de ese grupo.

En (3) se cambia la prioridad del thread main del grupo sistema.

En la parte (4) se genera un grupo hijo de sistema, y se le adjudica la máxima prioridad, que es la determinada por el grupo sistema.

En (5) y (6) se generan dos hilos para este último grupo y se observa la forma en que se puede modificar el atributo de prioridad.

En (7) se comprueba la herencia del atributo prioridad según viene determinado por el grupo padre y en (8) se crea un nuevo grupo hijo del anterior. La creación de varios threads para este grupo y la visualización de sus propiedades ocurre en (9) y (10).

En el apartado (11) se puede ejecutar la parte de suspend() que cambiará a estado de dormido a todos los threads del grupo y comentado esas líneas, se ejecutaría el stop() que finalizaría todos los threads creados.

Como se ha podido comprobar con estos ejemplos que hemos expuesto, el manejo de grupos de threads es simple y eficaz, de forma que el control de los grupos de threads ofrece una mayor potencia que el manejo individual de cada uno de ellos.

LOS THREADS EN LOS NAVEGADORES

Actualmente es muy común encontrar en las páginas web en Internet programas que se ejecutan en el navegador. Estos programas son applets Java, y su desarrollo esta íntimamente ligado al desarrollo de threads. La estructura básica de un applet recuerde a la implementación de un thread, de forma que se obtiene:

public class miApplet extends 
Applet implements Runnable { } 

Por tanto, estamos definiendo ya nuestro applet como un thread, de tal forma que sólo queda implementar el método run() que desarrolle la tarea que deseemos obtener. Además, los applets ofrecen características propias, disponiendo de unos procedimientos que se ejecutan automáticamente cuando el applet se carga y se destruye. Por ejemplo, el método init() es el primer método que se ejecuta cuando se carga un applet. Éste a su vez llama al método start() y es ahí donde comienza la ejecución propiamente dicha, desde donde se llama a los métodos update() y paint() para la visualización en pantalla. La parada de un applet o su destrucción pasa por ejecutar los métodos stop() y destroy() del propio applet.

Ahora bien, estos métodos están concebidos para controlar la creación y destrucción del applet, pero de su ejecución se debe encargar el programador y esto es posible debido a la implementación de la interaz Runnable para la creación de hilos, por lo que nuestro applet internamente lo que estará haciendo es ejecutar un thread. Gracias a esta posibilidad, los applets pueden realizar cualquier tarea como una aplicación normal, aunque también destacar que están definidas ciertas limitaciones para algunas tareas.

UNOS EJEMPLOS: EL RELOJ Y LA MÚSICA

Vamos a desarrollar un sencillo ejemplo para ver cómo los applets ejecutan internamente threads que se encargan de la ejecución de tareas. El ejemplo del Listado 3 muestra un reloj en el navegador.

Como se observa en el código explicado, hemos declarado un thread denominado hilo que se encargará de mostrar el reloj en una página del navegador. La ejecución del applet comienza por el método init() que muestra en la parte inferior del navegador el mensaje Preparando Reloj ... , indicando así el inicio de la ejecución del thread. A continuación se ejecuta el método start() y es ahí donde el thread es creado para que comience su ejecución. A continuación el método se llama al método run() y a partir de este momento la ejecución se desarrolla de forma análoga a un "normal". Por este motivo, tiene que dejar tiempo de ejecución a los otros posibles threads, por lo que se duerme durante un periodo de tiempo tras el cual ejecuta la tarea de mostrar el reloj. Para conseguir los valores correctos recoge la hora del sistema y la presenta en pantalla. La visualización en los applets siempre es realizada por el método repaint(), en el sentido de que es el único método que puede realizar a llamada al método paint() que es el que verdaderamente dibuja la salida que visualizaremos en el navegador, en este caso, el reloj.

Como se ha descrito, la finalización de un applet se realiza en el método stop(), que en caso de que tengamos threads será utilizado para finalizarlos. En este caso, en este método llama al proceso de stop() del thread.

Si bien se observa que generar applet basándonos en la creación de threads es algo relativamente sencillo, la creación de applets que contengan ciertas características obliga a realizar ciertas reflexiones previas.

Por ejemplo, si un applet tiene que tocar una canción y el tamaño del fichero de música es algo grande, nuestro applet estaría detenido hasta que se finalizara la carga del fichero de música, lo cual aparentemente no es algo muy recomendable. Una solución simple para este tipo de problemas es crear un thread que se encargue de todas esas tareas. En el caso de la música, tendríamos que generar un thread que se encargara de cargar la música y tocarla.

El ejemplo del Listado 4 muestra la forma en que un thread realiza la tarea de fondo de cargar la música y ejecutarla, mientras que en el applet otro thread ejecuta la tarea de mostrar el reloj, por lo que el thread que muestra el reloj no espera a que la música se cargue y empiece a tocar.

La parte fundamental del código se encuentra en la clase Player, que es la encargada de recibir la referencia del applet mediante el cual puede acceder al método play() que se encarga de cargar la música y tocarla. De este modo, tenemos dos threads ejecutándose a la vez, mientras que uno toca la música otro visualiza la hora en el navegador.

UTILIZACIÓN DE LOS THREADS EN LAS APLICACIONES CLIENTE/ SERVIDOR

El uso de threads en los servidores de aplicaciones cliente/servidor permite que las peticiones de los clientes se atiendan a mayor velocidad, además de producir una menor carga en el sistema debido a que el trasiego de los threads es mucho menos que el producido por los procesos tradicionales que se crean en el servidor. Estas dos cualidades hacen ideales a los threads en el desarrollo de servidores en aplicaciones cliente/servidor.

UN SERVIDOR BÁSICO

Veamos un ejemplo de un servidor multithread básico. El código del Listado 5 es un servidor de eco que acepta múltiples conexiones.

La clase servidor es una clase que genera hilos por cada conexión que se efectúe. Dentro de un bucle que efectúa la llamada accept() se genera un thread que se encarga de gestionar la conexión, generando su stream de entrada y salida del socket cliente. Todo lo que recibe el thread servidor es reenviado hasta que el cliente desconecte, por lo que el servidor ejecuta el método cerrar_conexión() en el que se cierra el socket y se elimina el thread de la conexión actual.

Los applets son programas Java que requieren para su funcionamiento el uso de las técnicas de los threads

Una variante, ya demasiado afinada, consiste en generar un thread por cada stream del socket, esto es, se genera un thread para la gestión de los datos recibos y otro para los datos que se desea enviar al cliente, de forma que entre estos threads se establece un mecanismo de comunicación. Se trata de un buen ejercicio que realizar.

LA TÉCNICA THREAD POOL PARA SERVIDORES

De todos modos, este sistema se puede mejorar. Como se ha mencionado, la operación de generar un thread, si bien es mucho más económica que la de un proceso, es aún considerable. Además, la operación de eliminar también requiere su tiempo.

Por tanto, una solución a la que deberíamos llegar, podría pasar por generar un conjunto de threads que estén esperando las conexiones y realizar la gestión sobre ellos, de forma que una cantidad de thread se generan según se inicia el servidor y no se destruyen sino que se utilizan para distintas conexiones, con lo que se evita el trasiego de creación y destrucción de los threads.

La idea anterior es conocida como la técnica de thread Pool. Aplicada al ejemplo anterior tendríamos el código que aparece en el Listado 6.

Con el uso de multihilos siempre surge la necesidad de realizar operaciones que afecten a todos los threads

Estudiando el código detenidamente se puede observar que se inicia la creación de los threads de forma global, de tal forma que se crea una cantidad de hilos.

Estos threads son los encargados de realizar las conexiones con los clientes, pero ahora no tenemos un único punto donde esperamos conexiones, sino que hay tantos métodos accept() como threads generados. Otra ventaja es que los threads ya se han generado y no se destruyen, por lo que todo el tiempo que consume estas operaciones se ahorra.

Gracias a la utilización de los threads, podemos tener un applets que efectue varias tareas simultaneamente

Los problemas que puede plantear esta técnica se refieren a la dificultad que supone determinar el número óptimo de conexiones que realizar, si bien, siempre se puede implementar un método que avise que se ha alcanzado el número máximo de conexiones ocupadas o encolar las conexiones hasta que se puedan atender.

CONCLUSIÓN

Los grupos de threads permiten tener un control y gestión de todos los threads que agrupan de una forma fácil y eficaz.

La aplicación de los threads en los navegadores nos permite la creación de nuevas aplicaciones sobre éstos de manera que un applet puede estar tocando una música mientras al mismo tiempo, visualiza una animación gráfica y a su vez se puede estar calculando cualquier serie de datos, todo ello de manera simultanea.

El uso de threads en aplicaciones del tipo cliente/servidor logra la mejora en el rendimiento de éstas

Además de todo esto, la utilización de threads en aplicaciones del tipo cliente/servidor nos permite a los usuarios que éstas se ejecuten de forma mucho más eficiente y se descarga a la máquina que contiene el servidor del cambio de contexto tan "pesado" que generan los procesos denominados tradicionales.



Banner 468x60
Explorer 4.0, Netscape 4.0. Resolución 800 x 600.
©Tower Communications 1.998.
Diseño: GRUPO ALBERTINA DE COMUNICACION.