7. Los hilos de ejecución
7.1. Introducción
Cuando se inicia una aplicación, esta se ejecuta en un flujo de ejecución denominado «hilo». La clase que modela un thread es la clase java.lang.Thread, cuyas propiedades y métodos se detallan a continuación:
devuelve el hilo que se está ejecutando actualmente | |
establece el nombre del hilo | |
nombre del hilo | |
indica si el hilo está activo (true) o no (false) | |
inicia la ejecución de un hilo | |
método que se ejecuta automáticamente tras la ejecución del método «start» anterior | |
detiene la ejecución de un hilo durante n milisegundos | |
Operación bloqueante: espera a que finalice el hilo para pasar a la siguiente instrucción |
Los constructores más utilizados son los siguientes:
crea una referencia a una tarea asíncrona. Esta aún está inactiva. La tarea creada debe disponer del método run: lo más habitual es que se utilice una clase derivada de Thread. | |
Lo mismo, pero es el objeto Runnable, pasado como parámetro, el que implementa el método run. |
Veamos una primera aplicación que pone de manifiesto la existencia de un hilo principal de ejecución, aquel en el que se ejecuta la función main de una clase:
// uso de subprocesos
import java.io.*;
import java.util.*;
public class thread1{
public static void main(String[] arg)throws Exception {
// inicialización del hilo actual
Thread main=Thread.currentThread();
// visualización
System.out.println("Thread courant : " + main.getName());
// cambio de nombre
main.setName("myMainThread");
// comprobación
System.out.println("Thread courant : " + main.getName());
// bucle infinito
while(true){
// se obtiene la hora
Calendar calendrier=Calendar.getInstance();
String H=calendrier.get(Calendar.HOUR_OF_DAY)+":"
+calendrier.get(Calendar.MINUTE)+":"
+calendrier.get(Calendar.SECOND);
// visualización
System.out.println(main.getName() + " : " +H);
// parada temporal
Thread.sleep(1000);
}//while
}//main
}//clase
Resultados en pantalla:
Thread courant : main
Thread courant : myMainThread
myMainThread : 15:34:9
myMainThread : 15:34:10
myMainThread : 15:34:11
myMainThread : 15:34:12
Terminer le programme de commandes (O/N) ? o
El ejemplo anterior ilustra los siguientes puntos:
- la función main se ejecuta correctamente en un hilo
- se puede acceder a las características de este hilo mediante Thread.currentThread()
- la función del método sleep. En este caso, el hilo que ejecuta main entra en suspensión periódicamente durante 1 segundo entre dos visualizaciones.
7.2. Creación de hilos de ejecución
Es posible tener aplicaciones en las que fragmentos de código se ejecutan de forma «simultánea» en diferentes hilos de ejecución. Cuando se dice que los thread se ejecutan de forma simultánea, a menudo se comete un error de expresión. Si el equipo solo tiene un procesador, como suele ser el caso, los thread comparten dicho procesador: disponen de él, cada uno por turnos, durante un breve instante (unos milisegundos). Esto es lo que da la impresión de que la ejecución es paralela. El tiempo asignado a un thread depende de diversos factores, entre ellos su prioridad, que tiene un valor por defecto pero que también puede fijarse mediante programación. Cuando un thread dispone del procesador, normalmente lo utiliza durante todo el tiempo que se le ha asignado. Sin embargo, puede liberarlo antes de tiempo:
- poniéndose en espera de un evento (wait, join)
- entrando en suspensión durante un tiempo determinado (sleep)
- Un hilo T puede crearse de diversas formas
- derivando la clase Thread y redefiniendo su método run
- implementando la interfaz Runnable en una clase y utilizando el constructor new Thread(Runnable). Runnable es una interfaz que solo define un único método: public void run(). Por lo tanto, el argumento del constructor anterior es cualquier instancia de clase que implemente este método run.
En el siguiente ejemplo, los subprocesos se crean mediante una clase anónima que deriva de la clase Thread:
// se crea el hilo i
tâches[i]=new Thread() {
public void run() {
affiche();
}
};//definición de tareas[i]
El método run se limita aquí a redirigir a un método affiche.
- La ejecución del hilo T se inicia mediante T.start(): este método pertenece a la clase Thread y realiza una serie de inicializaciones para, a continuación, iniciar automáticamente el método run del hilo o de la interfaz Runnable. El programa que ejecuta la instrucción T.start() no espera a que finalice la tarea T: pasa inmediatamente a la instrucción siguiente. Así, tenemos dos tareas que se ejecutan en paralelo. A menudo, estas deben poder comunicarse entre sí para saber en qué punto se encuentra el trabajo común que deben realizar. Este es el problema de la sincronización de los hilos.
- Una vez iniciado, el thread se ejecuta de forma autónoma. Se detendrá cuando la función run que está ejecutando haya finalizado su trabajo.
- Se puede esperar a que finalice la ejecución del hilo T mediante T.join(). Se trata de una instrucción bloqueante: el programa que la ejecuta queda bloqueado hasta que la tarea T haya terminado su trabajo. También es un método de sincronización.
Analicemos el siguiente programa:
// uso de subprocesos
import java.io.*;
import java.util.*;
public class thread2{
public static void main(String[] arg) {
// inicialización del hilo actual
Thread main=Thread.currentThread();
// se asigna un nombre al hilo actual
main.setName("myMainThread");
// Inicio de main
System.out.println("début du thread " +main.getName());
// creación de subprocesos de ejecución
Thread[] tâches=new Thread[5];
for(int i=0;i<tâches.length;i++){
// se crea el hilo i
tâches[i]=new Thread() {
public void run() {
affiche();
}
};//tareas de def[i]
// se establece el nombre del hilo
tâches[i].setName(""+i);
// se inicia la ejecución del hilo i
tâches[i].start();
}//for
// fin de main
System.out.println("fin du thread " +main.getName());
}//Main
public static void affiche() {
// se obtiene la hora
Calendar calendrier=Calendar.getInstance();
String H=calendrier.get(Calendar.HOUR_OF_DAY)+":"
+calendrier.get(Calendar.MINUTE)+":"
+calendrier.get(Calendar.SECOND);
// se muestra el inicio de la ejecución
System.out.println("Début d'exécution de la méthode affiche dans le Thread " +
Thread.currentThread().getName()+ " : " + H);
// suspensión durante 1 s
try{
Thread.sleep(1000);
}catch (Exception ex){}
// se recupera la hora
calendrier=Calendar.getInstance();
H=calendrier.get(Calendar.HOUR_OF_DAY)+":"
+calendrier.get(Calendar.MINUTE)+":"
+calendrier.get(Calendar.SECOND);
// se muestra el final de la ejecución
System.out.println("Fin d'exécution de la méthode affiche dans le Thread "
+Thread.currentThread().getName()+ " : " + H);
}// muestra
}//clase
El hilo principal, el que ejecuta la función main, crea otros 5 hilos encargados de ejecutar el método estático affiche. Los resultados son los siguientes:
début du thread myMainThread
Début d'exécution de la méthode affiche dans le Thread 0 : 15:48:3
fin du thread myMainThread
Début d'exécution de la méthode affiche dans le Thread 1 : 15:48:3
Début d'exécution de la méthode affiche dans le Thread 2 : 15:48:3
Début d'exécution de la méthode affiche dans le Thread 3 : 15:48:3
Début d'exécution de la méthode affiche dans le Thread 4 : 15:48:3
Fin d'exécution de la méthode affiche dans le Thread 0 : 15:48:4
Fin d'exécution de la méthode affiche dans le Thread 1 : 15:48:4
Fin d'exécution de la méthode affiche dans le Thread 2 : 15:48:4
Fin d'exécution de la méthode affiche dans le Thread 3 : 15:48:4
Fin d'exécution de la méthode affiche dans le Thread 4 : 15:48:4
Estos resultados son muy reveladores:
- en primer lugar, se observa que el inicio de la ejecución de un hilo no es bloqueante. El método main inició la ejecución de 5 hilos en paralelo y finalizó su ejecución antes que ellos. La operación
inicia la ejecución del hilo tareas[i], pero una vez hecho esto, la ejecución continúa inmediatamente con la instrucción siguiente sin esperar a que finalice la ejecución del hilo.
- Todos los subprocesos creados deben ejecutar el método affiche. El orden de ejecución es impredecible. Aunque en el ejemplo el orden de ejecución parezca seguir el orden de inicio de los subprocesos, no se pueden extraer conclusiones generales. El sistema operativo cuenta aquí con 6 subprocesos y un procesador. Distribuirá el procesador entre estos 6 subprocesos según sus propias reglas.
- En los resultados se observa una consecuencia del método sleep. En el ejemplo, es el hilo 0 el que ejecuta en primer lugar el método affiche. Se muestra el mensaje de inicio de ejecución y, a continuación, ejecuta el método sleep, que lo suspende durante 1 segundo. Entonces pierde el procesador, que queda así disponible para otro hilo. El ejemplo muestra que es el hilo 1 el que lo va a obtener. El hilo 1 seguirá el mismo recorrido, al igual que los demás hilos. Cuando finalice el segundo de espera del hilo 0, su ejecución podrá reanudarse. El sistema le cede el procesador y puede terminar la ejecución del método affiche.
Modifiquemos nuestro programa para finalizar el método main con las instrucciones:
// fin de la función
System.out.println("fin du thread " +main.getName());
// parada de la aplicación
System.exit(0);
Al ejecutar el nuevo programa, se obtiene:
début du thread myMainThread
Début d'exécution de la méthode affiche dans le Thread 0 : 16:5:45
Début d'exécution de la méthode affiche dans le Thread 1 : 16:5:45
Début d'exécution de la méthode affiche dans le Thread 2 : 16:5:45
Début d'exécution de la méthode affiche dans le Thread 3 : 16:5:45
fin du thread myMainThread
Début d'exécution de la méthode affiche dans le Thread 4 : 16:5:45
En cuanto el método main ejecuta la instrucción:
detiene todos los subprocesos de la aplicación y no solo el subproceso main. El método main podría querer esperar a que finalice la ejecución de los subprocesos que ha creado antes de terminarse él mismo. Esto se puede hacer con el método join de la clase Thread:
// espera a que terminen todos los hilos
for(int i=0;i<tâches.length;i++){
// se espera al hilo i
tâches[i].join();
}//for
// fin de main
System.out.println("fin du thread " +main.getName());
// parada de la aplicación
System.exit(0);
De este modo, se obtienen los siguientes resultados:
début du thread myMainThread
Début d'exécution de la méthode affiche dans le Thread 0 : 16:11:9
Début d'exécution de la méthode affiche dans le Thread 1 : 16:11:9
Début d'exécution de la méthode affiche dans le Thread 2 : 16:11:9
Début d'exécution de la méthode affiche dans le Thread 3 : 16:11:9
Début d'exécution de la méthode affiche dans le Thread 4 : 16:11:9
Fin d'exécution de la méthode affiche dans le Thread 0 : 16:11:10
Fin d'exécution de la méthode affiche dans le Thread 1 : 16:11:10
Fin d'exécution de la méthode affiche dans le Thread 2 : 16:11:10
Fin d'exécution de la méthode affiche dans le Thread 3 : 16:11:10
Fin d'exécution de la méthode affiche dans le Thread 4 : 16:11:10
fin du thread myMainThread
7.3. Importancia de los hilos
Ahora que hemos puesto de manifiesto la existencia de un hilo por defecto —el que ejecuta el método Main— y que sabemos cómo crear otros, detengámonos en la utilidad que tienen para nosotros los hilos y en la razón por la que los presentamos aquí. Hay un tipo de aplicaciones que se prestan muy bien al uso de subprocesos: las aplicaciones cliente-servidor de Internet. En una aplicación de este tipo, un servidor ubicado en una máquina S1 responde a las solicitudes de clientes ubicados en máquinas remotas C1, C2, ..., Cn.

