La concurrencia es la ejecución de diferentes tareas por la CPU de un ordenador, compartiendo los recursos del sistema entre ellas (memoria, ficheros, periféricos, etc). Cuando hablamos de concurrencia podemos hablar de procesos o de hilos.
Un programa se puede definir como un conjunto de instrucciones que persiguen una finalidad, diseñados es un lenguaje de programación. Un proceso es un programa en estado de ejecución. Consiste en el código del programa, los datos, un contador de programa, etc.
Es la capacidad de ejecutar varios procesos de forma simultánea. Los Sistemas Operativos actuales disponen de mecanismos que permiten la ejecución de muchos procesos. A estos SSOO les llamamos multitarea.
Para que los distintos procesos se puedan ejecutar concurrentemente, el sistema operativo debe parar e iniciar los procesos para irles asignando tiempo de ejecución. Cuando se para un proceso para ejecutar otro, se debe guardar el estado del proceso para reanudar su ejecución posteriormente. Cada vez que el sistema detiene un proceso para ejecutar otro, debe guardar su información es una estructura de datos llamado Bloque de Control de Proceso. A este proceso se le conoce como cambio de contexto:
Los hilos permiten ejecutar diferentes tareas dentro de un mismo proceso, mejorando el rendimiento de la CPU. En este caso una misma aplicación puede ejecutar diferentes hilos de forma concurrente que se encargan de diferentes tareas. Todos estos hilos comparten recursos del proceso.
En entornos concurrentes, un algoritmo de planificación se encarga de elegir qué proceso o hilo se ejecutará a continuación. Un algoritmo de planificació debe:
El primer proceso que llegue al procesador se ejecuta antes y de forma completa. Hasta que su ejecución no termina no podrá pasarse a ejecutar otro proceso.
Se le conoce también como algoritmo de turno rotatorio. En este caso se designa una cantidad corta de tiempo (quantum) de procesamiento a todas las tareas. Las que necesiten más tiempo de proceso deberán esperar a que vuelva a ser su turno para seguir ejecutándose.
En este algoritmo, de todos los procesos listos para ser ejecutados, lo hará primero el más corto
De todos los procesos listos para ejecución, se ejecutará aquel al que le quede menos tiempo para terminar.
Es un algoritmo más complejo que todos los anteriores y, por tanto, más realista. Se utiliza en entornos donde se desconoce el tiempo de ejecución de un proceso al inicio de su ejecución. En este caso, el sistema dispone de varias colas que a su vez pueden disponer de diferentes políticas unas de otras. Los procesos van pasando de una cola a otra hasta que terminan su ejecución. En algunos casos, el algoritmo puede adaptarse modificando el número de colas, su política, . . .
Es la programación de aplicaciones capaces de realizar varias tareas de forma simultánea utilizando hilos o threads. En este caso todas las tareas compiten por el uso del procesador (lo más habitual es disponer sólo de uno) y en un instante determinado sólo una de ellas se encuentra en ejecución. Además, habrá que tener en cuenta que diferentes hilos pueden compartir información entre sí y eso complica mucho su programación y coordinación.
Es la programación de aplicaciones que ejecutan tareas de forma paralela, de forma que no compiten por el procesador puesto que cada una de ellas se ejecuta en uno diferente. Normalmente buscan resultados comunes dividiendo el problema en varias tareas que se ejecutan al mismo tiempo.
Es la programación de aplicaciones en las que las tareas a ejecutar se reparten entre varios equipos diferentes (conectados en red, a los que llamaremos nodos). Juntos, estos equipos, forman lo que se conoce como un Sistema Distribuido, que busca formar redes de equipos que trabajen con un fin común 1)
A continuación, tenemos un esquema del desarrollo del bloque, a través de las diferentes etapas hasta la práctica final.
Los hilos o Threads son procesos ligeros que se ejecutan simultáneamente en un mismo proceso. La ejecución simultanea de diferentes hilos mejora el rendimiento y la utilización de la CPU ya que podemos realizar varias tareas al mismo tiempo. Todo programa usa como mínimo un hilo de ejecución que se conoce como hilo principal. Si no creamos explicitamente ningún otro hilo, dicho programa solo podrá ejecutar tareas secuencialmente.
Cada lenguaje tiene su propia API para trabajar con hilos; en Java podemos crear hilos implementando la interface Runnable
o heredando de la clase Thread
.
En un entorno multihilo, el planificador de hilos de Java, que forma parte de la JVM, se encarga de asignar un periodo de tiempo de ejecución a cada hilo de modo que todos puedan ejecutarse concurrentemente.
Aquí se ejecuta únicamente un hilo llamado “hilo principal”. En este hilo hemos programado siempre, sin conocimiento de que estábamos trabajando ya con hilos. También, puede ser usado para hacer cálculos u otros procesamientos complejos, aunque estos deberían de evitarse en este hilo. Puedo comprobar si estoy en el hilo principal:
Thread.currentThread().getName();
En inglés background, es donde se ejecutan el resto de hilos. El segundo plano tiene la característica de darse en el mismo momento que el primer plano. Estos hilos deben llevar las ejecuciones pesadas de la aplicación. El segundo plano es la ejecución que el usuario no ve, es más, ni le interesa; para el usuario no existe.
Para la creación de hilos en Java disponemos de varias vías, combinando el uso de la clase y el interface según nos interese:
Debemos crear una clase que extienda de Thread
y acordarnos de sobrescribir el método run()
. Es la forma más sencilla pero tiene la limitación de que en Java, cada clase solo puede extender de una única clase.
Para iniciar un hilo debemos crear un objeto de nuestra nueva clase y llamar a su método start()
que hereda de la clase Thread.
public class Hilo extends Thread { private int id; public Hilo(int id){ this.id = id; } @Override public void run() { System.out.println("Soy el hilo numero " + id); } } . . . public class HiloPrincipal { public static void main(String args[]) { Hilo hilo = null; for( int i = 0; i < 3; i++){ hilo = new Hilo(i); hilo.start(); } System.out.println("Fin del hilo principal"); } }
Iniciar y parar hilo: Extendiendo de Thread
Nos permite seguir extendiendo de otra clase así como implementar otras interfaces. Además, el compilador nos fuerza a sobrescribir el método run()
.
En este caso debemos crear un objeto Thread
por cada hilo que necesitemos y pasarle nuestro objeto Runnable
. Esto nos permite compartir el mismo objeto Runnable
para cada hilo, muy útil cuando necesitamos que diferentes hilos compartan elementos del mismo objeto Runnable.
public class Tarea implements Runnable { private int id; public Hilo(int id){ this.id = id; } @Override public void run() { System.out.println("Soy el hilo numero " + id); } } . . . public class Programa { public static void main(String args[]) { Tarea tarea1 = new Tarea(1); Thread hilo1 = new Thread(tarea1); System.out.println("Inicio del Hilo principal"); hilo1.start(); System.out.println("Fin del hilo principal"); } }
Iniciar y parar hilo: Implementando Runnable
En algunas situaciones puede resultar más cómodo crear un hilo utilizando una clase anónima. Esto se recomienda cuando la clase no tiene una estructura muy compleja.
La ventaja es que, desde el método run()
se puede acceder directamente a miembros de la clase principal, sin necesidad de recibir parámetros.
public class Principal { public static void main(String args[]) { Thread hilo = new Thread(new Runnable() { @Override public void run() { System.out.println("Hilo secundario"); } }); hilo.start(); } }
Crear hilo mediante clase anónima
También se puede utilizar las expresiones lambda, o mediante una clase interna (definición de una clase dentro de otra):
//Usando lambdas (a partir de Java 8): similar a clase anónima Thread hilo = new Thread(() -> { //Contenido del método run() System.out.println("Hilo secundario"); }); hilo.start(); //Usando una clase interna public class Programa{ private int contador; private String cadena; . . . class Hilo implements Runnable{ @Override public void run(){ System.out.println("soy un hilo y accedo al campo cadena: " + cadena); } } }
Thread
cada hilo crea un objeto único asociado. Cuando implementamos Runnable
podemos reutilizar el mismo objeto Runnable con diferentes hilos.Override
) el método run()
e implementar allí lo que tiene que hacer el hilo.sleep()
para no sobrecargar la CPU.Contiene una serie de constructores y métodos para manejar hilos. Hay varios métodos que están obsoletos y que no debemos utilizar.
Constructor | Finalidad |
---|---|
Thread(Runnable objetoRunnable) | Crea un hilo a partir del objeto Runnable |
Thread(String name) | Crea un hilo y le da valor a su propiedad name |
Thread(Runnable objetoRunnable, String name) | Crea un hilo a partir del objeto Runnable y da nombre al hilo |
Método | Finalidad |
---|---|
void start() | Inicia el hilo. La JVM llama al método run() |
boolean isAlive() | Comprueba si se ha terminado el método run() |
int getId() | Devuelve el id del hilo |
String getName() | Devuelve el nombre del hilo |
void setName(String name) | Asigna un nombre al hilo |
void join() | Se espera la terminación del hilo que invoca a este método antes de continuar |
static void sleep(long ms) | Hace que el hilo espere la cantidad de ms |
static void yield() | Detiene temporalmente el hilo y permite que se ejecute otro |
static Thread currentThread() | Indica el hilo actual en ejecución |
void interrupt() | Desbloquea un hilo bloqueado (wait(),sleep(), join(), y activa el flag interupt status |
static boolean isInterrupted() | Comprueba el estado del flag interrupt status, y lo resetea |
En Java, cada hilo de jecución pasa por diferentes estados como vemos en la siguiente imagen:
La clase Thread
tiene el método getState()
el cual devuelve el valor del estado actual del hilo:
start()
)run()
run()
o debido a alguna excepción no capturada.sleep()
, wait()
, join()
…Hace referencia al enfoque que debemos tomar a la hora de programar aplicaciones en las que diferentes hilos comparten el mismo proceso y no genera situaciones inesperadas.
El API de Java proporciona una serie de métodos en la clase Thread
para la sincronización de los hilos en una aplicación:
Thread.sleep(int)
El hilo que ejecuta esta llamada permanece dormido durante el tiempo especificado como parámetro (en ms)join()
Se espera la terminación del hilo que invoca a este método antes de continuarisAlive()
Comprueba si el hilo permanece activo todavía (no ha terminado su ejecución)yield()
Sugiere al scheduler que sea otro hilo el que se ejecute (no se asegura)
En este caso el hilo duerme (detiene su ejecución) durante el tiempo especificado (en ms). Durante ese periodo podrán ejecutarse otros hilos. Propaga una excepción de clase InterruptedException
que debo controlar:
public class Tarea implements Runnable { @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println("Soy un hilo y esto es lo que hago"); // Controlar la excepción Thread.sleep(500); } } }
El método join()
hace que el hilo desde el que se invoca al método espera a la finalización del hilo sobre el que se invoca. Propaga una excepción de clase InterruptedException
que debo controlar.
En este ejemplo, el hilo principal inicia dos hilos e invoca al método join() sobre los dos: espera a que ambos hilos se hayan ejecutado para continuar (o para lo que sea). Los hilos 1 y 2 se ejecutan simultáneamente.
public static void main(String args[]) { Hilo hilo1 = new Thread(new Tarea()); Hilo hilo2 = new Thread(new Tarea()); hilo1.start(); hilo2.start(); . . . . . . hilo1.join(); hilo2.join(); System.out.println("Fin de la ejecución del hilo principal y de los dos hilos"); }
En el siguiente caso, el hilo principal (hilo main) espera a que termine el hilo1 para iniciar el hilo2, y luego espera a que hilo2 termine: los hilos 1 y 2 se ejecutan uno después de otro.
public static void main(String args[]) { Hilo hilo1 = new Thread(new Tarea()); Hilo hilo2 = new Thread(new Tarea()); hilo1.start(); hilo1.join(); hilo2.start(); hilo2.join(); System.out.println("Fin de la ejecución del hilo principal y de los dos hilos"); }
Sincronización de hilos desde el hilo main
Sincronización interna entre hilos, sin depender del hilo main
isAlive()
está indicando que el hilo está vivo (ha iniciado su ejecución y aún no ha muerto, puede estar en cualquier estado intermedio, incluso durmiendo). Nos permite mantener la ejecución de un hilo mientras otro siga en ejecución:
public class TareaPrincipal implements Runnable { @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println("Soy la TarePrincipal"); try { Thread.sleep(500); } catch (InterruptedException ie) { ie.printStackTrace(); } } } } . . . public class TareaAlive implements Runnable { private Thread otroHilo; public TareaAlive(Thread otroHilo) { this.otroHilo = otroHilo; } @Override public void run() { while (otroHilo.isAlive()) { System.out.println("Yo hago cosas mientras el otro hilo siga en ejecución"); try { Thread.sleep(500); } catch (InterruptedException ie) { ie.printStackTrace(); } } System.out.println("El otro hilo ha terminado. Yo también"); } } . . . public class Programa { public static void main(String args[]) { TarePrincipal tareaPrincipal = new TareaPrincipal(); Thread hiloPrincipal = new Thread(tareaPrincipal); TareaAlive tareaAlive = new TareaAlive(hiloPrincipal); Thread hiloAlive = new Thread(tareaAlive); hiloPrincipal.start(); hiloAlive.start(); System.out.println("Se han terminado los dos hilos?"); } }
Mantener la ejecución de un hilo dependiendo del estado de otro hilo
Dado que ciertos métodos de la clase Thread
para alterar la ejecución de un hilo están obsoletos por no ser seguros, Java nos anima a que seamos nosotros quienes implementemos formas limpias de detener, o pausar y reanudar nuestros hilos.
La forma más sencilla de controlar la ejecución de un hilo que realiza tareas iterativas es utilizando una variable de control, la cual modifico cuando quiera parar el hilo:
private boolean ejecucion; . . . public void parar(){ this.ejecucion = false; } @Override public void run() { while(ejecucion){ //Realizo las tareas } } // En caso de usar un bucle for @Override public void run() { for(int i = 0; i < cantidad; i++) //Realizo las tareas . . . if(!ejecucion){ break; } } }
También podría pausar un hilo de forma similar a la anterior. Hay que tener en cuenta que si paro el hilo también debo parar la pausa.
@Override public void run() { while(ejecucion){ //Realizo las tareas . . . // Si activo la pausa permanezco en el bucle while(pausado){ //Evito que la comprobación de pausa acapare la cpu Thread.sleep(200); //Compruebo si se detiene el hilo mientras está pausado if(!ejecucion){ pausado = false; return; } } } }
Si las condiciones de parada o de pausa son comunes para varios hilos, debo optar por el uso de una variable atómica de control para asegurar un sistema Thread-safe.
En el package java.util.concurrent.atomic
tenemos varias clases que nos permiten crear objetos Thread-Safe. Algunas de estas clases son AtomicBoolean o AtomicInteger. Usar este tipo de datos permite acceder y modificarlos de por diferentes hilos de forma segura.
Podemos acceder o modificar sus valores de forma segura mediante sus métodos set()
y get()
.
AtomicBoolean ejecucion = new AtomicBoolean(); ejecucion.set(true); while(ejecucion.get()){ . . . }
Si un hilo está en una situación de bloqueo porque ha ejecutado wait()
, sleep()
, join()
, etc, no podrá comprobar el estado de una variable de control ya que está bloqueado. Si el bloqueo dura un largo tiempo, debo utilizar el método interrupt().
El método interrupt()
no detiene un hilo, sino que solicita al hilo que se detenga en el primer momento que pueda. Es el programador quien debe gestionar esa solicitud.
interrupt()
funciona del siguiente modo:
InterruptedException
y continuando su ejecución.interrupt()
no afecta a la ejecución pero modifica el estado de una variable de control (interrupt status) a true.Thread.interrupted()
:@Override public void run() { for (int i = 0; i < cantidad; i++) { //Operaciones . . . if (Thread.interrupted()) { // Se ha interrumpido, terminamos ejecución return; } } }
Utilizando las dos formas vistas anteriormente para parar la ejecución de un hilo podemos plantear una solución del siguiente modo:
public class HiloTerminable implements Runnable { private Thread hilo; private AtomicBoolean ejecucion; public HiloTerminable () { ejecucion = new AtomicBoolean(false); } public void start() { hilo = new Thread(this); hilo.start(); } public void stop() { ejecucion.set(false); hilo.interrupt(); } public void run() { ejecucion.set(true); while (ejecucion.get()) { try { Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); System.out.println("El hilo fue terminado por el usuario"); } // Operaciones del hilo . . . } ejecucion.set(false); } }
Un mecanismo de exclusión mutua, mutex o monitor evita que una conjunto de operaciones puedan ser ejecutadas por más de un hilo al mismo tiempo. Protege el acceso a una sección de código (sección crítica) de un acceso concurrente de diversos hilos. La palabra synchronized
nos permite crear un monitor:
public class Sincrona{ private Object monitor; . . . //Solo un hilo puede ejecutar el siguiente método al mismo tiempo //El resto de hilos permanecen bloqueados hasta que termina su ejecución public synchronized void accesoExclusivo(){ // Operaciones } . . . //Lo mismo ocurre con este bloque synchronized(monitor){ // Operaciones } }
Al utilizar un monitor (synchronized), Java nos permite utilizar algunos métodos pertenecientes a la clase Object para sincronizar hilos:
wait()
: El hilo que invoca al método para su ejecución hasta que otro hilo llame a notify
o notifyAll
.notify()
: Continua la ejecución del hilo que invocó wait()
y desbloquea un hilo al azar para acceder al monitor.notifyAll()
: Continua la ejecución del hilo que invocó wait()
y desbloquea todos los hilos que quieren acceder al monitor.private boolean pausado; . . . @Override public void run() { for(int i = 0; i < cantidad; i++){ if(pausado){ esperar(); } //operaciones } } public synchronized void esperar(){ try { wait(); } catch (InterruptedException e) { System.out.println("Hilo parado por el usuario"); } } public void pausar(){ pausado = true; } public synchronized void continuar(){ pausado = false; notifyAll(); } . . . public static void main(String[] args){ Thread hiloPausado = new Thread(); hiloPausado.start(); //Suspendo su ejecución hiloPausado.pausar(); //Reanudo la ejecución hiloPausado.continuar(); }
En el ejemplo anterior vemos que podemos utilizar estos métodos para detener y reanudar la ejecución de un hilo.
La clase Collections
nos permite crear estructuras de datos seguras ante lecturas y modificaciones concurrentes a partir de una estructura de datos cualquiera:
ArrayList<Persona> lista = new ArrayList<>(); List<Persona> listaSincronizada = Collections.synchronizedList(lista); //Ahora los accesos y modificaciones son thread-safe listaSincronizada.add(persona1); listaSincronizada.get(2); listaSincronizada.remove(persona1);
Sin embargo, si lo que queremos es recorrer la colección, es el programador el que debe asegurar un acceso seguro a dicha iteracion:
//Indicamos ''synchronized'' para asegurar el acceso de un solo hilo public synchronized void recorrerLista(){ for(Persona persona : listaSincronizada){ persona.hacerOperacion(); } }
Como se detalla en la documentación de Java, Concurrencia en Swing, una aplicación con GUI es ejecutada por Java usando diferentes hilos, concretamente 3 tipos de hilos:
Si en una aplicación con GUI voy a realizar tareas de larga carga de proceso debo entender el funcionamiento del hilo EDT:
Todo el código de manejo de eventos, de visualización, y de actualización de componentes gráficos es ejecutado por Java en el Event-Dispatch Thread, el cual está diseñado para realizar operaciones cortas en respuesta a eventos.
Al ejecutar todos los manejos de eventos en el mismo hilo, Java se asegura que el bloque de operaciones en respuesta a un evento se ejecuta completamente antes de atender al siguiente evento. Si al responder a un evento, nuestro programa realiza tareas de larga ejecución, la ventana se congelará y no responderá hasta que el EDT termine de realizar dicha operación.
Para diseñar aplicaciones con GUIs Swing que sean Thread-Safe debo asegurar que toda intrucción que acceda o modifique el estado de la interfaz gráfica se ejecute en el hilo EDT2). Mientras que el acceso o modificación de componentes Swing se produzcan en los bloques de los listeners, no tendremos problema.
Podemos comprobar si un bloque de código se está ejecutando en el EDT, llamando al método isEventDispatchThread()
:
// Muestro la respuesta por consola a modo de comprobación System.out.println(SwingUtilities.isEventDispatchThread());
En resumen, nuestras aplicaciones con GUI deben respetar estas dos premisas:3)
Por lo tanto, como programador:
El problema puede surgir cuando realizamos modificaciones sobre la GUI desde hilos creados por nosotros, ya que estas operaciones dejarían de ejecutarse en el EDT. Para controlar que las operaciones sobre la GUI se ejecutan en el hilo destinado a tal fin (EDT) podemos usar:
Si en mi aplicación he implementado un hilo para realizar una tarea pesada y debo realizar operaciones que acceden o actualizan el estado de algún componente de la GUI, puedo añadir esa pequeña operación de acceso a la GUI a la cola de eventos para asegurarme que esas instrucciones se ejecuten en el EDT:
private JProgressBar pbProgreso; private JLabel lblContador; . . . // Método que responde a un evento sobre un botón @Override public void actionPerformed(ActionEvent e) { //Tarea en segundo plano de larga duración Thread hilo = new Thread(new Runnable() { @Override public void run() { for(int i = 1; i < tiempo; i++){ Thread.sleep(500); //Aseguro que la modificación de la GUI se hace en EDT actualizarGUI(i, tiempo); } } }); } . . . private void actualizarGUI(int cuenta, int total){ SwingUtilities.invokeLater(new Runnable() { @Override public void run() { lblContador.setText("Contador: " + cuenta); pbProgreso.setValue(cuenta * 100 / total); } }); }
El método actualizarGUI()
realiza lo siguiente:
invokeLater()
invokeLAter()
crea una tarea Runnable y la añade a la cola del hilo EDTSi programamos adecuadamente una GUI, las tareas del EDT serás cortas y rápidas, y la respuesta será inmediata.
En algunas situaciones puede que el método invokeLater()
no sea tan cómodo de utilizar, como cuando realizamos operaciones de larga duración. La clase SwingWorker
crea un hilo de ejecución, que nos permite acceder de forma segura a la GUI.
En aplicaciones con GUIs, varios hilos pueden actuar sobre el mismo elemento gráfico (una etiqueta de texto, por ejemplo) por lo que varios hilos compartirán ese recurso. Así, la clase SwingWorker
garantiza que el acceso a ese recurso compartido se hace de forma thread-safe, realizando las operaciones de acceso a la GUI en el Event Dispatch Thread.
La clase SwingWorker
tiene los siguientes métodos:
doInBackground()
: Método que realiza la tarea pesada del hilo. Permite llamar al método publish(). Es el único método obligatorio, ya que es abstracto.done()
: método que se ejecuta en el EDT, al terminar la ejecución de doInBackGround().Si queremos además contemplar el progreso de la tarea tenemos:
publish()
: para enviar valores desde doInBackground(), con los que actualizar componentes de swing.process()
: método que se ejecuta en el EDT, recibe una lista con los valores enviados por publish(). Se ejecuta regularmente.Visto esto, podemos actualizar componentes GUI al terminar la tarea (done()) y durante el progreso de la misma (process()).
Además el método doInBackground() permite devolver un objeto, por ejemplo el resultado de una carga de ficheros, o de una transferencia.
public class Tarea extends SwingWorker <Void, Integer> { private JProgressBar barraProgreso; private JLabel labelEstado; // Se pasa como parametro componentes swing public Tarea(JProgressBar barraProgreso, JLabel labelEstado) { this.barraProgreso = barraProgreso; this.labelEstado = labelEstado; } @Override protected Void doInBackground() throws Exception { // realiza una tarea pesada mediante un bucle (p.e. copiar un fichero) while(...){ . . . // Se notifica el avance (valor entre 0 y 100) publish(avanceCarga); } return null; } @Override protected void process(List<Integer> valores) { // En la lista valores obtengo lo que envía publish() // Se actualiza en el EDT de forma automática barraProgreso.setValue(valores.get(0)); } @Override protected void done() { // Se ejecuta en el EDT al terminar doInBackground() labelEstado.setText("Fichero copiado"); } } public class ProgramaGUI { . . . // Lanza la ejecucion de la tarea en segundo plano // También le paso los 2 componentes GUI que quiero actualizar en el EDT Tarea tarea = new Tarea(barra, label); tarea.execute(); }
En caso de querer finalizar las tareas del hilo worker por nuestra cuenta, podemos hacer uso del método cancel(true)
, el cual modifica el estado de ejecución de la tarea. Esto no detiene el hilo Worker directamente, sino que modificará un flag de estado que tiene la clase SwingWorker. Para comprobar el estado desde nuestra clase Worker, usaremos el método isCancelled()
.
. . . @Override protected Void doInBackground() throws Exception { // además de controlar el progreso, comprobamos se se ha llamado a cancel() while(... && !isCancelled()){ . . . //Realizo las tareas } return null; } . . . public static void main(String[] args){ TareaWorker tarea = new TareaWorker(); tarea.execute(); . . . tarea.cancel(true); }
Debemos de tener en cuenta que al cancelar la iteración de un bucle, el método doInBackground() terminará su ejecución. Y después se ejecutará también el método done()
, independientemente de si se ha llamado a cancel()
.
Clase SwingWorker
Es un manejador de eventos que puedo vincular a cualquier tipo de objeto del que quiera ser notificado ante algún cambio en su estado. En este caso voy a ver cómo puedo hacer que mi programa principal se entere de los cambios producidos en mi objeto SwingWorker, para poder actualizar la GUI desde un manejador de eventos que se ejecuta en el EDT.
La clase SwingWorker tiene dos propiedades4), cuyos cambios serán notificados al PropertyChangeListener:
progress
: se modifica mediante el método setProgress()
de la clase SwingWorker y admite valores entre 0-100.state
: indica el estado del objeto SwingWorker:doInBackground()
doInBackground()
doInBackground()
.
Además, SwingWorker nos permite generar eventos PropertyChangeEvent
en cualquier otro momento usando el método:
firePropertyChange()
Notificar cambios en un objeto:
public class Tarea extends SwingWorker <Void, Integer> { @Override public Void doInBackground() throws Exception { while(condicion){ // Tarea pesada . . . // Se notifica el avance modificando la propiedad "progress" setProgress(avanceCarga); } //Si la tarea se cancela sin llegar a terminar, creo un evento if(!isCancelled()){ firePropertyChange("cancelada", false, true); } } } . . . public class ProgramaGUI { . . . Tarea tarea = new Tarea(); tarea.addPropertyChangeListener(new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent event) { if (event.getPropertyName().equals("progress")) { int valor = (Integer) event.getNewValue(); // Pintar este valor en barra de progreso o similar } else if (event.getPropertyName().equals("cancelada")) { //Tarea cancelada: notificarlo en la GUI } } }); tarea.execute(); }
Actualizar GUI con PropertyChangeListener
A continuación se muestra, utilizando la librería log4j, un ejemplo de aplicación donde se realizan una serie de trazas a lo largo de su ejecución. La aplicación está compuesta por las dos clases que se muestran a continuación, con el objetivo de mostrar la traza cuando son varias clases las que ejecutan código.
public class Aplicacion { private static final Logger logger = LogManager.getLogger(Aplicacion.class); public static void main(String args[]) { // Diferentes niveles de traza logger.trace("Aplicación iniciada"); logger.error("Error de algo"); logger.trace("Aplicación finalizada"); logger.debug("Información para depurar"); logger.warn("Esto es un aviso"); OtraClase unObjeto = new OtraClase(); unObjeto.unMetodo(); try { // Forzamos una excepción para registrar su traza con log4j int x = 5 / 0; } catch (Exception e) { logger.trace("Se ha producido una excepción"); // Almacena la traza de la excepción como String y lo registra con log4j StringWriter sw = new StringWriter(); e.printStackTrace(new PrintWriter(sw)); logger.error(sw.toString()); } } }
public class OtraClase { private static final Logger logger = LogManager.getLogger(OtraClase.class); public void unMetodo() { logger.trace("Se ha ejecutado el método unMetodo"); } }
Antes de poder ejecutar la aplicación, se ha creado un fichero mínimo de configuración para log4j creando el siguiente fichero XML donde se habilita la traza por Consola y Fichero con un patrón de mensaje determinado
<?xml version="1.0" encoding="UTF-8"?> <Configuration status="WARN"> <Appenders> <Console name="Console" target="SYSTEM_OUT"> <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/> </Console> <File name="Fichero" fileName="ejemplolog4j.log" bufferedIO="false" advertiseURI="file://ejemplolog4j.log" advertise="true"> <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/> </File> </Appenders> <Loggers> <Root level="trace"> <AppenderRef ref="Console"/> <AppenderRef ref="Fichero"/> </Root> </Loggers> </Configuration>
Así, para el ejemplo anterior, la traza resultante (tanto para consola como para el fichero ejemplolog4j.log
) sería la siguiente
10:46:27.049 [main] TRACE com.sfaci.ejemplolog4j.Aplicacion - Aplicación iniciada 10:46:27.051 [main] ERROR com.sfaci.ejemplolog4j.Aplicacion - Error de algo 10:46:27.051 [main] TRACE com.sfaci.ejemplolog4j.Aplicacion - Aplicación finalizada 10:46:27.051 [main] DEBUG com.sfaci.ejemplolog4j.Aplicacion - Información para depurar 10:46:27.051 [main] WARN com.sfaci.ejemplolog4j.Aplicacion - Esto es un aviso 10:46:27.051 [main] TRACE com.sfaci.ejemplolog4j.OtraClase - Se ha ejecutado el método unMetodo 10:46:27.052 [main] TRACE com.sfaci.ejemplolog4j.Aplicacion - Se ha producido una excepción 10:46:27.052 [main] ERROR com.sfaci.ejemplolog4j.Aplicacion - java.lang.ArithmeticException: / by zero at com.sfaci.ejemplolog4j.Aplicacion.main(Aplicacion.java:38) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:483) at com.intellij.rt.execution.application.AppMain.main(AppMain.java:134)
Todos los proyectos realizados en los videos y otros de ejemplo de este tema se pueden encontrar en el repositorio de hilos de Bibucket.
Los proyectos de los ejercicios que se vayan haciendo en clase estarán disponibles en el repositorio psp-ejercicios de BitBucket
© 2024 Santiago Faci y Fernando Valdeón