Programación

1º DAM/DAW - Curso 2023-2024

User Tools

Site Tools


apuntes:concurrencia

Concurrencia

Programación multihilo

Multiproceso

El multiproceso consiste en la ejecución de varios procesos diferentes de forma simultánea para la realización de una o varias tareas relacionadas o no entre sí. En este caso, cada uno de estos procesos es una aplicación independiente. El caso más conocido es aquel en el que nos referimos al Sistema Operativo (Windows, Linux, MacOS, . . .) y decimos que es multitarea puesto que es capaz de ejecutar varias tareas o procesos (o programas) al mismo tiempo.

Multihilo

Hablamos de multihilo cuando se ejecutan varias tareas relacionadas o no entre sí dentro de una misma aplicación. En este caso no son procesos diferentes sino que dichas tareas se ejecutan dentro del mismo proceso del Sistema Operativo. A cada una de estas tareas se le conoce como hilo o thread (en algunos contextos también como procesos ligeros).

En ambos casos estaríamos hablando de lo que se conoce como Programación Concurrente. Hay que tener en cuenta que en ninguno de los dos casos la ejecución es realmente simultánea, ya que el Sistema Operativo es quién hace que parezca así, pero los ejecuta siguiendo lo que se conoce como algoritmos de planificación.

Algoritmos de planificación

En entornos multitarea, un algoritmo de planificación indica la forma en que el tiempo de procesamiento debe repartirse entre todas las tareas que deben ejecutarse en un momento determinado. Existen diferentes algoritmos de planificación, cada uno con sus ventajas e inconvenientes, pero todos intentan cumplir con los siguientes puntos:

  • Debe ser imparcial y eficiente
  • Debe minimizar el tiempo de respuesta al usuario, sobre todo en aquellos procesos o tareas más interactivas
  • Debe ejecutar el mayor número de procesos
  • Debe mantener un equilibrio en el uso de los recursos del sistema

Programación concurrente, paralela y distribuida

Programación concurrente

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.

Programación paralela

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.

Programación distribuida

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

concurrencia.jpg
Figure 1: Concurrencia
distribuida.jpg
Figure 2: Programación distribuida

¿Qué son los hilos?

Un hilo o thread es cada una de las tareas que puede realizar de forma simultánea una aplicación. Por defecto, toda aplicación dispone de un único hilo de ejecución, al que se conoce como hilo principal. Si dicha aplicación no despliega ningún otro hilo, sólo será capaz de ejecutar una tarea al mismo tiempo en ese hilo principal.

Así, para cada tarea adicional que se quiera ejecutar en esa aplicación, se deberá lanzar un nuevo hilo o thread. Para ello, todos los lenguajes de programación, como Java, disponen de una API para crear y trabajar con ellos.

En cualquier caso, es muy importante conocer los estados en los que se pueden encontrar un hilo. Estos estados se suelen representar mediante un gráfico como el que sigue:

Estados de un hilo

Figure 3: Estados de un hilo

Programación multihilo en Java

Creación de un hilo

Para la creación de hilos en Java disponemos de varias vías, combinando el uso de la clase Thread y el interface Runnable según nos interese:

  • Podemos utiliza la clase Thread heredando de ella. Es quizás la forma más cómoda porque una clase que hereda de Thread se convierte automáticamente en un hilo. Tiene una pega: esa clase ya no podrá heredera de ninguna otra, por lo que si la arquitectura de nuestra aplicación lo requiere ya no podríamos.
  • Si tenemos la limitación que acabamos de comentar para el primer caso, podemos implementar el interface Runnable de forma que la clase que nosotros estamos implementado podrá además heredar sin ninguna limitación. Sólo cambia un poco la forma de trabajar directamente con la clase hilo.
  • Por otra parte también podemos crear un hilo utilizando una clase anónima. No es un método que se recomiende pero en algunos casos, cuando la clase que hace de hilo no va a tener una estructura concreta es bastante cómodo hacerlo de esta manera.

Crear un hilo heredando de la clase Thread

public class Tarea extends Thread {
  @Override
  public void run() {
    for (int i = 0; i < 10; i++) {
      System.out.println("Soy un hilo y esto es lo que hago");
    }
  }
}
 
. . .
 
