UT2: Programación multihilo
Programación de Servicios y Procesos
DAM
Programación 1. Fundamentos
multihilo 2. Creación y puesta en marcha de
hilos
3. Gestión y planificación de hilos
Índice
4. Comunicación de hilos:
información compartida
5. Sincronización de hilos
1. Fundamentos
1.1. Introducción
Como vimos en la primera unidad, los
procesos se pueden descomponer en otros
subprocesos, como unidades de ejecución
más ligeras, llamadas hilos o threads .
De esta forma, un programa
con un único hilo solo
podemos realizar una Un hilo solo puede existir dentro de un
acción, mientras que con proceso, y no puede ejecutarse por sí solo .
varios hilos podemos Por tanto, un proceso siempre tendrá un hilo
ejecutar varias cosas al principal (main thread), a partir del cual
mismo tiempo, dentro de un pueden surgir otros nuevos hilos, que
mismo proceso. llamamos hilos hijos .
1. Fundamentos
1.2. Recursos de un hilo
Un hilo comparte con el proceso al que Los recursos que son propios de un hilo, y
pertenece todos los recursos indicados en el no son compartidos, son:
PCB:
Compartidos No compartidos
Variables y Atributos
Código Identificador
constantes globales propios
Stack o pila Variables
PID Registros
del proceso locales
Heap o memoria Contador de
dinámica (ficheros, programa
señales) propio
1. Fundamentos
1.2. Recursos de un hilo
El hecho de que los hilos compartan alguna
zona de memoria puede tener algunas ventajas ,
como la facilidad de comunicación, pero
también es necesario tomar precauciones .
En la programación multiproceso , cada
proceso protegía su zona de memoria frente a
los demás, pero en la programación multihilo ,
una corrupción en la zona de memoria de un
hilo provoca errores en todos los demás.
Por tanto, es más necesario que nunca utilizar mecanismos de bloqueo y sincronización entre
hilos a la hora de acceder a estos recursos compartidos, por lo que también se puede hacer más
complejo el proceso de programación y depuración de código.
1. Fundamentos
1.3. Ventajas del uso de hilos
Consumen menos recursos (son más ligeros)
Tardan menos tiempo en crearse y eliminarse
La conmutación entre hilos (Cambio de
Contexto) es más rápida que en los procesos
● Manejar entradas de distintos dispositivos a la vez
¿Cuándo se aconseja ● Implementar múltiples tareas simultáneas
utilizar hilos ● Diferenciar tareas por prioridad
● El sistema tiene varios núcleos o procesadores
2. Creación y puesta en marcha de hilos
2.1. Recursos Paquetes de Java:
[Link] [Link]
Utilidades:
Clase Thread Se encarga de producir los hilos y de su gestión
Permite añadir funcionalidad a los hilos, sin necesidad de
Interfaz Runnable
heredar la clase Thread
Clase Maneja y notifica Clase Permite manejar un grupo
Otras:
ThreadDeath errores en hilos ThreadGroup de hilos de forma conjunta
2. Creación y puesta en marcha de hilos
2.1. Recursos Paquetes de Java:
[Link] [Link]
Utilidades:
Clases de
Semaphore, CountDownLatch, CyclicBarrier, Exchanger
sincronización
Interfaces para BlockingQueue, LinkedBlockingQueue, ArrayBlockingQueue,
gestionar colas SynchronousQueue, PriorityBlockingQueue, DelayQueue
AtomicInteger, Permiten usar hilos como Lock, Útiles para la
Otras:
AtomicLong elementos atómicos ReadWriteLock sincronización
2. Creación y puesta en marcha de hilos
2.2. Creación de un hilo
En Java, hay dos formas de crear un
nuevo hilo:
● Extender de la clase Thread
● Implementar la interfaz Runnable
En ambos casos, se debe proporcionar
una funcionalidad, es decir, un código
para que el hilo lo ejecute. Esto se hace
mediante el método run() .
Recuerda que al lanzar un programa en Para saber el nombre del hilo que se está
Java, siempre se va a a ejecutar un hilo ejecutando en un momento determinado, usamos:
principal , que es el método main(). [Link]();
2. Creación y puesta en marcha de hilos
2.2. Creación de un hilo
Los hilos se definen dentro del mismo código (es decir, en el mismo archivo .java) de la clase
principal, que es la que contiene el main(). El hilo se define en una clase, que puede estar
dentro de la clase principal (en cuyo caso deberá ser estática), o fuera de ella. Esta clase será
la que puede extender de Thread o implementar Runnable (más aconsejable lo segundo).
Dentro de esta clase, tendrá un método del tipo public void run() , que contiene el código
con las instrucciones que va a ejecutar el hilo.
public class Principal{
static class Hilo{
public static void main(){
public void run(){
//Código del programa principal
//Código del hilo
//Llamada al hilo
}
}
}
}
2. Creación y puesta en marcha de hilos
2.2. Creación de un hilo
Extender de la clase Thread
1. Crear clase con el código del hilo, extendiendo de 2. Desde el código del main, instanciar la clase del
la clase Thread . Dentro debe tener el método run() : hilo y ejecutarlo:
static class Hilo1 extends Thread{ public static void main(String[] args){
@Override (Esta línea es opcional) Hilo1 nuevoHilo = new Hilo1();
public void run(){ [Link]();
Código que ejecutará el hilo }
}
2. Creación y puesta en marcha de hilos
2.2. Creación de un hilo
Implementar la interfaz Runnable
1. Crear clase con el código del hilo, extendiendo de 2. Desde el código del main, instanciar la clase del
la clase Thread . Dentro debe tener el método run() : hilo en una nueva clase Thread, y ejecutarlo:
static class Ejec1 implements Runnable{ public static void main(String[] args){
@Override (Esta línea es opcional) Ejec1 exe = new Ejec1();
public void run(){ Thread nuevoHilo = new Thread(exe);
Código que ejecutará el hilo [Link]();
} }
}
2. Creación y puesta en marcha de hilos
2.2. Creación de un hilo
Otras consideraciones
● Si llamamos directamente al método [Link](), se ejecutará el código del hilo, pero no como
una nueva entidad independiente, sino dentro del hilo principal, que no es lo que
deseamos .
● Una vez se ha llamado al método [Link]() de un hilo, no se puede volver a llamar para el
mismo hilo; de lo contrario, lanzará un error.
● Algunos métodos nos permiten conocer cuál es el hilo que está ejecutándose
(currentThread), conocer el identificador (ID) del hilo (getID()), o su nombre (getName()).
● El orden de ejecución de los hilos no está determinado, dependerá de cada ejecución.
● Se puede modificar el nombre por defecto del hilo, de las siguientes formas:
Thread Runnable Thread hilo = new
Una vez se ha [Link](); Al instanciar el hilo Thread(runnable, “nombre
instanciado el hilo con la clase Thread hilo”);
2. Creación y puesta en marcha de hilos
2.3. Estados de un hilo
● Nuevo: se ha creado el hilo, pero
aún no está listo para ejecutarse.
● Ejecutable: el hilo está listo para
ejecutarse. Puede estar en espera, es
decir, listo o preparado , o
ejecutándose en ese instante.
● No ejecutable o detenido: no se le
puede asignar un procesador, ya Para obtener el estado de un hilo, se usa:
que está bloqueado o en pausa.
[Link]();
● Muerto o finalizado: el hilo ha
finalizado, ya sea por error o por Para conocer si el proceso está vivo (Ejecutable o
No Ejecutable):
terminar el código del método run.
[Link]();
2. Creación y puesta en marcha de hilos
2.4. Detener temporalmente un hilo
Se puede alcanzar el estado de No ejecutable de
tres formas distintas:
● Dormido: cuando llamamos al método
sleep(), indicando el tiempo que el hilo
debe permanecer detenido.
● Esperando: cuando se llama al método
wait(), el hilo se detiene hasta que otro hilo
lo despierta con notify() o notifyAll().
● Bloqueado: al igual que ocurría con los Cuando un hilo sale del estado No ejecutable ,
vuelve de nuevo a Ejecutable , igual que los
procesos, un hilo puede bloquearse para
procesos volvían a la cola. La diferencia es que
ejecutar una operación de E/S o similar, en los hilos no hay una cola, sino que se van
que se ejecuta en otra parte del sistema. ejecutando de forma indeterminada.
3. Gestión y planificación de hilos
3.1. Paralelismo y concurrencia
Al igual que los procesos, la ejecución de hilos se
puede realizar mediante dos formas
(normalmente será una combinación de ambas ):
● Paralelismo: en un sistema multiprocesador El tiempo de ejecución y alternancia de los
(varios núcleos), cada uno puede ejecutar hilos dependerá de cómo actúe el
un hilo. planificador de cada dispositivo. El orden
● Concurrencia: una sola CPU ejecuta varios de ejecución NO está determinado .
hilos, alternándolos. Según la aplicación, se pueden utilizar
prioridades. La prioridad de un hilo se
puede establecer o modificar con
setPriority(), y para obtener cuál es la
prioridad de un hilo se usa getPriority()
(aunque no lo usaremos en esta unidad).
3. Gestión y planificación de hilos
3.2. Esperar hilos 3.3. Dormir hilos
Al igual que en el tema anterior podíamos También vimos en el primer tema cómo
esperar a que un proceso terminara su ejecución hacer que el proceso se quedara esperando
para continuar con el proceso principal, para lo durante un tiempo concreto, indicado en
cual usábamos waitFor(), se puede hacer lo milisegundos. Esto hace que el proceso o
mismo con los hilos, en este caso con el método hilo principal se quede “dormido ” durante
join(): el tiempo indicado.
[Link]();
Si lo que queremos es dormir uno de los
hilos hijos, se puede hacer de dos formas,
simplemente hay que indicar el sleep()
dentro del método run() de la clase del
proceso hijo:
[Link](milisegundos);
3. Gestión y planificación de hilos
3.4. Hilos egoístas 3.5. Hilos daemon
Existen algunos planificadores que, dependiendo Se trata de un tipo de hilos que se ejecuta en
del SO, pueden hacer que un hilo se ejecute “segundo plano”. Además, estos terminarán
desde su principio hasta su fin, sin alternar con cuando termine su hilo padre, es decir, son
los demás. Para evitar esto, se puede utilizar si es dependientes del hilo que los ha creado. Para
necesario [Link](). Esta instrucción hace ello, después de instanciar el hilo, y antes de
que el proceso vuelva a ponerse en espera, y ejecutarlo, se usa el método:
“deje su sitio” momentáneamente a otro.
[Link](true);
4. Comunicación de hilos: información compartida
4.1. Constructor de un hilo
A veces será necesario pasar un parámetro a un hilo para que pueda ejecutarse. Para ello, al
instanciarlo desde el programa principal, se le indica entre paréntesis:
Hilo1 hiloSuma = new Hilo1(10,30);
Para que el hilo pueda utilizar estos parámetros dentro del hilo, se crea un constructor (public
Hilo(parámetros)), de la misma forma que con cualquier clase:
static class Hilo1 extends Thread{
private int num1, num2;
public Hilo1(int numero1, int numero2){ De esta forma, ya podrá usar las
this.num1 = numero1; variables num1 y num2 con los
this.num2 = numero2; valores aportados, en este caso 10 y 30.
}
4. Comunicación de hilos: información compartida
4.2. Compartir variables
Una de las ventajas principales de los hilos, es que pueden compartir variables. Para ello, la
clase que define el hilo debe estar en la misma clase principal, pero fuera del main .
Para que una variable sea accesible por todos los hilos, incluido el principal, se debe declarar de
forma global, privada y estática:
public class Principal{
private static int varCompartida = 0;
Al igual que ocurría con los ficheros
compartidos por procesos, a la hora de
static class Hilo1 extends Thread{ acceder a variables compartidas por
…
}
varios procesos podría haber problemas
public static void main(String[] args{ de sincronización , que se deben
… solucionar con algunas de las
}
}
herramientas que se nos proporcionan.
5. Sincronización de hilos
5.1. Monitores
El principal método de sincronización de hilos son los monitores . Estos se utilizan para
otorgar a un hilo el “permiso ” necesario para realizar una acción. Es decir, funcionan como un
objeto que permite ejecutar un trozo de código solo al hilo que lo posea.
Para ello, se utiliza el método synchronized. Hay dos formas de utilizarlo: crear un método
sincronizado, o utilizar un objeto de sincronización.
Métodos sincronizados
public class Principal{
Se crea un método o función con la public static synchronized void funcion(){
palabra clave synchronized, de forma //Código del método sincronizado
}
que todo el código dentro de la función
se ejecutará automáticamente de forma …
sincronizada entre hilos. En este caso, el }
monitor es el propio método .
5. Sincronización de hilos
5.1. Monitores
Objetos sincronizados public class Principal{
Se crea un objeto de tipo Object , que private static Object monitor = new Object();
será el monitor que controle qué hilo
static class Hilo1{
puede ejecutar ciertas acciones. Para //Código del hilo
ello, dentro del código de un hilo, se synchronized(monitor){
inicia un bloque sincronizado, al cual //Código del método sincronizado
}
solo puede entrar un hilo a la vez. }
public static void main(String[] args){
//Código del programa principal
synchronized(monitor){
//Código del método sincronizado
}
}
}
5. Sincronización de hilos
5.2. Wait y notify
wait() notify(), notifyAll()
Se usa para suspender Envía una señal a un hilo suspendido con wait() para “despertarlo”.
temporalmente un hilo, hasta Con notifyAll() se despierta a todos los hilos suspendidos, mientras
que sea “despertado” por otro. que con notify() se despierta solo a uno, de forma aleatoria.
Para poder utilizar estas acciones, deben estar dentro de un bloque sincronizado, ya sea un
método o un objeto monitor (se recomienda de la segunda forma). En ambos casos, para llamar a
los métodos habrá que indicarle delante el objeto. Por ejemplo:
public static synchronized void notificar(){ synchronized (monitor){
No recomendado.
[Link](); Puede generar [Link]();
} errores. }
public static synchronized void esperar(){ Synchronized (monitor){
[Link](); [Link]();
} }
5. Sincronización de hilos
5.3. Semáforos
Otro método de sincronización son los semáforos. Como su nombre indica, se encargan de
controlar el acceso a un recurso compartido. Funcionan de manera similar a los bloqueos, pero
con una diferencia fundamental:
Los semáforos permiten indicar el número máximo de hilos que pueden acceder a la vez a un recurso.
Sus métodos son:
Crea el objeto del semáforo,
private static Semaphore sem = new normalmente como variable global . El
Semaphore(3); número indica cuántos hilos pueden
acceder a la vez (en este ejemplo, 3).
[Link](); [Link]();
El hilo accede al semáforo, si es El hilo libera el semáforo,
que hay espacio libre para ello. deja de ocupar un espacio.
5. Sincronización de hilos
5.4. CountDownLatch
Hemos visto que el hilo principal puede esperar a que termine cualquiera de sus hilos hijos
usando [Link](), donde hilo es el nombre de la instancia del hilo. Sin embargo, cuando tenemos
un número elevado de hilos y queremos esperarlos a todos, se hace complejo usar este método
uno a uno. Para ello existe CountDownLatch , que espera hasta que se haya ejecutado una acción
un número definido de veces.
Crea el objeto de la cuenta atrás,
private static CountDownLatch latch = indicando el número de veces que
new CountDownLatch(10); queremos que se ejecute (es decir, el
valor inicial de la cuenta).
Se queda esperando a que termine la cuenta atrás, es decir, a
[Link]() que llegue a 0 . Se suele utilizar en el hilo principal, o en
cualquier hilo que espere a otros, aunque no sean sus hijos.
Decrementa en uno el valor de la cuenta. Puede ser decrementada
[Link]() por muchos hilos distintos, cuando vayan terminando sus acciones,
o puede hacerlo varias veces un mismo hilo.
5. Sincronización de hilos
5.5. CyclicBarrier
Un mecanismo de sincronización muy útil son las barreras . De forma similar a la cuenta atrás, el
objeto CyclicBarrier permite que un hilo se quede esperando a que ocurran una serie de
acciones. En este caso, la diferencia es que un hilo no espera a que otros terminen, sino que todos
los hilos se esperan entre sí . Es decir, los hilos se quedarán esperando en un punto hasta que
haya cierto número de ellos que hayan alcanzado el punto de espera. Entonces, podrán continuar.
Crea el objeto de la barrera,
private static CyclicBarrier barrera = indicando el número de hilos que
new CyclicBarrier(10); deben alcanzar ese punto antes de
continuar (en este caso, 10).
Se queda esperando a que los demás hilos lleguen a la
barrera . Cuando el número de hilos que están esperando
[Link]()
sea igual al indicado en el objeto (10 en el ejemplo), todos
podrán continuar, y la barrera se resetea.
5. Sincronización de hilos
5.6. Lock y ReadWriteLock
Estos métodos sí son muy similares al bloqueo de los procesos. En este caso, se ejecutan como un
objeto en sí, no sobre el canal de otro. Además, se pueden definir como ReentrantLock, lo cual
añade ciertas funcionalidades nuevas.
Crea el objeto compartido que sirve
private static ReentrantLock bloqueo = new ReentrantLock();
como bloqueo.
Entra en la región crítica y bloquea
Libera el recurso y sale de la
[Link]() el recurso, igual que con un lock [Link]()
región crítica, igual que release().
normal.
Funcionalidades adicionales de ReentrantLock
● Si existe un bloqueo dentro de un bloqueo (es decir, una región crítica dentro de otra), entrará
directamente en la segunda.
● Se puede “intentar ” el bloqueo usando [Link](), devolviendo true si se ha conseguido y
false si no lo ha hecho (porque ya está bloqueado). También se puede indicar que lo intente
durante un tiempo determinado con [Link](tiempo, unidades).
5. Sincronización de hilos
5.6. Lock y ReadWriteLock
Por otro lado, ReadWriteLock también sirve para realizar bloqueos, en este caso orientados a la
lectura y escritura concurrente. Sin embargo, hace más eficiente estas acciones, ya que permite
que varios hilos lean a la vez de un recurso, siempre que no haya ningún otro escribiendo, pero
nunca escribiendo a la vez, que es lo que puede causar problemas de corrupción de los datos.
private static ReentrantReadWriteLock bloqueoRW = new Crea el objeto compartido que sirve
ReentrantReadWriteLock(); como bloqueo.
[Link]() [Link]()
Indica que el bloqueo es de solo Indica que el bloqueo es de solo
lectura, por lo que otros hilos de escritura, por lo que no podrán acceder
lectura podrán acceder a la vez. otros hilos, ni de lectura ni de escritura.
[Link](); Libera el recurso y sale de la Se puede usar tryLock() en
[Link](); región crítica, igual que en un lock ReentrantReadWriteLock igual
normal. que ReentrantLock .
5. Sincronización de hilos: Resumen
Establece una parte de código que solo ejecutará un hilo al mismo tiempo. Puede ser
synchronized directamente un método (public static synchronized…), o un bloque sincronizado
con un monitor de tipo Object (synchronized(monitor){...}).
Los hilos se duermen con wait(), hasta que son despertados por otro hilo. Con
wait / notify notify() se despierta a un solo hilo, con notifyAll() se despierta a todos. Para poder
usarlo, debe estar dentro de un bloque de tipo synchronized mediante un monitor.
Establece una parte del código a la cual solo pueden acceder un número de hilos
Semaphore concretos , a la cual se accede con acquire() y se libera con release().
Crea una cuenta atrás, que puede ser decrementada por cualquier hilo usando
CountDownLatch countDown(). Con await() el hilo se queda esperando hasta que la cuenta llegue a
0.
Al usar await(), hace que los hilos se queden esperando en una barrera . No podrán
CyclicBarrier avanzar hasta que a la barrera no hayan llegado el número de hilos indicado .
Funciona para bloquear recursos (similar a Lock en los procesos, pero como un
objeto), pero permite diferenciar entre lectura ([Link]()) y escritura
ReadWriteLock ([Link]()), por lo que es más eficiente. Se liberan con [Link]() y
[Link]().