Professional Documents
Culture Documents
Trabajo Teórico
Mayo, 2004
Multihilo en Java
Abstract
In this document the basic details for working with threads in Java are presented. It only intent
to be an introduction to this specific subject, we only try to comment the operations that we can
realize with them and explain some details of them implementation.
This operations decides the different states of the life’s cycle that a thread does, some ways
of synchronization for preventing turnout problems are also explain, and finally thread’s
priorities for that threads are executed in a fixed order.
1. Introducción ______________________________________________ 1
9. Comentario_______________________________________________ 21
2. Multitarea y multihilo
Muchos entornos tienen la llamada multitarea en su sistema operativo, esto es distinto al
multihilo. En un sistema operativo multitarea a las tareas se les llama procesos pesados,
mientras que en un entorno multihilo se les denomina procesos ligeros o hilos, la diferencia es
que los procesos pesados están en espacios de direccionamiento distintos y por ello la
comunicación entre procesos y el cambio de contexto es caro. Por el contrario, los hilos
comparten el mismo espacio de direcciones y comparten cooperativamente el mismo proceso
pesado, cada hilo guarda su propia pila, variables locales y contador de programa, por ello, la
comunicación entre hilos es muy ligera y la conmutación de contexto muy rápida.
En la Figura 1 observamos cómo sobre una CPU pueden estar ejecutándose distintos
procesos pesados y uno de ellos puede estar compuesto por distintos flujos de ejecución
(threads o hebras o hilos). Estos threads tendrán que “disputarse” el tiempo de ejecución que el
sistema operativo le dé al proceso en el que residen.
-1-
Multihilo Java
Aquí, cuando se llama a main(), la aplicación imprime el mensaje y termina. Esto ocurre
dentro de un único thread, un único hilo de ejecución.
3. Creación de threads
Lo primero que hay que realizar para poder utilizar un thread es crearlo. El programador puede
elegir cualquiera de las dos posibilidades que proporciona Java, y que como explica Pedro
Manuel Cuenca en su libro [1], se describen a continuación.
En la Figura 2 se puede observar una de las posibles salidas por pantalla que se pueden
obtener al ejecutar este programa.
Esto imprimiría:
Hilo 1
p1 = 4
p2 = 5
Hilo 2
p1 = 7
p2 = 9
¾ yield(): este método libera el procesador para que pueda ser utilizado por otros
threads. Esto no significa que el thread se detenga definitivamente, sino que
simplemente indica al planificador que pase a ejecutar otro thread. El thread que invocó
a yield() se volverá a ejecutar cuando le toque el turno de nuevo.
¾ join(): este método detiene el hilo actual hasta que termine el hilo sobre el que se
llama join(). Es usado por tanto para que unos hilos esperen a la finalización de
otros.
Se acaban de explicar los métodos de clase y métodos de instancia más interesantes dentro
del ámbito de este trabajo. Pero existen muchos más y se pueden consultar en la página web de
Sun [2].
Cuando un hilo está en este estado, es simplemente un objeto Thread vacío. El sistema no
ha destinado ningún recurso para él, únicamente memoria. Desde este estado solamente puede
arrancarse llamando al método start(), o detenerse definitivamente, llamando al método
stop(). La llamada a cualquier otro método carece de sentido y lo único que provocará será la
generación de una excepción de tipo IllegalThreadStateException.
5.4.2 Ejecutable
Obsérvense las dos líneas de código siguientes:
La llamada al método start() reservará los recursos del sistema necesarios para que el
hilo pueda ejecutarse, lo incorpora a la lista de procesos disponibles para ejecución del sistema y
llama al método run() del hilo de ejecución. En este momento se encuentra en el estado
Ejecutable del diagrama. Y este estado es Ejecutable y no En Ejecución, porque cuando el hilo
está aquí no significa que esté corriendo. Muchos ordenadores tienen solamente un procesador
lo que hace imposible que todos los hilos estén corriendo al mismo tiempo. Java implementa un
tipo de scheduling o lista de procesos, que permite que el procesador sea compartido entre todos
los procesos o hilos que se encuentran en la lista.
Cuando el hilo se encuentra en este estado, todas las instrucciones de código que se
encuentren dentro del bloque declarado para el método run(), se ejecutarán secuencialmente.
5.4.3 Parado
Un hilo de ejecución entra en estado Parado cuando alguien llama al método suspend(),
cuando se llama al método sleep(), cuando el hilo está bloqueado en un proceso de
entrada/salida o cuando el hilo utiliza su método wait() para esperar a que se cumpla una
determinada condición.
Por ejemplo, en el trozo de código siguiente:
Thread MiThread = new MiClaseThread();
MiThread.start();
try {
MiThread.sleep( 10000 );
} catch( InterruptedException e ) {}
la línea de código que llama al método sleep() hace que el hilo se duerma durante 10
segundos. Durante ese tiempo, incluso aunque el procesador estuviese totalmente libre,
MiThread no correría. Después de esos 10 segundos MiThread volvería a estar en estado
Ejecutable y ahora sí que el procesador podría hacerle caso cuando se encuentre disponible.
Los métodos de recuperación del estado Ejecutable, en función de la forma de llegar al
estado Parado del hilo, son los siguientes:
• Si un hilo está dormido, pasado el tiempo especificado en el sleep().
• Si un hilo de ejecución está suspendido, después de una llamada a su método
resume().
• Si un hilo está bloqueado en una entrada/salida, una vez que el comando de
entrada/salida concluya su ejecución.
• Si un hilo está esperando por una condición, cada vez que la variable que controla esa
condición varíe debe llamarse al método notify() o notifyAll() .
5.4.4 Muerto
Un hilo de ejecución se puede morir de dos formas: por causas naturales o porque lo maten (con
stop()). Un hilo muere normalmente cuando concluye su método run().
Por ejemplo, en el siguiente trozo de código:
un hilo morirá de forma natural después de que se complete el bucle y run() concluya.
También se puede matar en cualquier momento un hilo, invocando a su método stop(),
en el trozo de código siguiente:
Thread MiThread = new MiClaseThread();
MiThread.start();
try {
MiThread.sleep( 10000 );
} catch( InterruptedException e ) {}
MiThread.stop();
6. Sincronización
La sincronización surge debido a la necesidad de evitar que dos o más threads traten de acceder
a los mismos recursos al mismo tiempo. Por ejemplo si un thread tratara de escribir en un
fichero y otro thread estuviera al mismo tiempo tratando de borrar dicho fichero, se produciría
una situación no deseada. También habría que sincronizar hilos cuando un thread debe esperar a
que estén preparados los datos que le debe suministrar otro thread. Para solucionar estos tipos
de problemas es importante poder sincronizar los distintos threads.
Las secciones de código de un programa que acceden a un mismo recurso (un mismo
objeto de una clase, un fichero del disco, etc.) desde dos threads distintos se denominan
secciones críticas. Para sincronizar dos o más threads, hay que utilizar el modificador
synchronized en aquellos métodos del objeto o recurso con los que puedan producirse
situaciones conflictivas. De esta forma Java bloquea, asocia un bloqueo o lock, el recurso
sincronizado. Por ejemplo:
public synchronized void metodoSincronizado() {
...// código de la sección critica
}
Todos los métodos que accedan a un recurso compartido deben ser declarados
synchronized. De esta forma, si algún método accede a un determinado recurso Java
bloquea dicho recurso, así logramos que el resto de threads no puedan acceder al mismo hasta
que el primero en acceder termine de realizar su tarea. Bloquear un recurso u objeto significa
que sobre ese objeto no pueden actuar simultáneamente dos métodos sincronizados.
En la sincronización se pueden utilizar dos niveles de bloqueo de un recurso, el primero es
a nivel de objetos, mientras que el segundo es a nivel de clases. El primero se consigue
declarando todos los métodos de una clase como synchronized. Cuando se ejecuta un
método synchronized sobre un objeto concreto, el sistema bloquea dicho objeto, de forma
que si otro thread intenta ejecutar algún método sincronizado de ese objeto, este segundo
método se mantendrá a la espera hasta que finalice el anterior y desbloquee por lo tanto el
objeto. Si existen varios objetos de una misma clase, como los bloqueos se producen a nivel de
objeto, es posible tener distintos threads ejecutando métodos sobre diversos objetos de una
misma clase.
El bloqueo de recursos a nivel de clases se corresponde con los métodos de clase o
static, y por lo tanto con las variables de clase o static. Si lo que se desea es conseguir
que un método bloquee simultáneamente una clase entera, es decir, todos los objetos creados de
una clase, será necesario declarar este método como synchronized static. Durante la
ejecución de un método declarado de esta segunda forma ningún método sincronizado tendrá
acceso a ningún objeto de la clase bloqueada.
La sincronización puede ser problemática y generar errores. Un thread podría bloquear un
determinado recurso de forma indefinida impidiendo que el resto de threads accedieran al
mismo, para evitar esto habrá que utilizar la sincronización sólo donde sea estrictamente
necesario.
Hay que tener presente que si dentro de un método sincronizado se utiliza el método
sleep() de la clase Thread, el objeto bloqueado permanecerá dormido durante el tiempo
indicado en el argumento de dicho método. Esto implica que otros threads no podrán acceder a
ese objeto durante ese tiempo, aunque en realidad no exista peligro de simultaneidad ya que
durante ese tiempo el thread que mantiene bloqueado el objeto no realizará cambios. Para
evitarlo es conveniente sustituir sleep() por el método wait(). Cuando se llama al método
wait(), que siempre debe hacerse desde un método o bloque synchronized, se libera el
bloqueo del objeto y por lo tanto es posible continuar utilizando ese objeto a través de métodos
sincronizados. El método wait() detiene el thread hasta que se llame al método notify() o
notifyAll() del objeto, o finalice el tiempo indicado como argumento del método wait().
El método unObjeto.notify() lanza una señal indicando al sistema que puede activar uno
de los threads que se encuentren bloqueados esperando para acceder al objeto unObjeto. El
método notifyAll() lanza una señal a todos los threads que están esperando la liberación
del objeto.
Los métodos notify() y notifyAll() deben ser llamados desde el thread que tiene
bloqueado el objeto para activar el resto de threads que están esperando la liberación de un
objeto. Un thread se convierte en propietario del bloqueo de un objeto ejecutando un método
sincronizado del objeto.
Observar las dos funciones siguientes, en las que put() inserta un dato y get() lo
recoge:
debido a que se ha podido acceder al objeto con el método b() al ser el thread que ejecuta el
método a() propietario con anterioridad del bloqueo del objeto.
El proceso de sincronización lleva bastante tiempo a la CPU, por ello se debe minimizar su
uso ya que el programa será más lento cuanta más sincronización incorpore.
6.1. Ejemplo
EL PROBLEMA DE LOS FILÓSOFOS COMILONES.
Cinco filósofos pasan su vida pensando y comiendo. Los filósofos comparten una mesa
circular rodeada por cinco sillas, una para cada uno de ellos. En el centro de la mesa se
encuentra una fuente de arroz, y también sobre la mesa hay cinco palillos chinos. Cuando un
filósofo piensa, no interactúa con sus colegas. Ocasionalmente, un filósofo tiene hambre y trata
de coger los dos palillos que están más cerca de él (los palillos colocados entre él y sus vecinos
de la derecha y de la izquierda). Un filósofo sólo puede coger un palillo a la vez y, obviamente,
no puede ser el que está en la mano de un vecino. Cuando un filósofo hambriento tiene sus dos
palillos al mismo tiempo, come sin soltarlos. Cuando termina de comer, coloca ambos palillos
sobre la mesa y comienza a pensar otra vez.
La situación de los Filósofos comelones se puede observar en la Figura 4.
class semAforo
{
private int valor=1;
} catch (InterruptedException e)
{}
System.out.println("<"+this.number+"> Estoy
pensando");
palillo_izq.incrementar();
palillo_der.incrementar();
try {
dormir=(alea.nextInt(4))+5;
sleep(dormir*1000);
} catch (InterruptedException e)
{}
}
}
}
class pensadores2 extends Thread
{
private int number;
private semAforo palillo_izq;
private semAforo palillo_der;
long dormir;
Random alea=new Random(2);
sleep(dormir*1000);
} catch (InterruptedException e)
{}
}
}
}
class probar
{
public static void main(String args[])
{
semAforo palillo1=new semAforo();
semAforo palillo2=new semAforo();
semAforo palillo3=new semAforo();
semAforo palillo4=new semAforo();
semAforo palillo5=new semAforo();
try
{
p1.start();
p2.start();
p3.start();
p4.start();
p5.start();
p1.join();
p2.join();
p3.join();
p4.join();
p5.join();
}catch(java.lang.InterruptedException ie)
{
System.out.println(ie);
}
}
}
En la Figura 5 se puede observar una de las posibles salidas por pantalla que se pueden
obtener al ejecutar este programa.
• La clase probar esta constituida por el método principal, es decir, el método main()
que nos sirve para ejecutar el programa y utilizar las clases anteriormente definidas. En
él se instancian varios objetos, de los cuales los de la clase pensadores1 y
pensadores2 llamarán al método start() para iniciar la ejecución de dichos hilos
(implícitamente se llamará también a run()) y se lleven a cabo las operaciones
implementadas, con lo que conseguiremos solucionar el problema. Por último
esperamos por la finalización de los hilos mediante el método join().
8. Grupos de hilos
Cualquier hilo de ejecución en Java debe formar parte de un grupo, la clase ThreadGroup
define e implementa la capacidad de un grupo de hilos.
Los grupos de hilos permiten que sea posible recoger varios hilos de ejecución en un solo
objeto y manipularlo como un grupo, en vez de individualmente. Por ejemplo, se pueden
regenerar los hilos de un grupo mediante una sola sentencia.
Cuando se crea un nuevo hilo, se coloca en un grupo, bien indicándolo explícitamente, o
bien dejando que el sistema lo coloque en el grupo por defecto. Una vez creado el hilo y
asignado a un grupo, ya no se podrá cambiar a otro grupo.
Si no se especifica un grupo en el constructor, el sistema coloca el hilo en el mismo grupo
en que se encuentre el hilo de ejecución que lo haya creado, y si no se especifica el grupo para
ninguno de los hilos, entonces todos serán miembros del grupo main, que es creado por el
sistema cuando arranca la aplicación.
La clase Thread proporciona constructores en los que se puede especificar el grupo del
hilo que se esta creando en el mismo momento de instanciarlo, y también métodos como
getThreadGroup(), que permiten determinar el grupo en que se encuentra un hilo de
ejecución.
9. Comentario
La posibilidad de soportar varios threads nos proporciona una gran flexibilidad para el diseño
de aplicaciones.
La especificación de Java sólo menciona que pueden utilizarse los recursos del sistema
operativo subyacente si éste soporta multithreading, en caso contrario es la maquina virtual
quien realiza todas las labores de planificación y cambio de contexto.
El comportamiento que puede esperar el programador del soporte de multithreading en
Java no permite la realización de aplicaciones con fuertes requisitos temporales. No es posible
tan siquiera cambiar la política de planificación de la maquina virtual, y lo que es peor, ésta
depende de la plataforma en la que nos encontremos. Debido a estos motivos la utilización de
multithreading en Java debe limitarse simplemente a situaciones donde sea conveniente tener
varias tareas en paralelo, sin que importe demasiado la precisión con que se reparten el
procesador o la frecuencia con que realizan cambios de contexto.
Existe un aspecto significativo y no intuitivo dentro de los hilos, y es que debido a la
planificación de los hilos, se puede hacer que una aplicación se ejecute generalmente más rápido
insertando llamadas a sleep() dentro del bucle principal de run(). Debido a esto su uso
parece un arte, sobre todo cuando unos retrasos más largos parecen incrementar el rendimiento.
La razón por la que ocurre esto es que retrasos más breves pueden hacer que la interrupción del
planificador del final de sleep() se dé antes de que el hilo en ejecución este listo para ir a
dormir, forzando así al planificador a detenerlo y volver a arrancarlo más tarde para que pueda
acabar con lo que estaba haciendo, para ir después a dormir.
Las desventajas principales del multihilado son:
1. Ralentización durante la espera por recursos compartidos.
2. Sobrecarga adicional de la CPU necesaria para gestionar los hilos.
3. Complejidad sin recompensa, como la idea poco acertada de tener un hijo separado para
actualizar cada elemento de un array.
4. Problemas derivados como la inanición, la competición y el interbloqueo.
10. Conclusiones
11. Referencias
[1] Pedro Manuel Cuenca Jiménez. “Programación en Java”. Guía Práctica para
programadores. Anaya Multimedia.
12. Bibliografía
• Ed Tittel, Bill Brogden. “Manual Fundamental de Java”. Anaya Multimedia.
• El rincón de Java.
http://wwws.uib.es/rincon/java/ [Última vez visitado, 8-6-2004]