public class Programa {
  public static void main(String args[]) {
    Tarea tarea = new Tarea();
    tarea.start();
    System.out.println("Yo soy el hilo principal y sigo haciendo mi trabajo");
    System.out.println("Fin del hilo principal");
  }
}

Crear un hilo implementando la interfaz Runnable

public class OtraClase {
  . . .
  . . .
}
 
. . .
 
public class Tarea extends OtraClase 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");
    }
  }
}
 
. . .
 
public class Programa {
  public static void main(String args[]) {
    Tarea tarea = new Tarea();
    Thread hilo = new Thread(tarea);
    hilo.start();
    System.out.println("Yo soy el hilo principal y sigo haciendo mi trabajo");
    System.out.println("Fin del hilo principal");
  }
}

Crear un hilo implementando una clase anónima/expresión lambda

public class Programa {
  public static void main(String args[]) {
 
    Thread hilo = new Thread(new Runnable() {
      @Override
      public void run() {
        for (int i = 0; i < 10; i++) {
        System.out.println("Soy un hilo y esto es lo que hago");
      }
    });
 
    hilo.start();
    System.out.println("Yo soy el hilo principal y sigo haciendo mi trabajo");
    System.out.println("Fin del hilo principal");
  }
}

En cualquier caso tenemos que tener siempre en cuenta las siguientes consideraciones:

  • Siempre se debe sobreescribir (Override) el método run() e implementar allí lo que tiene que hacer el hilo
  • Podemos hacer que el hilo haga un número finito de cosas o bien que esté siempre en segundo plano (tendremos que asegurar que el método run() se ejecuta de forma continuada)(¿cómo se hace eso?)
  • Los problemas vienen cuando existen varios hilos. Hay que tener en cuenta que pueden compartir datos y código y encontrarse en diferentes estados de ejecución
  • La ejecución de nuestra aplicación será thread-safe si se puede garantizar una correcta manipulación de los datos que comparten los hilos de la aplicación sin resultados inesperados (más adelante veremos cómo)
  • Además, en el caso de aplicaciones multihilo, también nos puede interesar sincronizar y comunicar unos hilos con otros

También resulta interesante saber cómo detener un hilo. En este caso, la API de Java desaconsejó el método stop() que en un principio ideó para detener la ejecución. Así, hoy en día, se nos anima a que seamos nosotros quienes implementemos formas limpias de detener nuestros hilos.

Sincronización de hilos

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:

  1. join() Se espera la terminación del hilo que invoca a este método antes de continuar
  2. Thread.sleep(int) El hilo que ejecuta esta llamada permanece dormido durante el tiempo especificado como parámetro (en ms)
  3. isAlive() Comprueba si el hilo permanece activo todavía (no ha terminado su ejecución)
  4. yield() Sugiere al scheduler que sea otro hilo el que se ejecute (no se asegura)
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 de los dos hilos");
}

El hilo principal espera a que ambos hilos se hayan ejecutado para continuar (o para lo que sea)

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 de los dos hilos");
}

En este caso los hilos se ejecutan uno después de otro

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");
      try {
        Thread.sleep(500);
      } catch (InterruptedException ie) {
        ie.printStackTrace();
      }
    }
  }
}

En este caso el hilo duerme (detiene su ejecución) durante el tiempo especificado (en ms). Durante ese momento podrán ejecutarse otros hilos

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?");
  }
}

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)

Programación concurrente

synchronize

  • En un entorno multi-hilo, se da una condición de carrera cuando más de un hilo intenta “al mismo tiempo” actualizar un recurso compartido (que puede ser una variable)
  • Java proporciona un mecanismo para evitar estos problemas (palabra reserva synchronized)
  • Evita que más de un hilo puedan acceder a un zona de código o ejecutar un método determinado
public class RecursoCompartido {
  private int valor;
 
  public synchronized void calcular() { 
    setValor(getValor() + 1);
  }
}

Sin la palabra reservada synchronized, si más de un hilo ejecuta el método calcular, los resultados obtenidos pueden ser bastante inesperados (hacer prueba con y sin ella)

Executors y pools de hilos

En el apartado anterior vimos como crear y lanzar hilos individualmente utilizando la clase Thread que Java proporcina en su API. El código funciona perfectamente pero realmente se vuelve complicado de desarrollar si lo que tenemos que gestionar es una gran cantidad de hilos de forma simultánea. Para esos casos, Java proporciona un framework, Executors, que permite una gestión mucho más sencillo de cualquier tarea que se ejecuta en segundo plano.