Todos los días utilizamos aplicaciones de Internet que se ajustan a este esquema: servicios web, correo electrónico, consulta de foros, transferencia de archivos... En el esquema anterior, el servidor S1 debe atender a los clientes Ci de forma simultánea. Si tomamos el ejemplo de un servidor FTP (Protocolo de Transferencia de Archivos) que envía archivos a sus clientes, sabemos que una transferencia de archivos puede tardar a veces varias horas. Por supuesto, es impensable que un solo cliente monopolice el servidor durante tanto tiempo. Lo que se suele hacer es que el servidor cree tantos subprocesos de ejecución como clientes haya. Cada subproceso se encarga entonces de atender a un cliente concreto. Dado que el procesador se comparte cíclicamente entre todos los subprocesos activos de la máquina, el servidor dedica un poco de tiempo a cada cliente, garantizando así la simultaneidad del servicio.

7.4. Un reloj gráfico
Consideremos la siguiente aplicación, que muestra una ventana con un reloj y un botón para detener o reiniciar el reloj:
![]() | ![]() | ![]() |
Para que el reloj funcione, es necesario que un proceso se encargue de cambiar la hora cada segundo. Al mismo tiempo, hay que supervisar los eventos que se producen en la ventana: cuando el usuario haga clic en el botón «Detener», habrá que detener el reloj. Aquí tenemos dos tareas paralelas y asíncronas: el usuario puede hacer clic en cualquier momento.
Consideremos el momento en el que el reloj aún no se ha iniciado y el usuario hace clic en el botón «Iniciar». Se trata de un evento clásico y cabría pensar que un método del hilo en el que se ejecuta la ventana podría gestionar entonces el reloj. Sin embargo, cuando se ejecuta un método de la aplicación gráfica, el hilo de esta ya no está a la escucha de los eventos de la interfaz gráfica. Estos se producen y se colocan en una cola para que la aplicación los procese cuando finalice el método que se está ejecutando en ese momento. En nuestro ejemplo del reloj, el método estará siempre en ejecución, ya que solo el clic en el botón «Detener» puede detenerlo. Sin embargo, este evento no se procesará hasta que el método haya finalizado. Es un círculo vicioso.
La solución a este problema sería que, cuando el usuario haga clic en el botón «Iniciar», se inicie una tarea para gestionar el reloj, pero que la aplicación pueda seguir escuchando los eventos que se producen en la ventana. De este modo, tendríamos dos tareas distintas que se ejecutarían en paralelo:
- gestión del reloj
- escucha de los eventos de la ventana
Volvamos a nuestro reloj gráfico:
![]() |
|
El código útil de la aplicación creada con JBuilder es el siguiente:
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import java.util.*;
public class interfaceHorloge extends JFrame {
JPanel contentPane;
JTextField txtHorloge = new JTextField();
JButton btnGoStop = new JButton();
// atributos de instancia
boolean finHorloge=true;
//Construir el marco
public interfaceHorloge() {
enableEvents(AWTEvent.WINDOW_EVENT_MASK);
try {
jbInit();
}
catch(Exception e) {
e.printStackTrace();
}
}
private void runHorloge(){
// se repite el bucle hasta que se nos indique que paremos
while( ! finHorloge){
// se obtiene la hora
Calendar calendrier=Calendar.getInstance();
String H=calendrier.get(Calendar.HOUR_OF_DAY)+":"
+calendrier.get(Calendar.MINUTE)+":"
+calendrier.get(Calendar.SECOND);
// la mostramos en el campo T
txtHorloge.setText(H);
// espera un segundo
try{
Thread.sleep(1000);
} catch (Exception e){
// salida con error
System.exit(1);
}//try
}// while
}// runHorloge
//Inicializar el componente
private void jbInit() throws Exception {
...................
}
//Sustituido, así podemos salir cuando se cierre la ventana
protected void processWindowEvent(WindowEvent e) {
.............
}
void btnGoStop_actionPerformed(ActionEvent e) {
// se inicia/detiene el reloj
// se recupera el texto del botón
String libellé=btnGoStop.getText();
// ¿Iniciar?
if(libellé.equals("Lancer")){
// se crea el hilo en el que se ejecutará el reloj
Thread thHorloge=new Thread(){
public void run(){
runHorloge();
}
};//definir hilo
// se autoriza el inicio del hilo
finHorloge=false;
// se cambia el texto del botón
btnGoStop.setText("Arrêter");
// se inicia el hilo
thHorloge.start();
// fin
return;
}//si
// detener
if(libellé.equals("Arrêter")){
// se le indica al hilo que se detenga
finHorloge=true;
// se cambia el texto del botón
btnGoStop.setText("Lancer");
// fin
return;
}//if
}
}
Cuando el usuario hace clic en el botón «Ejecutar», se crea un hilo mediante una clase anónima:
El método run del hilo remite al método runHorloge de la aplicación. Una vez hecho esto, se inicia el hilo:
A continuación, se ejecutará el método runHorloge:
private void runHorloge(){
// se repite el bucle hasta que se nos indique que paremos
while( ! finHorloge){
// se obtiene la hora
Calendar calendrier=Calendar.getInstance();
String H=calendrier.get(Calendar.HOUR_OF_DAY)+":"
+calendrier.get(Calendar.MINUTE)+":"
+calendrier.get(Calendar.SECOND);
// se muestra en el campo T
txtHorloge.setText(H);
// espera un segundo
try{
Thread.sleep(1000);
} catch (Exception e){
// salida con error
System.exit(1);
}//intentar
}// while
}// runHorloge
El principio del método es el siguiente:
- muestra la hora actual en el cuadro de texto txtHorloge
- se detiene 1 segundo
- vuelve al paso 1, tras haber comprobado previamente la variable booleana finHorloge, que se establecerá en «verdadero» cuando el usuario haga clic en el botón Arrêter.
7.5. Applet de reloj
Transformamos la aplicación gráfica anterior en un applet siguiendo el método habitual y creamos el siguiente documento HTML appletHorloge.htm:
<html>
<head>
<title>Applet Horloge</title>
</head>
<body>
<h2>Une applet horloge</h2>
<applet
code="appletHorloge.class"
width="150"
height="130"
></applet>
</center>
</body>
</html>
Cuando cargamos directamente este documento en IE haciendo doble clic sobre él, obtenemos la siguiente visualización:

En este ejemplo, todos los elementos necesarios para el applet se encuentran en la misma carpeta:
E:\data\serge\Jbuilder\horloge\1>dir
13/06/2002 12:17 3 174 appletHorloge.class
13/06/2002 12:17 658 appletHorloge$1.class
13/06/2002 12:17 512 appletHorloge$2.class
13/06/2002 12:20 245 appletHorloge.htm
Nuestro applet se puede mejorar. Hemos dicho que, al cargarse el applet, se ejecuta el método init y, a continuación, el método start, si existe. Además, cuando el usuario abandona la página, se ejecuta el método stop, si existe. Cuando vuelve a la página del applet, se vuelve a llamar al método start. Cuando un applet utiliza subprocesos de animación visual, a menudo se emplean los métodos start y stop del applet para iniciar y detener los subprocesos. De hecho, no tiene sentido que un subproceso de animación visual siga trabajando en segundo plano mientras la animación está oculta.
Por lo tanto, añadimos a nuestro applet los siguientes métodos start y stop:
public void stop(){
// la página está oculta
// seguimiento
System.out.println("Page stop");
// la página está oculta: se detiene el hilo
finHorloge=true;
}
public void start(){
// la página vuelve a aparecer
// seguimiento
System.out.println("Page start");
// se reinicia un nuevo hilo de reloj si es necesario
if(btnGoStop.getText().equals("Arrêter")){
// se cambia el texto
btnGoStop.setText("Lancer");
// y se simula que el usuario ha hecho clic en él
btnGoStop_actionPerformed(null);
}//if
}//start
Además, hemos añadido un seguimiento en el método run del hilo para saber cuándo se inicia y se detiene:
private void runHorloge(){
// seguimiento
System.out.println("Thread horloge lancé");
// se repite el bucle hasta que se nos indique que paremos
while( ! finHorloge){
// se obtiene la hora
Calendar calendrier=Calendar.getInstance();
String H=calendrier.get(Calendar.HOUR_OF_DAY)+":"
+calendrier.get(Calendar.MINUTE)+":"
+calendrier.get(Calendar.SECOND);
// la mostramos en el campo T
txtHorloge.setText(H);
// espera de un segundo
try{
Thread.sleep(1000);
} catch (Exception e){
// salida con error
return;
}//try
}// while
// seguimiento
System.out.println("Thread horloge terminé");
}// runHorloge
Ahora ejecutamos el applet con AppletViewer:
E:\data\serge\Jbuilder\horloge\1>appletviewer appletHorloge.htm
Page start // applet iniciado - página mostrada
Thread horloge lancé // el hilo se inicia en consecuencia
Page stop // applet minimizado
Thread horloge terminé // el hilo se detiene en consecuencia
Page start // el applet se vuelve a mostrar
Thread horloge lancé // el hilo se reinicia
Thread horloge terminé // se pulsa el botón de parada
Thread horloge lancé // se pulsa el botón «Iniciar»
Page stop // El applet aparece como icono e
Thread horloge terminé // hilo detenido en consecuencia
Page start // revisualización del applet
Thread horloge lancé // hilo reiniciado en consecuencia
Con AppletViewer, el evento start se produce cuando la ventana de AppletViewer está visible, y el evento stop cuando se minimiza. Los resultados anteriores muestran que, cuando el documento HTML está oculto, el hilo se detiene correctamente si estaba activo.
7.6. Sincronización de tareas
En nuestro ejemplo anterior, había dos tareas:
- la tarea principal, representada por la propia aplicación
- la tarea encargada del reloj
La coordinación entre ambas tareas la llevaba a cabo la tarea principal, que establecía un valor booleano para detener el hilo del reloj. Ahora abordamos el problema del acceso concurrente de las tareas a recursos comunes, problema también conocido como «compartición de recursos». Para ilustrarlo, vamos a estudiar primero un ejemplo.
7.6.1. Un recuento no sincronizado
Consideremos la siguiente interfaz gráfica:

