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.
|