Volver a Java Intermedio

Introducción

La concurrencia permite que los programas realicen múltiples tareas simultáneamente. Java proporciona soporte robusto de hilos a través de la clase Thread, ExecutorService y utilidades concurrentes modernas.

Creando Hilos

Extendiendo Thread:

class MiHilo extends Thread { @Override public void run() { System.out.println("Hilo ejecutándose: " + getName()); } } // Uso MiHilo hilo = new MiHilo(); hilo.start();

Implementando Runnable:

class MiTarea implements Runnable { @Override public void run() { System.out.println("Tarea ejecutándose"); } } // Uso Thread hilo = new Thread(new MiTarea()); hilo.start(); // Con lambda Thread hiloLambda = new Thread(() -> { System.out.println("Hilo lambda ejecutándose"); }); hiloLambda.start();

Ciclo de Vida del Hilo

Thread hilo = new Thread(() -> { try { System.out.println("Hilo iniciado"); Thread.sleep(1000); // Dormir por 1 segundo System.out.println("Hilo finalizado"); } catch (InterruptedException e) { System.out.println("Hilo interrumpido"); } }); hilo.start(); // Iniciar hilo hilo.join(); // Esperar a que el hilo termine hilo.isAlive(); // Verificar si está ejecutándose hilo.interrupt(); // Solicitar interrupción

ExecutorService

Mejor gestión de hilos:

ExecutorService executor = Executors.newFixedThreadPool(4); // Enviar tareas executor.submit(() -> { System.out.println("Tarea 1"); }); executor.submit(() -> { System.out.println("Tarea 2"); }); // Apagar executor executor.shutdown(); try { executor.awaitTermination(5, TimeUnit.SECONDS); } catch (InterruptedException e) { executor.shutdownNow(); }

Callable y Future

Retornar valores desde hilos:

ExecutorService executor = Executors.newSingleThreadExecutor(); Callable<Integer> tarea = () -> { Thread.sleep(1000); return 42; }; Future<Integer> future = executor.submit(tarea); // Hacer otro trabajo... try { Integer resultado = future.get(); // Bloquea hasta que esté listo System.out.println("Resultado: " + resultado); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } executor.shutdown();

Sincronización

Prevenir condiciones de carrera:

public class Contador { private int cuenta = 0; // Método sincronizado public synchronized void incrementar() { cuenta++; } // Bloque sincronizado public void decrementar() { synchronized(this) { cuenta--; } } public synchronized int obtenerCuenta() { return cuenta; } }

Locks

Más flexible que synchronized:

import java.util.concurrent.locks.*; public class CuentaBancaria { private double saldo; private final ReentrantLock lock = new ReentrantLock(); public void retirar(double monto) { lock.lock(); try { if (saldo >= monto) { saldo -= monto; } } finally { lock.unlock(); // Siempre desbloquear en finally } } public boolean intentarRetirar(double monto, long timeout) { try { if (lock.tryLock(timeout, TimeUnit.MILLISECONDS)) { try { if (saldo >= monto) { saldo -= monto; return true; } } finally { lock.unlock(); } } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return false; } }

Variables Atómicas

Operaciones thread-safe sin locks:

import java.util.concurrent.atomic.*; public class Estadisticas { private AtomicInteger cuenta = new AtomicInteger(0); private AtomicLong total = new AtomicLong(0); public void registrar(int valor) { cuenta.incrementAndGet(); total.addAndGet(valor); } public double promedio() { int c = cuenta.get(); return c == 0 ? 0 : (double) total.get() / c; } }

Colecciones Concurrentes

Colecciones thread-safe:

// ConcurrentHashMap Map<String, Integer> mapa = new ConcurrentHashMap<>(); mapa.put("clave", 1); mapa.computeIfAbsent("clave2", k -> 2); // CopyOnWriteArrayList (bueno para muchas lecturas, pocas escrituras) List<String> lista = new CopyOnWriteArrayList<>(); lista.add("item"); // BlockingQueue (patrón productor-consumidor) BlockingQueue<String> cola = new LinkedBlockingQueue<>(10); // Productor new Thread(() -> { try { cola.put("item"); // Bloquea si está llena } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }).start(); // Consumidor new Thread(() -> { try { String item = cola.take(); // Bloquea si está vacía System.out.println(item); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }).start();

CompletableFuture

Programación asíncrona:

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> { // Se ejecuta en hilo separado return "Hola"; }); future.thenApply(s -> s + " Mundo") .thenAccept(System.out::println); // Combinando futures CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> 5); CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> 3); CompletableFuture<Integer> combinado = future1.thenCombine(future2, (a, b) -> a + b); System.out.println(combinado.get()); // 8 // Manejo de excepciones CompletableFuture.supplyAsync(() -> { if (Math.random() > 0.5) throw new RuntimeException("Error"); return "Éxito"; }).exceptionally(ex -> "Falló: " + ex.getMessage()) .thenAccept(System.out::println);

Buenas Prácticas

  • Prefiere ExecutorService sobre hilos crudos
  • Siempre apaga los executor services
  • Usa colecciones concurrentes para datos compartidos
  • Evita locks anidados (riesgo de deadlock)
  • Usa variables atómicas para contadores simples
  • Maneja InterruptedException apropiadamente
  • Usa CompletableFuture para operaciones asíncronas
  • Prueba el código concurrente exhaustivamente

Ejemplo Práctico

public class ProcesadorParalelo { private final ExecutorService executor; public ProcesadorParalelo(int hilos) { this.executor = Executors.newFixedThreadPool(hilos); } public List<String> procesarArchivos(List<Path> archivos) { List<CompletableFuture<String>> futures = archivos.stream() .map(archivo -> CompletableFuture.supplyAsync(() -> { try { return Files.readString(archivo); } catch (IOException e) { throw new UncheckedIOException(e); } }, executor)) .collect(Collectors.toList()); return futures.stream() .map(CompletableFuture::join) .collect(Collectors.toList()); } public void apagar() { executor.shutdown(); try { if (!executor.awaitTermination(60, TimeUnit.SECONDS)) { executor.shutdownNow(); } } catch (InterruptedException e) { executor.shutdownNow(); Thread.currentThread().interrupt(); } } }

Conclusión

La concurrencia es poderosa pero compleja. Comprender hilos, sincronización y utilidades concurrentes modernas te ayuda a escribir aplicaciones multi-hilo eficientes evitando trampas comunes.

¡Esto completa el curso de Java Intermedio!