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!