Utilizando Executors, como programador, te olvidas en parte de la gestión de los hilos. Simplemente hay que centrarse en programar el código a realizar y asignar dicho código (una clase Runnable ) a un Executor , que se encargará de crear el hilo, lanzarlo y gestionar su ejecución.

Veamos un ejemplo de cómo lanzar una serie de tareas en segundo plano utilizando la clase ExecutorService que viene con el framework Executors de Java.

Supongamos una clase muy sencilla que implementa Runnable para definir un código que debe ejecutarse en segundo plano:

public class Tarea implements Runnable {
    @Override
    public void run() {
        System.out.println("Soy una clase que implementa Runnable");
        System.out.println("Ya me termino");
    }
}

Para lanzar la tarea, o varias instancia de la misma, podemos crear un pool de hilos (en este caso dos) y pasarles las tareas al objeto Executor para que sea él quién los lance. En este caso, puesto que sólo se dispone de dos hilos en el pool, sólo se podrán ejecutar dos hilos en cada momento. Si en algún momento se ocuparán los dos hilos, las demás tareas tendrían que esperar hasta que alguno de los dos quedará libre para ejecutarse.

Finalmente se ejecuta el método shutdown(), que hará finalizar el Executor una vez hayan terminado todas las tareas que se encuentran ejecutándose con él.

Hay que tener en cuenta que existe también un método shutdownNow que hace que el Executor finalice devolviendo todas las tareas que estaban esperando y no llegaron a ejecutarse. Se esperará que las tareas activas terminen o bien sean interrumpidas.

// Crea un executor con dos hilos
ExecutorService executor = Executors.newFixedThreadPool(2);
 
// Encola tareas a ajecutar
executor.execute(new Tarea());
executor.execute(new Tarea());
executor.execute(new Tarea());
executor.execute(new Tarea());
 
// Finaliza el executor. Terminará cuando todas las tareas que tiene
// se acaben de forma natural
executor.shutdown();

También es posible trabajar con un ExecutorService que simplemente cuente con un hilo

// Crea un executor con dos hilos
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.execute(new Tarea());
. . .

En el siguiente caso, instanciamos un pool de hilos planificados, de forma que podremos lanzar las tareas para que comiencen tras un tiempo de retraso asignado en su lanzamiento a través del método schedule():

// Crea un executor planificado con dos hilos
ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);
 
// Encola tareas a ajecutar añadiendo retraso
// en el lanzamiento de cada una de ellas
executor.schedule(new Tarea(), 1, TimeUnit.SECONDS);
executor.schedule(new Tarea(), 2, TimeUnit.SECONDS);
executor.schedule(new Tarea(), 3, TimeUnit.SECONDS);
executor.schedule(new Tarea(), 4, TimeUnit.SECONDS);
 
// Finaliza el executor. Terminará cuando todas las tareas que tiene
// se acaben de forma natural
executor.shutdown();

En cualquier caso, podemos encontrarnos con dos tipos de pools de hilos:

  • Fixed: Se pueden crear a través de los métodos Executors.newFixedThreadPool(…) y crean un pool de hilos fijo (de número determinado) que se reutilizan a medida que van quedando libres. En ningún caso se crearán más hilos de los fijados al crear el pool. Si todos los hilos están ocupados y se encola una nueva tarea, ésta tendrá que esperar hasta que un hilo quede libre para ejecutarla.
  • Cached: Se pueden crear a través de los métodos Executors.newCachedThreadPool(…) y crear un pool de hilos no fijo (sin número determinado) que se pueden reutilizar si quedan libres. En este caso, si todos los hilos están ocupados y se encola una nueva tarea se creará un nuevo hilo para poder ejecutarla. Los hilos que no hayan sido reutilizados en un tiempo determinado (6 segundos) serán finalizados y eliminados del pool.

Callable y Future

Hasta el momento hemos trabajado con objetos Runnable como objetos que se pueden asignar a un hilo para ejecutar su código en segundo plano. Los objetos Runnable no son capaces de devolver ningún valor, simplemente ejecutan el código (el método run() no devuelve nada). Tampoco pueden lanzar excepciones puesto que el método run() de Runnable no está prearado para ello. Java proporciona también una interface llamada Callable que permite crear objetos que pueden ser asignados a hilos para ejecutarse en segundo plano, y que además permite que éstas tareas devuelvan un resultado al finalizar y que lancen una excepción.

