La concurrencia permite ejecutar múltiples tareas simultáneamente. Java tiene una de las APIs de concurrencia más ricas del mundo. En 2026, con Java 21, los Virtual Threads simplifican enormemente la programación concurrente.
Hilos con Thread y Runnable
// Forma clásica: implementar Runnable
Runnable tarea = () -> {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
}
};
Thread hilo1 = new Thread(tarea, "Hilo-1");
Thread hilo2 = new Thread(tarea, "Hilo-2");
hilo1.start();
hilo2.start();
hilo1.join(); // esperar a que termine
hilo2.join();
ExecutorService: pool de hilos
Crear y destruir hilos es costoso. ExecutorService mantiene un pool de hilos reutilizables:
import java.util.concurrent.*;
ExecutorService executor = Executors.newFixedThreadPool(4);
for (int i = 0; i < 10; i++) {
final int tarea = i;
executor.submit(() -> {
System.out.println("Procesando tarea " + tarea +
" en " + Thread.currentThread().getName());
});
}
executor.shutdown(); // no acepta más tareas
executor.awaitTermination(30, TimeUnit.SECONDS);
Callable y Future: tareas con resultado
Callablecalcular = () -> { Thread.sleep(1000); return 42; }; ExecutorService exec = Executors.newSingleThreadExecutor(); Future futuro = exec.submit(calcular); // Hacer otro trabajo mientras se calcula... System.out.println("Calculando..."); int resultado = futuro.get(); // bloquea hasta que termina System.out.println("Resultado: " + resultado); exec.shutdown();
Virtual Threads (Java 21)
Los Virtual Threads son el cambio más importante de Java 21 para la concurrencia. Son hilos ligeros gestionados por la JVM (no por el SO), que permiten crear millones de hilos concurrentes sin colapsar la memoria:
// Crear un virtual thread directamente
Thread vt = Thread.ofVirtual().start(() -> {
System.out.println("Virtual thread: " + Thread.currentThread().isVirtual());
});
vt.join();
// Pool de virtual threads — ideal para I/O concurrente
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
List> futures = new ArrayList<>();
for (int i = 0; i < 10_000; i++) {
final int id = i;
futures.add(executor.submit(() -> procesarPeticion(id)));
}
// obtener resultados...
}
Antes de Java 21, para manejar 10.000 peticiones concurrentes se necesitaban pools de hilos cuidadosamente dimensionados. Con virtual threads, puedes crear un hilo por petición sin preocuparte por el límite.
Sincronización: synchronized y volatile
public class ContadorSeguro {
private int cuenta = 0;
public synchronized void incrementar() {
cuenta++;
}
public synchronized int obtener() {
return cuenta;
}
}
// AtomicInteger: más eficiente que synchronized para operaciones simples
import java.util.concurrent.atomic.AtomicInteger;
AtomicInteger contador = new AtomicInteger(0);
contador.incrementAndGet();
System.out.println(contador.get());
CompletableFuture: composición asíncrona
CompletableFuturefuturo = CompletableFuture .supplyAsync(() -> buscarDatosEnBD()) // ejecuta en otro hilo .thenApply(datos -> formatear(datos)) // transforma el resultado .thenApply(String::toUpperCase) .exceptionally(e -> "Error: " + e.getMessage()); String resultado = futuro.get(); System.out.println(resultado);
