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 llamado hilo. La clase que modela un thread es la clase java.lang.Thread, de la cual a continuación se presentan algunas propiedades y métodos:
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 después de que se haya ejecutado el método start anterior | |
detiene la ejecución de un hilo durante n milisegundos | |
Operación bloqueante: espera a que termine 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 contar con el método run: por lo general, se utilizará 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());
// se cambia el nombre
main.setName("myMainThread");
// verificació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 tiene acceso a las características de este hilo mediante Thread.currentThread()
- el papel del método sleep. Aquí, el hilo que ejecuta main entra en suspensión regularmente 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 manera «simultánea» en diferentes hilos de ejecución. Cuando se dice que los thread se ejecutan de manera simultánea, a menudo se comete un error de lenguaje. Si la máquina solo tiene un procesador, como suele ser el caso, los thread comparten ese procesador: cada uno dispone de él, por turnos, durante un breve instante (unos milisegundos). Esto es lo que da la ilusión de paralelismo en la ejecución. El tiempo asignado a un thread depende de diversos factores, entre ellos su prioridad, que tiene un valor predeterminado pero que también puede fijarse mediante programación. Cuando un thread tiene acceso al 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 estado de suspensión durante un tiempo determinado (sleep)
- Un hilo T puede crearse de diversas maneras
- 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 método: public void run(). Por lo tanto, el argumento del constructor anterior es cualquier instancia de una clase que implemente este método run.
En el siguiente ejemplo, los hilos 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 es iniciada por T.start(): este método pertenece a la clase Thread y realiza una serie de inicializaciones; luego, inicia 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 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 hilo thread se ejecuta de manera autónoma. Se detendrá cuando la función run que está ejecutando haya terminado su trabajo.
- Se puede esperar a que finalice la ejecución del hilo T mediante T.join(). Aquí tenemos una instrucción bloqueante: el programa que la ejecuta queda bloqueado hasta que la tarea T haya terminado su trabajo. Esto también es un método de sincronización.
Analicemos el siguiente programa:
// uso de hilos
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 le asigna un nombre al hilo actual
main.setName("myMainThread");
// Inicio de main
System.out.println("début du thread " +main.getName());
// creación de hilos 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);
// 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 terminó 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 hilos 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 hilos, no se pueden extraer conclusiones generales. El sistema operativo cuenta aquí con 6 hilos y un procesador. Distribuirá el procesador entre estos 6 hilos 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 primero el método affiche. Se muestra el mensaje de inicio de ejecución y luego ejecuta el método sleep, que lo suspende durante 1 segundo. Al hacerlo, pierde el procesador, que queda así disponible para otro hilo. El ejemplo muestra que es el hilo 1 el que lo obtendrá. El hilo 1 seguirá el mismo recorrido, al igual que los demás hilos. Cuando termine el segundo de espera del hilo 0, su ejecución podrá reanudarse. El sistema le asigna 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 main
System.out.println("fin du thread " +main.getName());
// se detiene 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
Tan pronto como el método main ejecute la instrucción:
detiene todos los hilos de la aplicación y no solo el hilo main. El método main podría querer esperar a que terminen de ejecutarse los hilos que ha creado antes de finalizar él mismo. Esto se puede lograr 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());
// aplicación detenida
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 destacado 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 los hilos para nosotros 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 manera simultánea. Si tomamos el ejemplo de un servidor FTP (Protocolo de Transferencia de Archivos) que entrega 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 específico. 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 monitorear los eventos que ocurren en la ventana: cuando el usuario haga clic en el botón «Detener», habrá que detener el reloj. Aquí tenemos dos tareas paralelas y asincrónicas: el usuario puede hacer clic en cualquier momento.
Consideremos el momento en 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 se podría pensar que un método del hilo en el que se ejecuta la ventana podría entonces gestionar 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 espera de los eventos de la interfaz gráfica. Estos eventos ocurren y se colocan en una fila de espera para que la aplicación los procese una vez que el método que se está ejecutando actualmente haya finalizado. En nuestro ejemplo del reloj, el método siempre estará en ejecución, ya que solo al hacer clic en el botón «Detener» se puede detener. 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 ocurren en la ventana. Así 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 ciclo 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 {
...................
}
//Reemplazado, 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;
}//si
}
}
Cuando el usuario hace clic en el botón «Iniciar», se crea un hilo utilizando 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 ciclo hasta que se nos indique que nos detengamos
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);
}//intentar
}// mientras
}// runHorloge
El principio del método es el siguiente:
- muestra la hora actual en el cuadro de texto txtHorloge
- se detiene 1 segundo
- repite el paso 1, tras haber comprobado previamente el valor booleano 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 mediante 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 este documento directamente 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 puede mejorarse. Hemos dicho que, al cargar el applet, se ejecuta el método init y, a continuación, el método start, si existe. Además, cuando el usuario sale de la página, se ejecuta el método stop, si existe. Cuando regresa 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, agregamos 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);
}//si
}//iniciar
Además, hemos agregado 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 ciclo hasta que se nos indique que paremos
while( ! finHorloge){
// obtenemos 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 presiona el botón de detener
Thread horloge lancé // se presiona el botón de iniciar
Page stop // El applet se minimiza 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 ocurre 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 realizaba la tarea principal, que establecía un valor booleano para detener el hilo del reloj. Ahora abordamos el problema del acceso concurrente de tareas a recursos comunes, problema también conocido como «compartición de recursos». Para ilustrarlo, primero analizaremos un ejemplo.
7.6.1. Un conteo 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 deben 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 que se presentaron y sobre la aplicación misma |
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 deben 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. Aquí buscamos mostrar los problemas que surgen en una situación de este tipo. A continuación, un ejemplo de ejecución:

Se observa que se solicitó la generación de 1000 subprocesos y que solo se contaron 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
//Construir el marco
public interfaceSynchro() {
..........
}
//Inicializar el componente
private void jbInit() throws Exception {
......................
}
//Reemplazado, 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 deben 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 generaron subprocesos
txtGénérés.setText("0"); // contador de tareas en 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 hilos
int[] compteurs=null; // los contadores
La matriz tâches será la matriz de los hilos 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 por recuperar el número de subprocesos que se deben generar. Si es necesario, se reporta un error si ese 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 reenvía a la clase al método incrémente. 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 que pertenece al hilo que se está ejecutando
- se detiene durante 100 ms, con el fin de ceder voluntariamente el procesador
- muestra el nuevo valor en el campo txtGénérés
Ahora expliquemos por qué el conteo 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. Luego 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 opera de la misma manera que el anterior: lee el contador de la ventana y recupera el 0 que aún se encuentra allí. Cambia el contador a 1 y, al igual que el anterior, se interrumpe durante 100 ms. El procesador se le asigna nuevamente al primer hilo: este escribirá el valor 1 en el contador de la ventana y terminará. El procesador se asigna ahora al segundo hilo, que también escribirá 1. Se obtiene un resultado incorrecto.
¿De dónde viene el problema? El segundo hilo leyó un valor incorrecto debido a que el primero había sido interrumpido antes de terminar su tarea, 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 retenido 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 en tener acceso al recurso.
7.6.2. Un conteo 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 manera diferente:
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 luego iniciados. Por lo tanto, ambos ejecutarán el método F.run. Supongamos que T1 llega primero. Ejecuta F.run y luego F.incremente, que es un método sincronizado. Lee el valor 0 del contador, lo incrementa y luego se detiene durante 100 ms. A continuación, se le cede el procesador a T2, que a su vez ejecuta F.run y luego 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. T2, a su vez, pierde el control del procesador sin haber podido leer el valor del contador. Al cabo de 100 ms, T1 recupera el procesador, muestra el valor 1 del contador, sale de F.incremente y luego de F.run, y finaliza. T2 recupera entonces el procesador y esta vez puede ejecutar F.incremente, ya que T1 ya no está ejecutando este método. T2 lee entonces el valor 1 del contador, lo incrementa y se detiene durante 100 ms. Al cabo de 100 ms, recupera el procesador, muestra el valor 2 del contador y también finaliza. Esta vez, el valor obtenido es correcto. A continuación se muestra un ejemplo probado:

7.6.3. Recuento sincronizado por 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 hilo a la vez. Por lo tanto, se trata de un recurso crítico. El acceso sincronizado a este recurso se garantizó 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 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 objeto F sea en sí mismo el recurso crítico, cuando un hilo T1 ejecuta una sección sincronizada de ese objeto, cualquier otra sección sincronizada del objeto se vuelve 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 si ningún hilo la está utilizando. Por lo tanto, es un método más restrictivo.
Supongamos, pues, que la ventana se convierte en el recurso crítico. Entonces 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, por ejemplo, se muestra otro método que da 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 por 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 ingresar a 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 un bucle esperando a que peutPasser se vuelva verdadero, es poco eficiente: el hilo ocupa el procesador innecesariamente. A esto se le llama 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 aquí es mejor: si el hilo no puede pasar, se pone en espera durante 100 ms antes de volver a verificar 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 está en verdadero. Entonces 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 sea interrumpido en ese momento, ya sea porque se le acabó su tiempo de procesador, porque una tarea de mayor prioridad solicitó 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 tal vez un hilo T2 que queda en bucle esperando a que peutPasser pase a verdadero. Este también descubrirá que peutPasser está en «verdadero» (el primer hilo no tuvo tiempo de ponerlo en «falso») y también pasará a la sección crítica. Lo cual no debía suceder.
La secuencia
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
es una secuencia crítica que debe protegerse 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) {}
}//mientras
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 regularmente el valor del booleano peutPasser. En lugar de despertarse regularmente cada 100 ms para verificar el estado de peutPasser, puede ponerse en espera y solicitar que lo despierten 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 el «propietario» momentáneo del objeto synchro. En este caso, la secuencia es:
synchronized(synchro){
…
}// sincronizado
la cual garantiza que el hilo sea el propietario del objeto synchro. Mediante la operación synchro.wait(), el hilo cede la propiedad del bloqueo de sincronización. ¿Por qué sucede esto? Por lo general, porque le faltan recursos para seguir trabajando. Así que, en lugar de bloquear a los otros hilos que esperan el recurso synchro, lo cede y entra en espera del 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. Luego sale de la sección crítica bloqueada por el objeto synchro. Otro hilo podrá entonces ingresar a la sección crítica para comprobar el valor de peutPasser. Lo encontrará falso y, por lo tanto, entrará en espera de un evento (wait). Al hacerlo, cede la propiedad del objeto synchro. Entonces, otro hilo puede ingresar a la sección crítica: este también se pondrá en espera porque peutPasser está en falso. Por lo tanto, puede haber varios hilos en espera de un evento en el objeto synchro.
Volvamos al hilo T1, al que le tocó el turno. Este ejecuta la sección crítica y luego indica que otro hilo ya puede pasar. Lo hace con la secuencia:
synchronized(synchro){
synchro.notify();
}
Primero debe recuperar el objeto synchro con la instrucción synchronized. Esto no debería ser un problema, ya que compite con hilos que, si obtienen momentáneamente el objeto synchro, deben liberarlo mediante un wait porque detectan que peutPasser es inválido. Por lo tanto, nuestro hilo T1 terminará por obtener 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. Luego vuelve a ceder la propiedad del objeto synchro, la cual se le asignará a uno de los hilos en espera. Este continúa su ejecución con la instrucción que sigue al wait que lo había puesto en espera. A su vez, ejecutará la sección crítica y ejecutará un synchro.notify para liberar a 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 deben generar
int nbThreads=0;
try{
// lectura del campo que contiene el número de hilos
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 en 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 hilos 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();
}
// Ya pasamos; se impide el paso a los demás hilos
peutPasser=false;
} catch(Exception e){
txtStatus.setText(""+e);
return;
}//try
}// sincronizado
// sección crítica
System.out.println(Thread.currentThread().getName()+ " passé");
incrémente();
// Ya terminamos; liberamos cualquier hilo que pudiera estar bloqueado al entrar a 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 tomar 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 particular 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 entonces obtiene 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 en espera. Este es el significado de los cuatro mensajes «en espera». Al cabo de 100 ms, T0 recupera el procesador y termina su trabajo: ese es el significado del mensaje «0 terminé». A continuación, libera uno de los hilos 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 esperando un evento: ninguno de ellos puede tomar el procesador. Al cabo de 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: ese es el significado de las tres series de mensajes: «pasado», «terminado».