public class TareaCallable implements Callable<Boolean> {
    @Override
    public Boolean call() throws Exception {
        // Realiza algo
        return true;
    }
}

Ligado a este nuevo tipo de tarea Callable aparece un nuevo tipo llamado Future que es el tipo de dato en el que estas nuevas tareas devolverán su valor. Puesto que ahora tenemos una tarea en segundo plano que debe devolver un valor al finalizar, necesitamos, de alguna manera, esperar y recoger ese valor cuando corresponda. Así, un objeto Future es capaz de almacenar el valor de devolución de un método que ha sido lanzado pero todavía no ha terminado.

Veamos ahora un ejemplo lanzando una tarea Callable y cómo se recoge su resultado:

// Crea un executor con dos hilos
ExecutorService executor = Executors.newFixedThreadPool(2);
 
// Los Callable se ejecutan con el método submit()
Future<Boolean> futuro = executor.submit(new TareaCallable());
 
// Podemos seguir ejecutando código en el hilo principal
System.out.println("El programa sigue funcionando . . .");
 
// Recogemos el resultado de la tarea Callable
// Hay que tener en cuenta que la llamada al método get()
// bloquea la ejecución hasta que termine la tarea
Boolean resultado = futuro.get();
System.out.println("El resultado de la tarea es: " + resultado);
 
// Finaliza el executor. Terminará cuando todas las tareas que tiene
// se acaben de forma natural
executor.shutdown();

Hay que tener en cuenta que la llamada al método get() bloquea la ejecución del hilo principal hasta que la tarea termine y devuelva el resultado. La clave aqui está en que unas líneas antes ya tenemos el objeto Future y podemos seguir ejecutando instrucciones hasta que realmente necesitemos utilizar el valor, que será cuando invoquemos al método get().

Además, siempre podremos cancelar el objeto Future e incluso comprobar si ha terminado para decidir hacer algo mientras la tarea no lo haya hecho. De esa manera, una vez lanzada la tarea y obtenido el futuro, podemos ver si procede o no acabar obteniendo el valor que devuelva. En el siguiente caso, vamos a suponer que a partir de un cierto tiempo de ejecución se cancela la tarea y el programa sigue. Modificamos la tarea TareaCallable para forzar a que dure un tiempo determinado y que asi el ejemplo tenga cierta duración y podamos comprobar como espera (o no) en función del tiempo de timeout que le hayamos asignado, o bien utilizando otro criterio cualquiera.

public class TareaCallable implements Callable<Boolean> {
    @Override
    public Boolean call() throws Exception {
        // Realiza algo
        return true;
    }
}

Y el programa principal quedaría asi:

// Crea un executor con dos hilos
ExecutorService executor = Executors.newSingleThreadExecutor();
 
long tiempoPasado = 0;
long tiempoInicio = System.currentTimeMillis();
// Los Callable se ejecutan con el método submit()
Future<Boolean> futuro = executor.submit(new TareaCallable());
 
// Podemos seguir ejecutando código en el hilo principal
System.out.println("El programa sigue funcionando . . .");
 
// Ejecutamos mientras la tarea no haya terminado
while (!futuro.isDone()) {
    try {
        System.out.println("Esperando a que termine la tarea o haciendo algo mientras");
        Thread.sleep(300);
        // Vamos sumando el tiempo de espera
        tiempoPasado = System.currentTimeMillis() - tiempoInicio;
 
        // 4000 hace de timeout. Pasado ese tiempo cancelamos la tarea
        // del segundo plano
        if (tiempoPasado > 4000) {
            futuro.cancel(true);
        }
    } catch (InterruptedException ie) {
        ie.printStackTrace();
    }
}
 
// Llegados a este punto podemos saber si la tarea fue cancelada
if (futuro.isCancelled()) {
    System.out.println("La tarea fue cancelada");
}
// o bien si terminó correctamente
else {
    // Recogemos el resultado de la tarea Callable
    try {
        Boolean resultado = futuro.get();
        System.out.println("El resultado de la tarea es: " + resultado);
    } catch (ExecutionException|InterruptedException e) {
        e.printStackTrace();
    }
}
 
