En este tema, exploraremos las utilidades de concurrencia en Java, que son herramientas y clases proporcionadas por el lenguaje para facilitar la programación concurrente. Estas utilidades ayudan a gestionar múltiples hilos de manera eficiente y segura, evitando problemas comunes como las condiciones de carrera y los bloqueos.

Contenidos

  1. Introducción a las Utilidades de Concurrencia
  2. Executor Framework
  3. Callable y Future
  4. ScheduledExecutorService
  5. CountDownLatch
  6. CyclicBarrier
  7. Semaphore
  8. Exchanger
  9. Ejercicios Prácticos

  1. Introducción a las Utilidades de Concurrencia

Java proporciona un paquete completo para la concurrencia: java.util.concurrent. Este paquete incluye una serie de clases y interfaces que facilitan la creación y gestión de hilos, así como la sincronización entre ellos.

  1. Executor Framework

El Executor Framework es una API que proporciona una forma estándar de gestionar un grupo de hilos. La interfaz principal es Executor, y sus implementaciones más comunes son ThreadPoolExecutor y ScheduledThreadPoolExecutor.

Ejemplo: Uso de ExecutorService

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ExecutorExample {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(3);

        for (int i = 0; i < 5; i++) {
            executor.submit(new Task(i));
        }

        executor.shutdown();
    }
}

class Task implements Runnable {
    private int taskId;

    public Task(int id) {
        this.taskId = id;
    }

    @Override
    public void run() {
        System.out.println("Task " + taskId + " is running");
    }
}

Explicación

  • ExecutorService: Es una interfaz que representa un grupo de hilos reutilizables.
  • Executors.newFixedThreadPool(3): Crea un pool de hilos con un tamaño fijo de 3.
  • submit: Envía una tarea al pool de hilos para su ejecución.
  • shutdown: Finaliza el pool de hilos una vez que todas las tareas han sido completadas.

  1. Callable y Future

Callable es similar a Runnable, pero puede devolver un resultado y lanzar una excepción. Future representa el resultado de una operación asincrónica.

Ejemplo: Uso de Callable y Future

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class CallableExample {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(3);

        Future<Integer> future = executor.submit(new Task());

        try {
            Integer result = future.get();
            System.out.println("Result: " + result);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }

        executor.shutdown();
    }
}

class Task implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        return 123;
    }
}

Explicación

  • Callable: Una interfaz que permite devolver un resultado y lanzar excepciones.
  • Future: Representa el resultado de una operación asincrónica.
  • future.get(): Bloquea hasta que el resultado esté disponible.

  1. ScheduledExecutorService

ScheduledExecutorService permite programar tareas para que se ejecuten después de un retraso o de manera periódica.

Ejemplo: Uso de ScheduledExecutorService

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class ScheduledExecutorExample {
    public static void main(String[] args) {
        ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);

        scheduler.scheduleAtFixedRate(new Task(), 0, 1, TimeUnit.SECONDS);
    }
}

class Task implements Runnable {
    @Override
    public void run() {
        System.out.println("Task is running");
    }
}

Explicación

  • ScheduledExecutorService: Permite programar tareas con un retraso inicial y una frecuencia fija.
  • scheduleAtFixedRate: Programa una tarea para que se ejecute periódicamente.

  1. CountDownLatch

CountDownLatch es una clase de sincronización que permite que uno o más hilos esperen hasta que un conjunto de operaciones en otros hilos se complete.

Ejemplo: Uso de CountDownLatch

import java.util.concurrent.CountDownLatch;

public class CountDownLatchExample {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(3);

        for (int i = 0; i < 3; i++) {
            new Thread(new Task(latch)).start();
        }

        latch.await();
        System.out.println("All tasks are completed");
    }
}

class Task implements Runnable {
    private CountDownLatch latch;

    public Task(CountDownLatch latch) {
        this.latch = latch;
    }

    @Override
    public void run() {
        System.out.println("Task is running");
        latch.countDown();
    }
}

Explicación

  • CountDownLatch: Permite que un hilo espere hasta que el contador llegue a cero.
  • latch.await(): Bloquea hasta que el contador llegue a cero.
  • latch.countDown(): Decrementa el contador.

  1. CyclicBarrier

CyclicBarrier es una clase de sincronización que permite que un conjunto de hilos esperen entre sí en un punto común.

Ejemplo: Uso de CyclicBarrier

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierExample {
    public static void main(String[] args) {
        CyclicBarrier barrier = new CyclicBarrier(3, () -> System.out.println("All tasks are completed"));

        for (int i = 0; i < 3; i++) {
            new Thread(new Task(barrier)).start();
        }
    }
}

class Task implements Runnable {
    private CyclicBarrier barrier;

