Multithreading.
Vamos a comentar uno de los aspectos más
interesantes del lenguaje Java: los threads o hilos de ejecución
independientes y concurrentes.
Introducción. ¿qué
es un thread?
La idea fundamental es bien sencilla. En
la programación tradicional hay un solo flujo de control, motivado
fundamentalmente porque la máquina internamente suele tener un solo
procesador (una sola "mente" que realiza las instrucciones, una tras otra).
La programación multithreading
permite la ocurrencia simultánea de varios flujos de control. Cada
uno de ellos puede programarse independientemente y realizar un trabajo,
distinto, idéntico o complementario, a otros flujos paralelos.
Hay miles de ejemplos en los que puede
ser útil pensar en varios flujos de ejecución (threads):
la posibilidad de editar mientras seguimos cargando o salvando un gran
fichero, la posibilidad de visualizar una página mientras se están
buscando las siguientes, la visualización de varios procesos que
ocurren a la vez de forma independiente, etc.
Es decir, un thread será un hilo
de ejecución, un proceso independiente, que se podrá ejecutar
paralela o concurrentemente con otros procesos. Este thread podrá
trabajar sobre datos distintos o compartidos, y podrá en un momento
dado pararse, reiniciarse, sincronizarse o esperar a otros.
Ejemplo 1.
En este ejemplo crearemos una clase que
será un Thread, y haremos dos instancias de esta clase para lanzar
dos ejecuciones simultáneas, paralelamente, y ver qué sucede.
El código de este ejemplo se
encuentra en el archivo:
\Curso Java\Java\ejemplos\curso4\EjemploThread.java
Si ejecutamos... ¿Extraño?
¿Qué es lo que ha ocurrido? Sencillo, el objeto thread1 ha
ejecutado su método run() (eso es lo que hace el método start()
que es el iniciador de todo thread) y ha escrito 100000 veces "PRIMERO"
en su en pantalla. Pero a la vez también se ha ejecutado el run()
del objeto thread2, que trataba de visualizar 100000 veces la palabra "SEGUNDO".
Y obviamente no hay dos pantallas, los dos threads tienen la misma salida
estándar, con lo que han ido entremezclándose las impresiones
de "PRIMERO" y "SEGUNDO" por pantalla.
Para ello hemos hecho que desciendan
de la clase java.lang.Thread.
Cualquier subclase de Thread permite después crear objetos y ejecutar
su método run() (que debe redefinirse, ya que por omisión
no hace nada) en paralelo con otros.
Algunas características de la
clase Thread son las siguientes:
Atributos:
-
int MAX_PRIORITY - Prioridad máxima
que puede tener un thread.
-
int MIN_PRIORITY - La mínima.
-
int NORM_PRIORITY - La prioridad que se
asigna por defecto.
Constructores:
-
public Thread() -> Crea un thread con nombre
"Thread-"+n (n es un entero secuencial).
-
public Thread(String name) -> Como el anterior,
pero con el nombre de thread name indicado.
-
public Thread(Runnable target) -> Crea
un thread asociado al objeto target. El nombre es "Thread-"+n.
-
public Thread(Runnable target, String name)
-> Como el anterior, pero con nombre específico.
Métodos de clase:
-
public static native Thread currentThread()
-> Devuelve el thread que está ejecutándose.
-
public static native void yield() -> Causa
que el thread en curso haga una pausa temporal y permita a otros threads
ejecutarse.
-
public static void sleep(long millis, int
nanos) throws InterruptedException -> Hace que el thread activo se "duerma"
(se detenga temporalmente) durante el tiempo indicado en milisegundos (millis),
y el tiempo adicional opcional en nanosegundos (nanos, de 0 a 999999).
-
public static boolean interrupted()
-> Chequea si el thread activo ha sido interrumpido (ver isInterrupted).
Métodos de instancia:
-
public native synchronized void start()
-> Inicia la ejecución de un thread. La máquina virtual de
Java llama al método run() correspondiente a este objeto o al Runnable
asociado. A partir de este momento
los dos threads, el que llama a start() y el nuevo, se ejecutan.
-
public void run()
-> Especificaremos en él lo que queremos que haga la ejecución
de nuestro Thread.
-
public final void stop()
-> Detiene la ejecución del thread. Es el método opuesto
a start().
-
public void interrupt()
-> Interrumpe la ejecución del thread.
-
public boolean isInterrupted()
-> Comprueba si el thread ha sido interrumpido.
-
public void destroy()
-> Destruye el thread.
-
public final native boolean isAlive()
-> Comprueba si el thread está aún "vivo", es decir, ejecutándose.
-
public final void suspend()
-> Suspende el thread y lo deja así hasta que se recupera (resume).
-
public final void resume()
-> Recupera la ejecución del thread en el punto en el que estaba
(ver suspend).
-
public final void setPriority(int newPriority)
-> Cambia la prioridad de este thread. A mayor prioridad, más tiempo
de procesador se concede con respecto a otros threads.
-
public final int getPriority()
-> Devuelve la prioridad del thread.
-
public final void setName(String name)
-> Cambia el nombre del thread. Los nombres de threads distintos no tienen
por qué ser distintos, aunque obviamente es recomendable.
-
public final String getName() -> Devuelve
el nombre del thread.
-
public native int countStackFrames()
-> Devuelve el número de registros de activación (stack frames)
de este thread. Ojo, que el thread debe estar suspendido para poder llamar
a este método.
-
public final synchronized void join(long
millis) throws InterruptedException
-> Espera a que el thread acabe (muera). Si se indica millis significa
el tiempo máximo (en milisegundos) que se va a esperar. Un valor
0 significa que se espera hasta que acabe (o sea, lo mismo que no ponerlo).
-
public final void setDaemon(boolean on)
-> Marca el thread como demonio o no (thread de usuario). Lo normal es
que los threads no sean demonios. La máquina virtual Java se ejecuta
hasta que no quedan threads de usuario en curso. Por ejemplo, el garbage
collector es un thread de prioridad mínima que se ejecuta como un
demonio. El método debe
llamarse antes del start().
-
public final boolean isDaemon()
-> Consultor de lo anterior.
La interfaz Runnable.
En el ejemplo anterior vimos que para crear
un Thread, un proceso que se ejecute paralelamente, hemos de heredar de
la clase Thread y escribir lo que queramos que haga la ejecución
de nuestro proceso en el método run. Pero, ¿qué sucede
si nosotros queremos heredar de otra clase que nos interese más
en lugar de la clase Thread? Como en Java no se soporta la herencia múltiple,
¿ya no podremos crear el Thread de ejecución independiente?
Para solucionar este problema está la interface java.lang.Runnable,
que lo único que te dice es que debes definir en tu clase un método
que tenga la forma public void run(). A partir de ahí, cualquier
objeto que lo haga puede usarse para crear un thread que ejecutará
al iniciarse, el método run() de ese objeto. Veámoslo en
un ejemplo:
Ejemplo 2.
En EjemploThread2 heredamos de Rectángulo
e implementamos Runnable (para poder construirnos posteriormente un Thread
con los objetos de esta clase) y definimos sobre él un método
run() que será el que se ejecute cuando cree un Thread con un objeto
de esta clase EjemploThread2. Vemos al ejecutarlo como se entremezclan
las ejecuciones de las dos instancias que nos hemos creado.
El código de este ejemplo se
encuentra en los archivos:
\Curso Java\Java\ejemplos\curso4\EjemploThread2.java
\Curso Java\Java\ejemplos\curso\Rectangulo.java
Grupos de Threads.
Mediante el uso de la clase java.lang.ThreadGroup
podemos agrupar Threads. Un grupo de Threads nos será de utilidad
cuando queramos coordinar un cierto número de Threads. Si agrupamos
un número de Threads en un ThreadGroup, podremos hacer cosas como
suspender la ejecución de todo el grupo, ejecutándo el método
correspondiente sobre el grupo, que a su vez lo ejecutará sobre
cada uno de los Threads miembros del grupo. Veamos un ejemplo que trate
un ThreadGroup.
Ejemplo 3.
Este ejemplo crea un ThreadGroup con dos
threads, al tiempo pregunta al grupo por si tiene un determinado Thread,
para si es así parar su ejecución.
El código de este ejemplo se
encuentra en el archivo:
\Curso Java\Java\ejemplos\curso4\EjemploThread3.java
Control de concurrencia.
Puede ocurrir que haya ciertos métodos
críticos en los que los threads comparten datos o recursos que debamos
controlar el correcto acceso de estos threads a estos recursos compartidos.
Por ejemplo:
public void escribirUnaLinea()
{
System.out.print ("Escribo la mitad de la línea");
System.out.println (" y ahora la finalizo");
}
Si creásemos 2 threads que ejecutasen
ese código constantemente por pantalla, podría ocurrir que
nuestro sistema operativo empezase a ejecutar el método sobre el
primer thread y justo cuando ha ejecutado la sentencia System.out.print
("Escribo la mitad de la línea"); le pasa el control al segundo
thread de modo que se entremezclarían las líneas de los dos
threads pudiendo ser el resultado:
Escribo la mitad
de la lineaEscribo la mitad de la línea ...
Para solucionar este problema se le
pone al método el modificador synchronized,
de la forma:
public synchronized
void escribirUnaLinea() {
System.out.print ("Escribo la mitad de la línea");
System.out.println (" y ahora la finalizo");
}
de esta manera cuando un thread empieza
la ejecución de un método marcado como synchronized, los
demás threads no podrán entrar en ningún otro método
que esté marcado como synchronized, por tanto la ejecución
ahora será correcta.
Ejercicio.
Escribe una clase SumaMatriz que devuelve
la suma de una matriz de doubles, creando un thread distinto para sumar
cada una de sus filas, de modo que finalmente se sume el resultado de cada
fila a un único total.