n.º | tipo | nombre | función |
1 | JTextField | txtAGénérer | indica el número de subprocesos que se van a generar |
2 | JTextfield (no editable) | txtGénéres | indica el número de subprocesos generados |
3 | JTextField (no editable) | txtStatus | proporciona información sobre los errores detectados y sobre la propia aplicación |
4 | JButton | btnGénérer | inicia la generación de subprocesos |
El funcionamiento de la aplicación es el siguiente:
- el usuario indica el número de subprocesos que se van a generar en el campo 1
- inicia la generación de estos hilos con el botón 4
- los hilos leen el valor del campo 2, lo incrementan y muestran el nuevo valor. Inicialmente, este campo contiene el valor 0.
Los subprocesos generados comparten un recurso: el valor del campo 2. Lo que pretendemos mostrar aquí son los problemas que surgen en una situación de este tipo. He aquí un ejemplo de ejecución:

Se observa que se ha solicitado la generación de 1000 subprocesos y que solo se han contabilizado 7. El código relevante de la aplicación es el siguiente:
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class interfaceSynchro extends JFrame {
JPanel contentPane;
JLabel jLabel1 = new JLabel();
JTextField txtAGénérer = new JTextField();
JButton btnGénérer = new JButton();
JTextField txtStatus = new JTextField();
JTextField txtGénérés = new JTextField();
JLabel jLabel2 = new JLabel();
// variables de instancia
Thread[] tâches=null; // los hilos
int[] compteurs=null; // los contadores
//Crear el marco
public interfaceSynchro() {
..........
}
//Inicializar el componente
private void jbInit() throws Exception {
......................
}
//Sustituido, así podemos salir cuando se cierre la ventana
protected void processWindowEvent(WindowEvent e) {
..................
}
void btnGénérer_actionPerformed(ActionEvent e) {
//Generación de subprocesos
// Se lee el número de subprocesos que se van a generar
int nbThreads=0;
try{
// Se lee el campo que contiene el número de subprocesos
nbThreads=Integer.parseInt(txtAGénérer.getText().trim());
// positivo >
if(nbThreads<=0) throw new Exception();
}catch(Exception ex){
//error
txtStatus.setText("Nombre invalide");
// se vuelve a intentar
txtAGénérer.requestFocus();
return;
}//captura
// al inicio no se han generado subprocesos
txtGénérés.setText("0"); // contador de tareas a 0
// se generan y se inician los hilos
tâches=new Thread[nbThreads];
compteurs=new int[nbThreads];
for(int i=0;i<tâches.length;i++){
// se crea el hilo i
tâches[i]=new Thread() {
public void run() {
incrémente();
}
};//hilo i
// se define su nombre
tâches[i].setName(""+i);
// se inicia su ejecución
tâches[i].start();
}//for
}//generar
// incrementar
private void incrémente(){
// se recupera el número del hilo
int iThread=0;
try{
iThread=Integer.parseInt(Thread.currentThread().getName());
}catch(Exception ex){}
// se lee el valor del contador de tareas
try{
compteurs[iThread]=Integer.parseInt(txtGénérés.getText());
} catch (Exception e){}
// se incrementa
compteurs[iThread]++;
// se espera 100 milisegundos; el hilo perderá entonces el control del procesador
try{
Thread.sleep(100);
} catch (Exception e){
System.exit(0);
}
// se muestra el nuevo contador
txtGénérés.setText("");
txtGénérés.setText(""+compteurs[iThread]);
// seguimiento
System.out.println("Thread " + iThread + " : " + compteurs[iThread]);
}// se incrementa
}// clase
Analicemos el código:
- la ventana declara dos variables de instancia:
// variables de instancia
Thread[] tâches=null; // los subprocesos
int[] compteurs=null; // los contadores
La matriz tâches será la matriz de los subprocesos generados. La matriz compteurs se asociará a la matriz tâches. Cada tarea tendrá su propio contador para recuperar el valor del campo txtGénérés de la interfaz gráfica.
- Al hacer clic en el botón Générer, se ejecuta el método btnGénérer_actionPerformed.
- Este método comienza recuperando el número de subprocesos que se deben generar. Si es necesario, se señala un error si dicho número no es válido. A continuación, genera los subprocesos solicitados, asegurándose de anotar sus referencias en un array y asignando un número a cada uno de ellos. El método run de los subprocesos generados redirige al método incrémente de la clase. Se inician todos los subprocesos (start). También se crea la matriz de contadores asociados a los subprocesos.
- El método incrémente:
- lee el valor actual del campo txtGénérés y lo almacena en el contador correspondiente al hilo que se está ejecutando
- se detiene durante 100 ms, con el fin de liberar voluntariamente el procesador
- muestra el nuevo valor en el campo txtGénérés
Veamos ahora por qué el recuento de subprocesos es incorrecto. Supongamos que hay dos subprocesos que generar. Se ejecutan en un orden impredecible. Uno de ellos pasa primero y lee el valor 0 del contador. A continuación, lo cambia a 1, pero no lo escribe en la ventana: se interrumpe voluntariamente durante 100 ms. De este modo, pierde el control del procesador, que pasa a otro hilo. Este último actúa de la misma forma que el anterior: lee el contador de la ventana y recupera el 0 que sigue ahí. Pone el contador a 1 y, al igual que el anterior, se interrumpe durante 100 ms. El procesador vuelve entonces a asignarse al primer hilo: este va a escribir el valor 1 en el contador de la ventana y finaliza. El procesador se asigna ahora al segundo hilo, que también va a escribir 1. El resultado es incorrecto.
¿De dónde viene el problema? El segundo hilo ha leído un valor erróneo debido a que el primero se había interrumpido antes de haber terminado su trabajo, que consistía en actualizar el contador de la ventana. Esto nos lleva al concepto de recurso crítico y de sección crítica de un programa:
- un recurso crítico es un recurso que solo puede ser poseído por un hilo a la vez. En este caso, el recurso crítico es el contador 2 de la ventana.
- Una sección crítica de un programa es una secuencia de instrucciones en el flujo de ejecución de un hilo durante la cual este accede a un recurso crítico. Debemos asegurarnos de que, durante esta sección crítica, sea el único que tenga acceso al recurso.
7.6.2. Un recuento sincronizado por método
En el ejemplo anterior, cada hilo ejecutaba el método incrémente de la ventana. El método incrémente se declaraba de la siguiente manera:
Ahora lo declaramos de otra forma:
La palabra clave «synchronized» significa que solo un hilo a la vez puede ejecutar el método incrémente. Consideremos las siguientes notaciones:
- el objeto ventana F que crea los hilos en btnGénérer_actionPerformed
- dos subprocesos, T1 y T2, creados por F
Ambos subprocesos son creados por F y, a continuación, iniciados. Por lo tanto, ambos ejecutarán el método F.run. Supongamos que T1 llega primero. Ejecuta F.run y, a continuación, F.incremente, que es un método sincronizado. Lee el valor 0 del contador, lo incrementa y, a continuación, se detiene durante 100 ms. A continuación, se cede el procesador a T2, que a su vez ejecuta F.run y, después, F.incremente. Y ahí se bloquea porque el hilo T1 está ejecutando F.incremente y la palabra clave synchronized garantiza que solo un hilo a la vez pueda ejecutar F.incremente. A su vez, T2 pierde el control del procesador sin haber podido leer el valor del contador. Transcurridos los 100 ms, T1 recupera el procesador, muestra el valor 1 del contador, sale de F.incremente y, a continuación, de F.run, y finaliza. A continuación, T2 recupera el procesador y, esta vez, puede ejecutar F.incremente, ya que T1 ya no está ejecutando este método. A continuación, T2 lee el valor 1 del contador, lo incrementa y se detiene durante 100 ms. Transcurridos 100 ms, recupera el procesador, muestra el valor 2 del contador y también finaliza su ejecución. En esta ocasión, el valor obtenido es correcto. A continuación se muestra un ejemplo probado:

7.6.3. Recuento sincronizado mediante un objeto
En el ejemplo anterior, el acceso al contador txtGénérés se sincronizó mediante un método. Si la ventana que crea los hilos se llama F, también se puede decir que el método F.incremente representa un recurso que solo debía ser utilizado por un único hilo a la vez. Por lo tanto, se trata de un recurso crítico. El acceso sincronizado a este recurso se ha garantizado mediante la palabra clave synchronized:
También se podría decir que el recurso crítico es el propio objeto F. Esto es más estricto que en el caso en el que el recurso crítico sea F.incremente. De hecho, en este último caso, si un hilo T1 ejecuta F.incremente, un hilo T2 no podrá ejecutar F.incremente, pero sí podrá ejecutar otro método del objeto F, ya sea sincronizado o no. En el caso de que el propio objeto F sea el recurso crítico, cuando un hilo T1 ejecuta una sección sincronizada de dicho objeto, cualquier otra sección sincronizada del objeto queda inaccesible para los demás hilos. Así, si un hilo T1 ejecuta el método sincronizado F.incremente, un hilo T2 no podrá ejecutar no solo F.incremente, sino también cualquier otra sección sincronizada de F, incluso aunque ningún hilo la esté utilizando. Por lo tanto, se trata de un método más restrictivo.
Supongamos, pues, que la ventana se convierte en el recurso crítico. En ese caso, escribiremos:
// incremento
private void incrémente(){
// sección crítica
synchronized(this){
// se obtiene el número del hilo
int iThread=0;
try{
iThread=Integer.parseInt(Thread.currentThread().getName());
}catch(Exception ex){}
// se lee el valor del contador de tareas
try{
compteurs[iThread]=Integer.parseInt(txtGénérés.getText());
} catch (Exception e){}
// se incrementa
compteurs[iThread]++;
// se espera 100 milisegundos; el hilo perderá entonces el control del procesador
try{
Thread.sleep(100);
} catch (Exception e){
System.exit(0);
}
// se muestra el nuevo contador
txtGénérés.setText("");
txtGénérés.setText(""+compteurs[iThread]);
}//sincronizado
}// se incrementa
Todos los hilos utilizan la ventana this para sincronizarse. Al ejecutarlo, se obtienen los mismos resultados correctos que antes. De hecho, es posible sincronizarse con cualquier objeto conocido por todos los hilos. A continuación se muestra, por ejemplo, otro método que ofrece los mismos resultados:
// variables de instancia
Thread[] tâches=null; // los hilos
int[] compteurs=null; // los contadores
Object synchro=new Object(); // un objeto de sincronización de subprocesos
// incrementa
private void incrémente(){
// sección crítica
synchronized(synchro){
..............
}//sincronizado
}// incrementado
La ventana crea un objeto de tipo Object que servirá para la sincronización de los hilos. Este método es mejor que el que se sincroniza con el objeto this porque es menos restrictivo. En este caso, si un hilo T1 se encuentra en la sección sincronizada de incrémente yun hilo T2 quiera ejecutar otra sección sincronizada del mismo objeto «this», pero sincronizada mediante un objeto distinto de synchro, podrá hacerlo.
7.6.4. Sincronización por eventos
En este caso, utilizamos un valor booleano peutPasser para indicar a un hilo si puede o no entrar en una sección crítica. Un código sin sincronización podría ser el siguiente:
while(! peutPasser); // se espera a que peutPasser pase a verdadero
peutPasser=false; // ningún otro hilo debe pasar
section critique; // aquí el hilo está solo
peutPasser=true; // otro hilo puede pasar a la sección crítica
La primera instrucción, en la que un hilo entra en bucle a la espera de que peutPasser pase a verdadero, es poco elegante: el hilo ocupa el procesador innecesariamente. A esto se le denomina «espera activa». Se puede mejorar el código de la siguiente manera:
while(! peutPasser){ // se espera a que peutPasser pase a verdadero
Thread.sleep(100); // parada durante 100 ms
}
peutPasser=false; // No debe pasar ningún otro hilo
section critique; // aquí el hilo está solo
peutPasser=true; // otro hilo puede pasar a la sección crítica
El bucle de espera es mejor en este caso: si el hilo no puede pasar, entra en estado de espera durante 100 ms antes de volver a comprobar si puede pasar o no. Mientras tanto, el procesador se asignará a otros hilos del sistema.
De hecho, estos dos métodos son incorrectos: no impiden que dos hilos entren al mismo tiempo en la sección crítica. Supongamos que un hilo T1 detecta que peutPasser es verdadero. A continuación, pasará a la siguiente instrucción, donde volverá a establecer peutPasser en falso para bloquear a los demás hilos. Sin embargo, es muy posible que se vea interrumpido en ese momento, ya sea porque se ha agotado su tiempo de procesador, porque una tarea con mayor prioridad ha solicitado el procesador o por cualquier otra razón. El resultado es que pierde el control del procesador. Lo recuperará un poco más tarde. Mientras tanto, otras tareas obtendrán el control del procesador, entre ellas quizá un hilo T2 que entra en bucle a la espera de que peutPasser pase a verdadero. Este también descubrirá que peutPasser está en «verdadero» (el primer hilo no ha tenido tiempo de ponerlo en «falso») y también pasará a la sección crítica. Lo cual no debía ocurrir.
La secuencia
while(! peutPasser){ // se espera a que peutPasser pase a verdadero
try{
Thread.sleep(100); // parada de 100 ms
} catch (Exception e) {}
}// while
peutPasser=false; // ningún otro hilo debe pasar
es una secuencia crítica que hay que proteger mediante sincronización. Siguiendo el ejemplo anterior, podemos escribir:
synchronized(synchro){
while(! peutPasser){ // se espera a que peutPasser pase a verdadero
try{
Thread.sleep(100); // pausa de 100 ms
} catch (Exception e) {}
}//while
peutPasser=false; // ningún otro hilo debe pasar
}// sincronizado
section critique; // aquí el hilo está solo
peutPasser=true; // otro hilo puede pasar por la sección crítica
Este ejemplo funciona correctamente. Se puede mejorar evitando la espera semiactiva del hilo cuando supervisa periódicamente el valor del booleano peutPasser. En lugar de despertarse periódicamente cada 100 ms para comprobar el estado de peutPasser, puede entrar en estado de suspensión y solicitar que se le despierte cuando peutPasser sea verdadero. Esto se escribe de la siguiente manera:
synchronized(synchro){
if (! peutPasser) {
try{
synchro.wait(); // si no se puede pasar, entonces se espera
} catch (Exception e){
…
}
}
peutPasser=false; // ningún otro hilo debe pasar
}// sincronizado
La operación synchro.wait() solo puede ser realizada por un hilo que sea «propietario» momentáneo del objeto synchro. En este caso, la secuencia es:
synchronized(synchro){
…
}// sincronizado
la que garantiza que el hilo sea propietario del objeto synchro. Mediante la operación synchro.wait(), el hilo cede la propiedad del bloqueo de sincronización. ¿Por qué? Por lo general, porque carece de los recursos necesarios para seguir trabajando. Así que, en lugar de bloquear a los demás hilos que esperan el recurso synchro, lo cede y pasa a esperar el recurso que le falta. En nuestro ejemplo, espera a que el valor booleano peutPasser pase a verdadero. ¿Cómo se le notificará este evento? De la siguiente manera:
synchronized(synchro){
if (! peutPasser) {
try{
synchro.wait(); // si no se puede pasar, entonces se espera
} catch (Exception e){
…
}
}
peutPasser=false; // ningún otro hilo debe pasar
}// sincronizado
section critique...
synchronized(synchro){
synchro.notify();
}
Consideremos el primer hilo que pasa por el bloqueo de sincronización. Llamémoslo T1. Imaginemos que encuentra el valor booleano peutPasser en verdadero, ya que es el primero. Por lo tanto, lo cambia a falso. A continuación, sale de la sección crítica bloqueada por el objeto synchro. Otro hilo podrá entonces entrar en la sección crítica para comprobar el valor de peutPasser. Lo encontrará falso y, a continuación, entrará en espera de un evento (wait). Al hacerlo, cede la propiedad del objeto synchro. Entonces, otro hilo puede entrar en la sección crítica: este también se pondrá en espera porque peutPasser es falso. Por lo tanto, puede haber varios hilos en espera de un evento en el objeto synchro.
Volvamos al hilo T1, al que le ha tocado el turno. Este ejecuta la sección crítica y, a continuación, indica que ahora puede pasar otro hilo. Lo hace con la secuencia:
synchronized(synchro){
synchro.notify();
}
Primero debe recuperar el objeto synchro mediante la instrucción synchronized. Esto no debería suponer ningún problema, ya que compite con hilos que, si obtienen momentáneamente el objeto synchro, deben abandonarlo mediante un wait porque detectan que peutPasser es falso. Por lo tanto, nuestro hilo T1 acabará obteniendo la propiedad del objeto synchro. Una vez hecho esto, indica mediante la operación synchro.notify que uno de los hilos bloqueados por un synchro.wait debe ser reactivado. A continuación, vuelve a ceder la propiedad del objeto synchro, que pasará entonces a uno de los hilos en espera. Este continúa su ejecución con la instrucción que sigue a la wait que lo había puesto en espera. A su vez, ejecutará la sección crítica y ejecutará una synchro.notify para liberar otro hilo. Y así sucesivamente.
Veamos este modo de funcionamiento con el ejemplo del recuento que ya hemos estudiado.
void btnGénérer_actionPerformed(ActionEvent e) {
//generación de subprocesos
// se lee el número de hilos que se van a generar
int nbThreads=0;
try{
// lectura del campo que contiene el número de subprocesos
nbThreads=Integer.parseInt(txtAGénérer.getText().trim());
// positivo >
if(nbThreads<=0) throw new Exception();
}catch(Exception ex){
//error
txtStatus.setText("Nombre invalide");
// se vuelve a intentar
txtAGénérer.requestFocus();
return;
}//captura
// RAZ contador de tareas
txtGénérés.setText("0"); // contador de tareas a 0
// El primer hilo puede pasar
peutPasser=true;
// se generan y se inician los hilos
tâches=new Thread[nbThreads];
compteurs=new int[nbThreads];
for(int i=0;i<tâches.length;i++){
// se crea el hilo i
tâches[i]=new Thread() {
public void run() {
synchronise();
}
};//hilo i
// se define su nombre
tâches[i].setName(""+i);
// se inicia su ejecución
tâches[i].start();
}//for
}//generar
Ahora, los subprocesos ya no ejecutan el método incrémente, sino el siguiente método synchronise:
// etapa de sincronización de los hilos
public void synchronise(){
// se solicita acceso a la sección crítica
synchronized(synchro){
try{
// ¿Se puede pasar?
if(! peutPasser){
System.out.println(Thread.currentThread().getName()+ " en attente");
synchro.wait();
}
// se ha pasado; se impide el paso a los demás hilos
peutPasser=false;
} catch(Exception e){
txtStatus.setText(""+e);
return;
}//try
}// synchronized
// sección crítica
System.out.println(Thread.currentThread().getName()+ " passé");
incrémente();
// Hemos terminado; liberamos cualquier hilo que pudiera estar bloqueado a la entrada de la sección crítica
peutPasser=true;
System.out.println(Thread.currentThread().getName()+ " terminé");
synchronized(synchro){
synchro.notify();
}// sincronizado
} // sincroniza
El método synchronise tiene como objetivo procesar los hilos uno por uno. Para ello, utiliza una variable de sincronización synchro. El método incrémente ya no está protegido por la palabra clave synchronized:
// incrementa
private void incrémente(){
// se recupera el número del hilo
int iThread=0;
try{
iThread=Integer.parseInt(Thread.currentThread().getName());
}catch(Exception ex){}
// se lee el valor del contador de tareas
try{
compteurs[iThread]=Integer.parseInt(txtGénérés.getText());
} catch (Exception e){}
// se incrementa
compteurs[iThread]++;
// se espera 100 milisegundos; el hilo perderá entonces el control del procesador
try{
Thread.sleep(100);
} catch (Exception e){
System.exit(0);
}
// se muestra el nuevo contador
txtGénérés.setText("");
txtGénérés.setText(""+compteurs[iThread]);
}// se incrementa
Para 5 subprocesos, los resultados obtenidos son los siguientes:
0 passé
1 en attente
2 en attente
3 en attente
4 en attente
0 terminé
1 passé
1 terminé
2 passé
2 terminé
3 passé
3 terminé
4 passé
4 terminé
Supongamos que T0 a T4 son los 5 subprocesos generados por la aplicación. T0 es el primero en adquirir la propiedad del bloqueo synchro y encuentra que peutPasser es verdadero. Establece peutPasser en falso y continúa: ese es el significado del primer mensaje passé. Con toda probabilidad, continúa y ejecuta la sección crítica, en concreto el método incrémente. En este, se pone en espera durante 100 ms (sleep). Por lo tanto, libera el procesador. Este se asigna a otro hilo, el hilo T1, que obtiene entonces la propiedad del objeto synchro. Descubre que no puede pasar y entra en espera (wait). A continuación, libera la propiedad del objeto synchro, así como el procesador. Este se asigna al hilo T2, que sufre el mismo destino. Durante los 100 ms que T0 permanece inactivo, los hilos T1 a T4 quedan, por tanto, en espera. Este es el significado de los cuatro mensajes «en espera». Transcurridos 100 ms, T0 recupera el procesador y finaliza su trabajo: este es el significado del mensaje «0 terminé». A continuación, libera uno de los subprocesos bloqueados y finaliza. El procesador liberado se asigna entonces a un hilo disponible: el que acaba de ser liberado. En este caso, es T1. El hilo T1 entra entonces en la sección crítica: ese es el significado del mensaje «1 passé». Hace lo que tiene que hacer y, a su vez, se detiene durante 100 ms. El procesador queda entonces disponible para otro hilo, pero todos están a la espera de un evento: ninguno de ellos puede hacerse con el procesador. Transcurridos 100 ms, el hilo T1 recupera el procesador y finaliza: ese es el significado del mensaje «1 terminé». Los hilos T1 a T4 tendrán el mismo comportamiento que T1: este es el significado de las tres series de mensajes: «pasado», «finalizado».