// Finaliza el executor. Terminará cuando todas las tareas que tiene
// se acaben de forma natural
executor.shutdown();

En cualquier caso, el método get() puede ser invocado para establecer un tiempo de timeout, a partir del cual el método devolvería una excepción TimeoutException. Es un caso muy útil cuando, desde nuestra aplicación, invocamos a algún servicio remoto del que no podemos garantizar su disponibilidad en todo momento.

. . .
// Espera 10 segundos antes de lanzar la excepción TimeoutException
Boolean resultado = futuro.get(10, TimeUnit.SECONDS);
. . .

Además, es posible lanzar varias tareas al mismo tiempo con el método executeAll de la clase ExecutorService:

. . .
List<Callable<Boolean>> listaTareas = . . .
List<Future<Boolean>> futuros = excecutor.invokeAll(listaTareas);
. . .
for (Future<Boolean> futuro : futuros) {
    System.out.println("Resultado :" + futuro.get());
}
. . .

CompletableFuture

Proporciona una API más completa que la que hay para la clase Future.

  • Lanzar una tarea en segundo plano de forma asíncrona y ejecutar un método al finalizar ésta (a través de la llamada a un método callback)
CompletableFuture.runAsync(() -> doSomething())
    .whenComplete((string, throwable) -> doSomethingWhenFinishFuture());
  • Lanzar una tarea y ejecutar algo cuando ésta termina:
private String doSomethingAndReturnResult() {
    return "ok";
}
 
CompletableFuture.supplyAsync(() -> doSomethingAndReturnResult())
                .thenAccept(System.out::println)
                .whenComplete((nothing, error) -> System.out.println("Fin"));

CountDownLatch

Sirve como contador para bloquear una llamada hasta que éste llega a cero

Se puede utilizar para que el código del hilo principal no continúe hasta que algo se haya ejecutado por todos los hilos que son necesarios

public class Tarea implements Runnable {
  private CountDownLatch countDownLatch;
 
  public Tarea(CountDownLatch countDownLatch) { 
    this.countDownLatch = countDownLatch;
  }
 
  @Override
  public void run() {
    hacerAlgo();
    countDownLatch.countDown(); 
  }
} 
 
. . .
. . .
 
CountDownLatch countDownLatch = new CountDownLatch(4); 
for . . .
  executorService.execute(new Tarea(countDownLatch));
. . .
countDownLatch.await(); // Espera hasta que el contador sea 0 System.out.println("Ya se han ejecutado las 5 tareas");

CyclicBarrier

Similar a CountDownLatch pero reutilizable

Se utiliza para que los hilos se esperen los unos a los otros antes de realizar alguna tarea concreta

public class Tarea implements Runnable {
  private CyclicBarrier cyclicBarrier;
 
  public Tarea(CyclicBarrier cyclicBarrier) { 
    this.cyclicBarrier = cyclicBarrier;
  }
 
  @Override
  public void run() {
    hacerAlgo(); 
    cyclicBarrier.await(); 
    hacerOtraCosa();
  } 
}
 
. . . 
 
// Acepta como segundo argumento un Runnable que será ejecutado por el último hilo que pase la barrera
CyclicBarrier cyclicBarrier = new CyclicBarrier(2); 
if (!cyclicBarrier.isBroken()) {
  executorService.execute(new Tarea(cyclicBarrier)); 
  . . .
}

Colecciones sincronizadas

Son lo que se conocen como colecciones thread-safe, puesto que están listas para usarse en entornos de concurrencia con varios hilos ejecutándose.

En la clase Collection hay una serie de métodos estáticos que devuelven instancia de colecciones sincronizadas. Por ejemplo:

  • synchronizedList()
  • synchronizedMap()
  • synchronizedSet()