    public Task(CyclicBarrier barrier) {
        this.barrier = barrier;
    }

    @Override
    public void run() {
        System.out.println("Task is running");
        try {
            barrier.await();
        } catch (InterruptedException | BrokenBarrierException e) {
            e.printStackTrace();
        }
    }
}

Explicación

  • CyclicBarrier: Permite que un conjunto de hilos esperen entre sí en un punto común.
  • barrier.await(): Bloquea hasta que todos los hilos lleguen al punto de barrera.

  1. Semaphore

Semaphore es una clase de sincronización que controla el acceso a un recurso compartido mediante un contador.

Ejemplo: Uso de Semaphore

import java.util.concurrent.Semaphore;

public class SemaphoreExample {
    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(1);

        for (int i = 0; i < 3; i++) {
            new Thread(new Task(semaphore)).start();
        }
    }
}

class Task implements Runnable {
    private Semaphore semaphore;

    public Task(Semaphore semaphore) {
        this.semaphore = semaphore;
    }

    @Override
    public void run() {
        try {
            semaphore.acquire();
            System.out.println("Task is running");
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            semaphore.release();
        }
    }
}

Explicación

  • Semaphore: Controla el acceso a un recurso compartido mediante un contador.
  • semaphore.acquire(): Adquiere un permiso del semáforo.
  • semaphore.release(): Libera un permiso del semáforo.

  1. Exchanger

Exchanger es una clase de sincronización que permite que dos hilos intercambien datos entre sí.

Ejemplo: Uso de Exchanger

import java.util.concurrent.Exchanger;

public class ExchangerExample {
    public static void main(String[] args) {
        Exchanger<String> exchanger = new Exchanger<>();

        new Thread(new Task(exchanger, "Data from Thread 1")).start();
        new Thread(new Task(exchanger, "Data from Thread 2")).start();
    }
}

class Task implements Runnable {
    private Exchanger<String> exchanger;
    private String data;

    public Task(Exchanger<String> exchanger, String data) {
        this.exchanger = exchanger;
        this.data = data;
    }

    @Override
    public void run() {
        try {
            String exchangedData = exchanger.exchange(data);
            System.out.println("Exchanged data: " + exchangedData);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

Explicación

  • Exchanger: Permite que dos hilos intercambien datos entre sí.
  • exchanger.exchange(data): Intercambia datos con el otro hilo.

  1. Ejercicios Prácticos

Ejercicio 1: Uso de ExecutorService

Descripción: Crea un programa que utilice ExecutorService para ejecutar 5 tareas en paralelo. Cada tarea debe imprimir su ID y dormir durante 1 segundo.

Solución:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ExecutorExercise {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(3);

        for (int i = 0; i < 5; i++) {
            executor.submit(new Task(i));
        }

        executor.shutdown();
    }
}

class Task implements Runnable {
    private int taskId;

    public Task(int id) {
        this.taskId = id;
    }

    @Override
    public void run() {
        System.out.println("Task " + taskId + " is running");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

Ejercicio 2: Uso de CountDownLatch

Descripción: Crea un programa que utilice CountDownLatch para esperar a que 3 tareas completen su ejecución antes de imprimir "All tasks are completed".

Solución:

import java.util.concurrent.CountDownLatch;

public class CountDownLatchExercise {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(3);

        for (int i = 0; i < 3; i++) {
            new Thread(new Task(latch)).start();
        }

        latch.await();
        System.out.println("All tasks are completed");
    }
}

class Task implements Runnable {
    private CountDownLatch latch;

    public Task(CountDownLatch latch) {
        this.latch = latch;
    }

    @Override
    public void run() {
        System.out.println("Task is running");
        latch.countDown();
    }
}

Conclusión

En esta sección, hemos explorado varias utilidades de concurrencia en Java, incluyendo ExecutorService, Callable, Future, ScheduledExecutorService, CountDownLatch, CyclicBarrier, Semaphore, y Exchanger. Estas herramientas son esenciales para escribir programas concurrentes eficientes y seguros. Asegúrate de practicar con los ejemplos y ejercicios proporcionados para consolidar tu comprensión de estos conceptos.

Curso de Programación en Java

Módulo 1: Introducción a Java

Módulo 2: Flujo de Control

Módulo 3: Programación Orientada a Objetos

Módulo 4: Programación Orientada a Objetos Avanzada

Módulo 5: Estructuras de Datos y Colecciones

Módulo 6: Manejo de Excepciones

Módulo 7: Entrada/Salida de Archivos

Módulo 8: Multihilo y Concurrencia

Módulo 9: Redes

Módulo 10: Temas Avanzados

Módulo 11: Frameworks y Librerías de Java

Módulo 12: Construcción de Aplicaciones del Mundo Real

© Copyright 2024. Todos los derechos reservados