JAVA
Utilización
de threads (I)
La utilización de threads
(literalmente hilos) permite la ejecución simultánea de
varios flujos de control que pueden realizar diferentes tareas e
incluso interactuar entre ellas. Mediante el uso de Java se pueden
desarrollar programas con hilos de una forma simple y efectiva,
permitiendo hacer uso de nuevas y potentes tecnologías.
Como usuarios estamos familiarizados
con las aplicaciones multithread. Por ejemplo, los procesadores de
texto imprimen los documentos sin que por ello uno se tenga que
detener a que termine la impresión, se puede continuar
escribiendo un nuevo texto o continuar el actual. Incluso, los
navegadores de Internet visualizan varias imágenes a la vez,
sin que ello nos pueda parecer nada extraño. Todo esto es
posible gracias a la programación con threads.
¿QUÉ ES UN THREAD?
Un thread (hilo o contexto de
ejecución) es un flujo de control de un programa, esto es, una
secuencia de instrucciones en ejecución. El concepto que
debemos acuñar como thread es similar al de proceso, excepto
que múltiples thread pueden ejecutarse dentro del mismo
proceso, compartiendo los datos y el código de programa. La
mayoría de los programas se pueden considerar como de thread único,
es decir, existe un único camino de ejecución, el
programa tiene un inicio, se realizan una serie de cálculos y
se finaliza. Los programas multihilo pueden tener varios flujos de
control, varios threads que se desarrollan en diferentes partes del código
ejecutándose "simultáneamente".
En un proceso típico con
varios threads, ninguno, uno o varios hilos pueden están ejecutándose
a la vez. Cuando se dice "a la vez", hay que tener en cuenta
el número de procesadores que tengamos en el ordenador sobre el
que se ejecuta el proceso. Una máquina con <n>
procesadores puede ejecutar hasta <n> threads en paralelo. Si únicamente
se dispone de un procesador, se podrán ejecutar todos los
thread del proceso compartiendo el tiempo que se le ha adjudicado a
cada proceso, de forma que aparentemente todos los thread se están
ejecutando simultáneamente.
CARACTERÍSTICAS DE LOS
HILOS
Como ya hemos comentado, el uso de
hilos en los programas permite la ejecución simultánea
de varias tareas dentro de un mismo programa, además una mejora
en rendimiento del mismo. Todo esto es posible gracias a las características
de los threads, que se describen a continuación:
- Los threads de un proceso
comparten el código que se ejecuta, luego sólo hay una
porción de código ocupando memoria, siendo además
una ocupación mínima. Además el trasiego de
threads que tendrá que hacer el sistema operativo será
escaso, entendiendo como tal el cambio que deberá realizar el
sistema para colocar un proceso en ejecución y colocar el que
se está ejecutando en espera. Este proceso se denomina cambio
de contexto.
- Cada thread tiene su propia pila
y sus propios valores para sus variables, por lo que se define su
estado de ejecución, así como el flujo de control.
- Permiten un sencillo manejo de la
memoria compartida, tanto en lo referente a datos como a variables,
así como una forma práctica de comunicación
entre ellos.
El uso de threads en un programa
permite la ejecución "simultánea" de
diferentes tareas.
Las principales ventajas del uso de
hilos frente a los procesos son que el cambio de contexto entre dos
hilos es mucho más "barato" que entre dos procesos,
ya que por ejemplo, el código de los diferentes threads como se
ha comentado anteriormente es compartido. Además los hilos únicamente
se diferencian entre sí por los valores de la pila y variable
de cada hilo, y el tratamiento de memoria compartida resulta más
sencillo y práctico.
Pero también la programación
con threads tiene sus inconvenientes, siendo el principal debido a que
durante el desarrollo de aplicaciones, resulta complicado el proceso
de depuración. Esto es debido a que existen varios flujos de
control que no se reproducen en el mismo orden debido a la asignación
de tiempos (no olvidemos que aunque depuramos un código de
programa único estamos tratando con varios flujos, varias
ejecuciones simultáneas), y las herramientas que actualmente
están disponibles no ofrecen muchas facilidades. Por lo tanto,
programación con threads requiere cierto manejo, experiencia y
paciencia, que como no, se ve recompensada por los resultados finales.
UN EJEMPLO PRÁCTICO
Un clásico de la programación
tradicional es el conocido HelloWorld, como programa que visualiza ese
mismo mensaje. Ese programa a su vez puede ser ejecutado mediante un
thread como se detalla a continuación:
public class mensaje extends Thread {
public void run() {
System.out.println ( " Hello World ");
}
public static void main ( String args[] ) {
mensaje miHilo = new mensaje ();
miHilo.start();
}
}
CREACIÓN DE THREAD
Para crear un nuevo
thread es necesario generar una instancia de la clase java.lang.Thread.
Un objeto de este tipo representa un verdadero hilo en ejecución
para el intérprete de Java y es utilizado para el control y
sincronización durante su aplicación. Mediante este
objeto, podemos dormir el hilo, arrancarlo, suspenderlo, etc. Cabría
esperar que el constructor de la clase java.lang.Thread sólo
necesitase saber por dónde debería comenzar el hilo a
ejecutarse, pero desafortunadamente no es así, por lo que para
crear un thread se presentan dos alternativas posibles que vamos a
exponer a continuación:
- Declarar el hilo con una clase
extendida de la clase java.lang.Thread, mediante la herencia.
- Implementar la interfaz
java.lang.Runnable.
IMPLEMENTACIÓN FRENTE
A HERENCIA La
implementación de la interfaz java.lang.Runnable es la forma
habitual de crear threads, ya que proporciona una forma de abstracción
a la hora de definir una clase. Pero antes de continuar con la
implementación de un thread debemos recordar unas cuántas
diferencias entre una interfaz y una clase. Una
interfaz sólo puede contener métodos abstractos, además
de variables estáticas y finales. Las clases, por otra parte,
tienen la posibilidad de implementar métodos y contener variables
que no sean constantes. Una
interfaz no puede implementar cualquier método. Una clase que
implemente una interfaz debe implementar todos los métodos
definidos en esa interfaz. Además una interfaz tiene la capacidad
de heredar de otras interfaces y al contrario que las clases, puede
heredar de múltiples de ellas. Para
finalizar, una interfaz no puede ser instancia del operador new. Recordando
esto, un objeto que desee utilizar los métodos que proporciona la
clase java.lang.Thread se puede declarar mediante la implementación
de la interfaz java.lang.Runnable, que simplemente se define de la
siguiente manera descrita:
public interface Runnable {
abstract public void run();
}
Con esto, el inicio de cada hilo
quedaría definido mediante la ejecución del método
run() para cada objeto creado, que contendrá el código que
se va a ejecutar. Se debe destacar que run() es un método
publico, no acepta parámetros y no lanza ninguna excepción.
Por tanto, cualquier clase que contenga un método run() con las
características anteriores, simplemente debe implementar la
interfaz java.lang.Runnable. Cada objeto de esta clase sería
Runnable y por tanto se podrá utilizar con la clase
java.lang.Thread.
Una vez mostrados los
fundamentos, pasemos a la parte práctica en la que se aprecia la
sencillez de este método para generar hilos. Como se ha descrito,
la clase debe implementar la interfaz Runnable, por lo que para empezar,
nuestra clase debe tener la siguiente definición:
public class miThread implements Runnable{ }
Bien, nuestro hilo empieza a
ejecutarse en el método run() de la interfaz que estamos
implementando, por lo que añadiremos a la clase:
public class miThread implements Runnable {
public void run () {
// Código del hilo que ejecutar una vez creado
}
}
A partir de este momento ya hemos
implementado la interfaz y el método, luego ya solamente queda
poder utilizar nuestra clase como un verdadero objeto tipo de tipo
Thread. Por lo tanto, generaremos un objeto tipo java.lang.Thread y le
pasaremos un objeto del tipo de nuestra clase, de la forma mostrada en
el ejemplo: miThread miHilo = new miThread ();
new Thread( miHilo ).start();
A partir de este momento ya hemos
implementado la interfaz y el método, luego ya solamente queda
poder utilizar nuestra clase como un verdadero objeto tipo de tipo
Thread. Por lo tanto, generaremos un objeto tipo java.lang.Thread y le
pasaremos un objeto del tipo de nuestra clase, de la forma mostrada en
el ejemplo:
Es posible indicar que comience la
ejecución de nuestro hilo mediante el uso de los métodos
que proporciona la clase java.lang.Thread, ya que hemos generado un
objeto de esta última clase pasando como argumento nuestro hilo
a ejecutar.
Como se observa el método que
se utiliza es start(). Cabe esperar que fuera run(), pues es el método
que tiene nuestro código a ejecutar, pero no es así, el
método start() es el encargado de llamar a nuestro método
run(). Esto se debe a que el método start() de
java.lang.Thread() arranca el objeto tipo Thread como tal, y no como
un método, lo que implica a continuación la llamada al método
run() de nuestra clase. En la figura 1 se puede observar gráficamente
este proceso.
HERENCIA
El otro método para la creación
de hilos consiste en heredar un objeto de la clase java.lang.Thread.
Se trata de una forma más sencilla que la anterior, ya que la
propia clase java.lang.Thread contiene el método run() que
ejecuta nuestro código. Por tanto, para que nuestra clase pueda
ser ejecutada como un hilo, heredamos de la clase java.lang.Thread y
escribimos nuestro código dentro del procedimiento run(), que
tiene las mismas características que el método run() de
la interfaz Runnable. Siguiendo los pasos descritos, seremos capaces
de generar un hilo heredando de la clase java.lang.Thread, tendremos: class miHilo extends Thread {
}
Como la clase java.lang.Thread
consta del método run() entonces sólo tenemos que
redefinir nuestro método run() :
class miHilo extends Thread {
public void run () {
// Código a ejecutar
}
}
La creación de nuestro hilo y
su ejecución sería de la forma siguiente:
miHilo mio = new miHilo(); mio.start();
Como se observa, la ejecución
también se realiza con la llamada al método start() tal
y como se explicó en apartados anteriores.
La implementación de los
threads mediante este método es muy interesante pero, por
ejemplo, si se quiere crear un thread que herede de otra clase que no
sea java.lang.Thread, como por ejemplo, Applet, no se permite. Ésto
restringe considerablemente el empleo su empleo.
De los dos métodos detallados
en el artículo, conceptualmente heredar de java.lang.Thread
parece la mejor solución porque el objeto ya contiene el método
run() y ya es de por sí (por la herencia) de la clase
java.lang.Thread. Por tanto, se suele heredar de la clase
java.lang.Thread cuando es posible (si nuestra clase no hereda de
otras, debido a que Java no permite la herencia múltiple). Por
otra parte, utilizaremos la interfaz Runnable cuando nuestra clase
herede de otras, de tal forma que es el método más comúnmente
utilizado.
ESTADOS DE UN THREAD
Durante el ciclo de vida de un
thread, éste se puede encontrar en diferentes estados, los
cuales pueden apreciarse en la figura siguiente. En ella aparecen
dichos estados y los métodos que provocan el paso de uno a
otro. Este diagrama no es una máquina de estados finita, pero
es lo que más se aproxima al funcionamiento de un thread, que
como se aprecia es muy similar al proceso clásico.
Antes de pasar a detallar los
estados de un thread cabe destacar que los threads son objetos que
tienen como atributo predefinido su estado de ejecución. Éste
no es directamente accesible como es habitual en un atributo de
cualquier objeto, sino que su estado cambia a través de los métodos
disponibles por la clase java.lang.Thread.
CREACIÓN
La creación del thread se
produce mediante los procedimientos anteriormente descritos, pero como
se ha podido observar, su creación no implica de manera automática
su ejecución, su arranque, sino que el thread pasa al estado de
creado. Por ejemplo, suponiendo que creamos una clase miClaseThread
que hereda de java.lang.Thread, tendríamos:
MiClaseThread miHilo = new miClaseThread();
También podríamos
haber hecho:
Thread miHilo = new miClaseThread();
(Se entiende que miClaseThread
hereda de java.lang.Thread y como éste es un objeto también
de tipo java.lang.Thread la sentencia es correcta).
Cuando un thread se encuentra en
este estado es simplemente un objeto de tipo java.lang.Thread, al que
no se le asignan más recursos que la memoria necesaria. El
thread esta aún sin activar, y sólo se puede arrancar
usando el método start() o deternerlo definitivamente mediante
el método stop(). En esta situación llamar a cualquier
otro método carece de sentido y lo único que se provocaría
sería el lanzamiento de una excepción de tipo
IllegalThreadStateException.
ACTIVO O EJECUTABLE
Con la llamada al método
start() se crearán los recursos necesarios para la que el
thread pase al estado de activación, se incorporará a la
lista de procesos disponibles para ser ejecutados por el sistema y se
llamará al método run() de nuestro thread. En este
momento el thread estará activo. Siguiendo la creación
del ejemplo anterior, tendríamos: miHilo.start();
Se denomina en estado activo y no "en
ejecución" porque no se puede decir que el thread se esté
ejecutando permanentemente en nuestro ordenador, es decir, que su
estado sea activo no quiere decir que sé este ejecutando,
aunque sí se encuentra en disposición de hacerlo.
En aquellos ordenadores con un único
procesador no se pueden ejecutar n threads a la vez, por lo que para
simular esta situación Java implementa un tipo de lista de
prioridades que permite que el procesador sea compartido entre todos
los threads que se encuentran en la lista. Gracias a la rapidez del
procesador aparentemente para nosotros es como si se estuvieran
ejecutando todos a la vez. Por tanto, un thread activo es aquel al que
el procesador asigna regularmente un lapso o perodo de tiempo.
Cuando el thread se encuentre en
este estado y se le asigne una porción de tiempo de procesador,
todas las instrucciones de código que estén dentro del
procedimiento run() serán ejecutadas.
DORMIDO
Un thread se encuentra en este
estado si se llama al método suspend(), cuando se invoca el método
sleep(), si el thread esta bloqueado por la espera de unos datos de la
entrada/salida o cuando se realiza una llamada al método wait()
para esperar a que se cumpla una determinada condición. En
cualquiera de estas circunstancias el thread no consume recursos de la
máquina.
Los métodos de recuperación
o vuelta al estado activo dependerán, como es razonable, de la
forma en que haya llegado al estado de dormido, siendo cada uno de
ellos una forma exclusiva, por lo que tendremos:
- Si el thread está dormido
por un periodo de tiempo, volverá al estado de activo al
finalizarse dicho período.
- Si un thread esta suspendido
mediante suspend(), se activa si se realiza la llamada al método
resume ().
- Si el thread esta bloqueado por
una entrada/salida, cuando ésta se resuelva, automáticamente
se volverá al estado activo.
- Si el thread espera por una
condición (invocación wait()), cada vez que la
variable que controla la condición varíe debe llamarse
a notify() o notifyAll().
Por ejemplo, un thread puede estar
esperando una entrada/salida porque se requiera una entrada por
teclado, con una sentencia del estilo: System.in.read();
En nuestro procedimiento run()
dormiría el hilo hasta que el usuario introdujera un dato por
el teclado.
También podemos provocar la
espera de un thread mediante el uso del método sleep(), por
ejemplo:
class mensaje extends Thread {
public void run () {
System.out.println
("Me voy a dormir");
try {
sleep ( 20*1000 );
//tiempo en milisegundos
}
catch ( Exception e) {
System.out.println
("Que sueño mas raro
");
}
}
}
La línea de código
sleep (20*1000) provoca que el hilo "duerma" durante veinte
segundos. Durante ese tiempo transcurrido, aunque el procesador
estuviera sin utilizar el thread no se ejecutaría. Tras esos
segundos volverá al estado activo y entonces el procesador si
le adjudicaría un lapso de tiempo cuando llegase su turno.
Este método tiene muchas
aplicaciones, por ejemplo, si se quiere que el thread se active según
una frecuencia de tiempo, o se lo que se desea es que el resto de
hilos tengan más tiempo de ejecución, etc.
Cabe otra posibilidad es que el
thread pase a dormido mediante suspend(), por lo que estaría en
este estado hasta que no se ejecutara el método resume(). Por
ejemplo:
class mensaje extends Thread {
public void run () {
while ( true ){
System.out.println
( "hola hola");
}
}
public static void main ( String args[] ) {
mensaje miHilo = new mensaje();
miHilo.start();
System.out.println
(" Veo muchos hola hola
");
System.out.println
(" No me acaba de gustar ");
System.out.println
(" Lo siento, los paro
");
miHilo.suspend();
System.out.println
(" Que tranquilidad
");
System.out.println
(" Bueno, no están tan mal
");
miHilo.resume();
}
}
El programa mezclará los
mensajes "hola hola" del thread, junto con el resto de
mensajes, pero cuando se ejecute el método suspend() sólo
aparecerá "Que tranquilidad
" sin mezclarse con
los del thread. Al llamarse al método resume() el thread vuelve
a activo y continuará visualizando infinitos "hola hola".
MUERTO
El último estado en que puede
estar un thread es el de muerto. A él se puede llegar de dos
formas diferentes tal y como detallamos:
- Debido a una ejecución
normal o debido al empleo del método run().
- Por la ejecución del método
stop().
En el primer caso, el de la ejecución
normal, la finalización proviene debido a que la tarea que se
quería realizar ha finalizado,y se ha llegado al final del método
run(). Por ejemplo:
public void run() {
int veces ;
for ( veces = 0 ; veces < 10 ; veces ++ ){
System.out.println (" Turno " + veces );
}
}
Cuando se haya visualizado el número
9 el método run() finaliza, por lo que el hilo también
termina.
También se puede finalizar un
thread llamadon al método stop().
Si en el ejemplo de la clase
mensajes se eliminan las llamadas a suspend() y resume(), y se cambian
por un stop(), quedaría de la forma siguiente:
System.out.println
(" Lo siento, los paro
");
// Eliminamos miHilo.suspend();
System.out.println
(" Que tranquilidad
");
System.out.println
(" Bueno, no están tan mal
");
// Eliminamos miHilo.resume();
miHilo.stop();
Al ejecutarse el método
stop() se finalizaría el hilo, y no aparecerían más
mensajes "hola hola" por pantalla. El hilo ha concluido de
forma definitiva. Este método se encarga de enviar un objeto de
tipo ThreadDeath al hilo que quiere detener, por lo que el thread
morirá cuando reciba esta excepción.
Aparte de todos estos métodos
descritos, existe un método que posibilita conocer en qué
estado se encuentra un thread. Se trata del método isAlive().
Devuelve un valor lógico para diferenciar si el proceso se
encuentra activo o dormido, por lo que obtenemos verdadero o falso si
el proceso no ha sido creado o si está muerto.
Una vez que conocemos los estados de
un thread y los métodos asociados, vamos a desarrollar un
ejemplo en que se pueda apreciar la importancia de la programación
con threads.
EJEMPLO PRÁCTICO
En el ejemplo vamos a simular una
carrera entre una liebre, una tortuga y un caracol, de forma que cada
uno será un objeto de una clase "animal" que tendrá
la velocidad de cada animal y su nombre. Cada objeto de la clase
Animal será un thread que dormirá en función de
su velocidad, de tal forma que, por ejemplo, el thread asociado a la
liebre dormirá menos que el del caracol.
class fabula {
static Animal liebre ;
static Animal tortuga;
static Animal caracol ;
public static void main( String args[] )
throws InterruptedException {
liebre = new Animal ( 5, "L" );
tortuga = new Animal ( 3, "T" );
caracol = new Animal ( 1, "C" );
liebre.start();
tortuga.start();
caracol.start();
//El método join() consigue que hasta que no han
//finalizado todos hilos la carrera no se acabe
liebre.join();
tortuga.join();
caracol.join();
}
}
class Animal extends Thread{
String nombre ;
int velocidad ;
public Animal ( int velocidad, String nombre ){
this.nombre = nombre;
this.velocidad = velocidad ;
}
public void run (){
for ( int i = 0 ; i < 10 ; i ++ ) {
System.out.print ( nombre );
try {
sleep ( 1000 / velocidad );
}
catch ( Exception e ) {
/* Nada */ ;
}
}
System.out.println
( " \t " + nombre + " llegó " );
}
}
Como se observa, la L (liebre) al
ser más rápida llega antes que los demás a la
meta, tras ella la tortuga y finalmente el caracol. Con este ejemplo
se ha comprobado la creación de varios hilos mediante el método
de la herencia, su ejecución, el estado de dormido mediante el
método sleep() y la muerte natural tras finalizar el método
run(). Es un buen ejercicio realizar este mismo programa sin
programación con threads.
PRIORIDADES DE LOS THREADS
Java garantiza de cierta manera la
gestión de los threads en cuanto al nivel de prioridad asignado
a cada uno de ellos, de tal forma que cada hilo tiene una prioridad
asociada. Si en un momento determinado un thread con prioridad mayor
que el thread actual pasa a estado de activo, éste pasa a
ejecutarse y el actual será dormido. Dicho de otra forma,
mientras el thread de mayor prioridad no esté dormido, es él
el que contará con los recursos. Si se duerme, se pasa a los
otros hilos la posibilidad de utilizar los recursos, pero si vuelve a
pasar a activo el thread de mayor prioridad, éste recupera de
nuevo los recursos. Por defecto los threads con idéntica
prioridad se ejecutan con el sistema del round robin, el cual define
que si un thread comienza a ejecutarse, lo seguirá haciendo
hasta que ocurra una de las siguientes circunstancias:
- Se llame al método sleep()
o wait().
- Se espere una entrada/salida.
- Si se ejecuta un método
sincronizado.
- Se llame al método
yield().
- Se invoque el método
stop().
Gráficamente este método
podría describirse tal y como muestra el esquema de la figura
3.
De todos modos, Java deja ciertos
aspectos del cambio de thread a la implementación que estemos
utilizando. El principal problema de este asunto es que no todas las
implementaciones de Java usan el mismo sistema, ya que por ejemplo el
entorno Windows utiliza la técnica de time slicing (fragmento
de tiempo) para los threads con la misma prioridad. Con este método,
el tiempo de procesamiento de cada thread viene determinado de forma
que cada hilo se ejecuta durante un período de tiempo hasta que
llega el cambio de contexto, esto es, se le termina el tiempo de
procesador, y se ejecuta otro hilo, aunque se siguen manteniendo las
mismas reglas para los thread con mayor prioridad. Debido a que Java
no garantiza la fragmentación de tiempo, todas las aplicaciones
que se diseñen deben tratarse como si se fueran a ejecutar con
el sistema de round robin.
TRATAMIENTO DE LAS PRIORIDADES
La prioridad por defecto de un
thread está definida por la variable estática
Thread.NORM_PRIORITY que tiene un valor asignado de 5.Existen otras
dos variables estáticas que son Thread.MIN_PRIORITY que está
fijada a 1 (menor prioridad) y finalmente Thread.MAX_PRIORITY que
tiene valor 10 (de mayor prioridad).
El método disponible para
asignar la prioridad de un thread es setPriority(), el cual recibe
como parámetro el valor con la prioridad para el thread, por
tanto, un número entre uno y diez. Si se necesita cambiar la
prioridad, normalmente se incrementa en uno o dos los valores de los
estándares definidos anteriormente, de forma que un pequeño
incremento de prioridad es suficiente para nuestros objetivos. Por
ejemplo, especificar NORMPRIORITY+1 es más que suficiente para
reflejar un cambio de prioridad entre los hilos.
También puede darse la
circunstancia de que necesitemos saber qué prioridad tiene un
thread en un momento determinado. Para ello se dispone del método
getPriority() que devuelve un número entero con el valor actual
de prioridad para el thread en cuestión.
Pasemos en este momento a un ejemplo
para comprobar el uso de las prioridades mediante el que se comprueba
el uso del cambio, gracias a setPriority y así como el
funcionamiento del método round robin cuando hay dos hilos con
la misma prioridad.
class mihilo extends Thread
{
String mensaje;
mihilo ( String cadena) {
mensaje = cadena ;
}
public void run() {
while ( true ) {
System.out.print ( mensaje );
}
}
public static void main ( String args[] ) {
new mihilo("K1 ").start();
new mihilo("K2 ").start();
mihilo obj2 = new mihilo("L ");
obj2.setPriority
( Thread.NORM_PRIORITY+1); obj2.start(); }
}
Dependiendo del sistema en el que se
ejecute el programa, podremos observar como resultado una salida u
otra. De todos modos, cabe esperar que el hilo al que se le asigna
mayor prioridad se ejecute bastantes más veces que los otros
dos, ya que hasta que éste no se bloquea, pasa a dormido u otra
condición no cede los recursos a los otros dos hilos de igual
prioridad. Además cuando vuelva a requerir recursos los obtendrá
con prioridad sobre los dos threads citados.
Los dos hilos de igual prioridad en
el caso de que se aplique la fragmentación de tiempo se irán
alternando en aparición, y en caso de tengamos round robin,
aparecerán aleatoriamente pero no tendrán apariciones
simultáneas como define el sistema.
La solución para el problema
de que los thread de misma prioridad no compartan los recursos queda
resuelta mediante el uso del método yield(), esto es, el thread
voluntariamente cede su tiempo a otro thread, por lo que obtendremos
un reparto homogéneo de los recursos entre los diferentes
threads.
Volviendo al ejemplo anterior, añadiremos
yield() a nuestro thread, de forma que obtendremos:
public void run() {
while ( true ) {
System.out.print
( mensaje ); yield();
}
}
Ahora se observará que los
hilos de igual prioridad aparecen alternándose el uno con el
otro, cosa que no ocurriría anteriormente en implementaciones
con round robin. Por ello, se recomienda siempre el uso de yield()
para todos aquellos casos en que los threads tengan la misma prioridad
y casi imprescindible en aquellos que realicen cálculos
intensivos que requieren mucho recurso de procesador, por lo que se
debería buscar un buen lugar para situar la llamada yield().
Un ejercicio interesante consiste en
modificar el programa Fábula que simula una carrera entre la
liebre, la tortuga y el caracol, asignando diferentes prioridades con
setPriority() y situando el método yield() en diferentes
lugares.
CONCLUSIONES
Como se ha podido comprobar a lo
largo del artículo, el uso del lenguaje Java permite una gran
flexibilidad a la hora de programar con threads.
La simplicidad para crear, ejecutar
y controlar los threads permite escribir potentes aplicaciones que de
otra forma serían difíciles de implementar.
En el próximo artículo
comprobaremos los métodos para sincronizar threads, el uso de
datos compartidos y cómo generar applets Java utilizando esta técnica
entre otras muchas cosas. |