Además, existe alguna colección ya sincronizada como Vector (similar a ArrayList y HashTable (similar a HashMap)

Objetos atómicos

Los objetos atómicos son estructuras Java que sirven para representar valores y realizar operaciones de forma atómica para algunos tipos de datos.

AtomicInt, AtomicLong y AtomicBoolean son algunos de los objetos atómicos que tenemos disponibles dentro de la API de Java Por poner algún ejemplo, AtomicInt garantiza la ejecución atómica de algunas operaciones como estas: * get() * compareAndSet(valorComparacion, nuevoValor) * addAndGet(valor) * getAndIncrement() * incrementAndGet() * decrementAndGet() * getAndDecrement() ==== Variables volatile ==== Es una palabra reservada que sirve para indicarle a Java que la variable debe ser escriba en memoria principal de forma inmediata cuando vea modificado su valor, para evitar que otros hilos que la utilicen puedan tener un valor diferente no actualizado * private volatile int valor; Hay que tener en cuenta que no siempre será suficiente y sólo servirá para situaciones donde un hilo esté leyendo alguna variable que otro hilo modifique. No nos ayudará a evitar las condiciones de carrera que hemos visto anteriormente. En esos casos necesitaremos, como ya vimos, crear zonas sincronizadas para evitar problemas. <figure> <caption>variable volatile</caption></figure> ==== Streams paralelos ==== La ejecución en paralelo significa que se ejecutan fragmentos de código de una aplicación al mismo tiempo. Hay que tener en cuenta que, en determinadas circunstancias, puede no interesar puesto que, al ejecutarse de forma paralela, no se garantiza en que orden se ejecutarán En otras circustancias puede interesar puesto que la aplicación aprovecha mucho más los recursos de la máquina al forzar a ésta a ejecutar algunas operaciones de forma simultánea <figure> <caption>Ejecución stream secuencial vs stream paralelo</caption></figure> En los streams paralelos, se procesan los elementos de forma paralela por segmentos <code java> List<String> cities = new ArrayList<>(); TODO Poblar la lista de ciudades cities.parallelStream() .forEach(System.out::println); </code> Puede mejorar el rendimiento en algunos casos, ya que permite que la aplicación realice algunos cálculos (matemáticos en este caso) de forma paralela, aprovechando mucho más los recursos de la máquina <code java> long count = Stream.iterate(0, i → i + 1) .limit(500000) .parallel() .filter(Main::isPrime) .count(); System.out.println(“Hay ” + count + “ números primos”); </code> ==== Locks ==== Es otro mecanismo para la sincronización de hilos parecido a como hace synchronized Es algo más flexible que synchronized, puesto que podemos bloquear/desbloquear la zona exclusiva en diferentes métodos Lock es un interface que tiene varias opciones para implementar. Veremos un ejemplo con ReentrantLock <code java> Lock lock = new ReentrantLock(); lock.lock(); try { Aqui se accede al recurso/código compartido } finally { lock.unlock(); } </code> Podemos encapsular la operación de bloqueo (lock) para evitar que se produzcan deadlocks (esperar indefinidamente un recurso) utilizando el método tryLock indicando el tiempo que el hilo esperará por el recurso <code java> boolean isLocked = lock.tryLock(2, TimeUnit.SECONDS); if (isLocked) { try { Aqui se accede al recurso/código compartido } finally { lock.unlock(); } } </code> ==== Patrón Observer ==== Patrón de diseño por el cual un objeto, llamada sujeto, mantiene a otros objetos, llamados observadores, notificados acerca de cualquier cambio que sobre él se produce. Hasta Java 9, se proporcionaba una serie de clases que permitían definir una solución para este Patrón. A partir de entonces se recomienda el uso de PropertyChangeListener para dicha implementación. === Clase Observable === <code java> public class Product { private String nombre; . . . private PropertyChangeSupport change; @Override public void addPropertyChangeListener(PropertyChangeListener listener) { change.addPropertyChangeListener(listener); } @Override public void removePropertyChangeListener(PropertyChangeListener listener) { change.removePropertyChangeListener(listener); } public void decreaseStock(int quantity) { change.firePropertyChange(“stock”, stock, (stock – quantity)); stock -= quantity; } } </code> === La clase Observer === <code java> public class Provider implements PropertyChangeListener { private String name; . . . @Override public void propertyChange(PropertyChangeEvent event) { if (event.getPropertyName().equals(“stock”)) { System.out.println(“El stock ha bajado”); } } } </code> Y el programa principal para ver el ejemplo funcionar: <code java> public static void main(String args[]) { Product product = new Product(); . . . Customer customer = new Customer(); . . . product.addPropertyChangeListener(customer); product.setStock(10); ¿Qué ocurre aqui? .. . } </code> —- © 2021-2024 Santiago Faci

apuntes/concurrencia.txt · Last modified: 2023/05/28 23:59 by Santiago Faci