Programación Avanzada · 2026

Concurrencia, sin magia

Una guía para entender qué pasa cuando varias tareas avanzan, cómo se rompen los datos y qué herramienta Java elegir para coordinar sin adivinar.

7 módulos progresivos5 demos manipulables18 preguntas con explicación

Antes de empezar

Cómo usar esta guía

  1. Leé los módulos en orden la primera vez.
  2. Probá cada demo: equivocarte acá es gratis.
  3. Copiá y ejecutá los snippets Java.
  4. Marcá cada tema entendido: el progreso queda en este navegador.
  5. Usá el buscador para repasar.
  6. Terminá con la hoja de síntomas y el quiz.

Dependencias

Mapa mental

Tareasprocesos / hilosestado compartidoriesgosatomicidad / exclusión / coordinaciónherramientas Javapatrones

Módulo

01 · Fundamentos

Origen, motivaciones y definición

Concurrencia es estructurar un programa para que varias tareas progresen durante el mismo período, intercaladas o superpuestas. Nació para aprovechar esperas de E/S, repartir recursos con equidad y separar responsabilidades.

Analogía: una persona atiende varias mesas: toma un pedido mientras otra mesa espera la comida.

Sirve para
respuesta fluida, mejor uso de recursos y programas modulares.
Riesgo
el orden puede variar: la misma entrada puede tener resultados distintos si el diseño depende del intercalado.

Comunicación mueve información entre tareas; sincronización coordina cuándo pueden avanzar. No son sinónimos.

Secuencial, concurrente y paralelo

Secuencial: una tarea termina antes de empezar otra. Concurrente: varias tareas están en progreso, aunque una CPU las intercale. Paralelo: dos o más tareas ejecutan físicamente al mismo instante, normalmente en varios núcleos.

No los confundas: concurrencia es organización; paralelismo es ejecución simultánea real. Podés tener concurrencia sin paralelismo.

Analogía: un cocinero alterna salsa y agua = concurrencia; cocinero y ayudante trabajan a la vez = paralelismo.

Asincronía tampoco equivale a paralelismo: “pedir ahora y recibir el resultado después” puede ocurrir en un solo hilo.

Demo 1 · Tres formas de trabajar

Tarea A
Tarea B

Elegí un modo.

Proceso, hilo, sistemas distribuidos, PCB y TCB

Un proceso es un programa en ejecución con memoria propia; un hilo es una unidad de ejecución dentro de ese proceso. Los hilos comparten heap y recursos, pero cada uno tiene stack, registros y contador de programa.

Analogía: el proceso es un taller; los hilos son operarios que comparten máquinas. Talleres distintos se pasan paquetes, no comparten estantes.

ProcesoHilo
MemoriaAisladaCompartida dentro del proceso
Creación/cambioMás costosoMás liviano
FallaMejor aislamientoPuede afectar al proceso
ComunicaciónIPC/mensajesMemoria compartida o mensajes

En distribuidos hay procesadores y memorias independientes. En una PC, procesos comparten CPU pero no memoria; hilos comparten CPU y memoria del proceso.

PCB: id, espacio de direcciones, recursos, estado, prioridad y TCB. TCB: id del hilo, stack, registros, program counter, estado y prioridad. El SO los usa para planificar.

Memoria compartida vs pasaje de mensajes

CriterioMemoria compartidaMensajes
IdeaVarios hilos leen/escriben el mismo objetoSe envían datos por cola, canal o buzón
VentajaAcceso directo y rápidoAislamiento y razonamiento local
CostoSincronización delicadaCopia/cola y protocolo
EjemploContador compartidoPedidos en BlockingQueue

Compartir memoria exige proteger el estado mutable. Pasar mensajes reduce carreras, pero no elimina todos los errores de coordinación.

Módulo

02 · Hilos en Java

Thread directo, extender Thread y Runnable

Hay tres formas: pasar una lambda/Runnable a new Thread, extender Thread y sobrescribir run(), o implementar Runnable. Preferí Runnable: separa tarea de mecanismo y deja libre la herencia.

Analogía: Runnable es la receta; Thread es quien la ejecuta.

start() crea una ejecución nueva que luego llama run(). Invocar run() directamente es una llamada común en el hilo actual.

public class PrimerHilo {
  public static void main(String[] args) throws InterruptedException {
    Runnable tarea = () -> System.out.println(Thread.currentThread().getName());
    Thread hilo = new Thread(tarea, "impresora-1");
    hilo.start();       // crea ejecución concurrente
    hilo.join();        // main espera su final
    System.out.println("¿Vivo? " + hilo.isAlive());
  }
}

start/run, join, sleep, yield e interrupt

  • join(): quien llama espera que termine otro hilo.
  • sleep(ms): duerme al hilo actual; no libera locks que tenga.
  • yield(): sugerencia al planificador; no garantiza nada.
  • interrupt(): pedido cooperativo de cancelación, no “mata” al hilo.
  • isInterrupted(): consulta sin limpiar la marca. Thread.interrupted(): consulta la marca del hilo actual y la limpia.
  • currentThread(), nombres, prioridad e isAlive() ayudan a observar; la prioridad es una pista, no un contrato.

Error común: tragar InterruptedException. Restaurá la marca o terminá limpiamente.

public class Cancelable {
  public static void main(String[] args) throws Exception {
    Thread t = new Thread(() -> {
      try {
        while (!Thread.currentThread().isInterrupted()) {
          Thread.sleep(100); // puede lanzar InterruptedException
        }
      } catch (InterruptedException e) {
        Thread.currentThread().interrupt(); // restaura la marca
      }
    }, "descarga");
    t.setPriority(Thread.NORM_PRIORITY); // no depender de esto
    t.start(); Thread.sleep(250); t.interrupt(); t.join();
  }
}

Cambio de contexto y rendimiento

El SO guarda registros/estado del hilo saliente en PCB/TCB y carga los del entrante. Es voluntario cuando el hilo espera, hace E/S, sleep o yield; involuntario cuando vence su turno o aparece otro prioritario.

Analogía: cambiar de materia obliga a guardar apuntes y reconstruir dónde estabas.

Costo: tiempo de planificación, cachés menos útiles y ningún trabajo de negocio durante el cambio. Demasiados hilos pueden empeorar el rendimiento. Medí; un pool acotado suele superar “un hilo por tarea”.

Estados de un hilo

NEW todavía no inició; RUNNABLE está listo o ejecutando; BLOCKED espera entrar a un monitor; WAITING espera sin plazo (join/wait); TIMED_WAITING espera con plazo (sleep); TERMINATED finalizó.

El nombre del estado no identifica por sí solo la causa: observá stack traces y locks.

Módulo

03 · Seguridad y estado

Thread safety, sección crítica y atomicidad

La seguridad en hilos o thread safety es la propiedad por la que varios hilos pueden usar un componente sin corromper datos. El foco es el estado compartido mutable. Una sección crítica toca ese estado y requiere una política.

Si el resultado incorrecto depende del intercalado aparece una condición de carrera (race condition).

Atomicidad significa “todo o nada” sin estados intermedios observables. n++ no es atómico: lee, calcula y escribe. Exclusión mutua deja entrar a uno. Sincronización es más amplia: también ordena eventos (por ejemplo, una barrera), sin necesariamente excluir.

Demo 2 · Carrera vs protección

Dos hilos incrementan desde el mismo valor.

Valor: 0

    Race condition, deadlock, starvation, livelock y visibilidad

    Race condition
    el resultado depende de un intercalado no controlado.
    Deadlock
    A retiene llave 1 y espera 2; B retiene 2 y espera 1. Nadie progresa.
    Self-deadlock
    un hilo intenta retomar un lock no reentrante que ya posee.
    Starvation / inanición
    un hilo siempre pierde el recurso y espera indefinidamente.
    Livelock / bloqueo activo
    los hilos reaccionan y cambian, pero no avanzan.
    Visibilidad
    un hilo no observa a tiempo la escritura de otro por cachés/reordenamientos.

    Prevención: orden global de locks, secciones breves, tryLock/timeout, justicia cuando importa y evitar estado compartido.

    Inmutabilidad, volatile, confinamiento y atómicas

    Inmutabilidad: el valor no cambia después de construirlo. Confinamiento: cada hilo usa datos propios o partes disjuntas. Ambos evitan compartir mutabilidad.

    volatile garantiza visibilidad y orden de lecturas/escrituras de esa variable, pero no vuelve atómicas operaciones compuestas como contador++. Es útil para una bandera simple.

    Las variables atómicas ofrecen actualizaciones indivisibles sin locks explícitos para un único valor.

    import java.util.concurrent.atomic.AtomicInteger;
    class Contador {
      private final AtomicInteger n = new AtomicInteger();
      int incrementar() { return n.incrementAndGet(); }
      int valor() { return n.get(); }
    }
    class Control { volatile boolean seguir = true; }

    synchronized, monitor y reentrancia

    Cada objeto Java tiene un monitor. Un método synchronized de instancia bloquea this; uno estático bloquea el objeto Clase.class; un bloque permite elegir lock y achicar la sección crítica.

    Los monitores Java son reentrantes: el mismo hilo puede entrar otra vez al mismo monitor sin self-deadlock.

    No sincronices sobre String públicos ni expongas el objeto lock. El lock estático y el de instancia son distintos.

    class Inventario {
      private int unidades;
      private final Object lock = new Object();
      public synchronized int leerInstancia() { return unidades; }
      public static synchronized void auditoriaGlobal() { }
      public void sumar(int x) {
        synchronized (lock) { unidades += x; }
      }
    }

    AtomicInteger/Long/Boolean/Reference y arrays

    AtomicInteger y AtomicLong sirven para contadores; AtomicBoolean para cambios de bandera; AtomicReference<T> reemplaza una referencia de manera atómica. compareAndSet(esperado,nuevo) permite actualización condicional.

    AtomicIntegerArray, AtomicLongArray y AtomicReferenceArray protegen cada elemento, no una invariantes entre varios índices.

    Una colección de operaciones atómicas no convierte una regla compuesta en atómica.

    ConcurrentHashMap y CopyOnWrite

    ConcurrentHashMap permite concurrencia segura y operaciones compuestas como compute. CopyOnWriteArrayList y CopyOnWriteArraySet copian el arreglo al modificar: excelentes con muchas lecturas y muy pocas escrituras.

    Analogía: CopyOnWrite imprime una edición nueva de la lista por cada cambio.

    No uses “obtener y luego poner” si debe ser una sola decisión: preferí computeIfAbsent, merge o similares.

    import java.util.concurrent.*;
    class Frecuencias {
      private final ConcurrentHashMap<String,Integer> m = new ConcurrentHashMap<>();
      private final CopyOnWriteArrayList<String> oyentes = new CopyOnWriteArrayList<>();
      void contar(String k) { m.merge(k, 1, Integer::sum); }
    }

    Módulo

    04 · Coordinación y exclusión

    wait, notify, notifyAll

    Se llaman dentro de synchronized sobre el mismo objeto. wait() libera ese monitor y espera; notify() despierta uno; notifyAll() despierta todos para que vuelvan a competir.

    Analogía: esperás dentro del local y soltás la única llave; el encargado anuncia que cambió la condición.

    Esperá siempre en while, no if: puede haber despertares espurios o la condición pudo cambiar antes de retomar el lock.

    class Buzon {
      private String dato;
      synchronized void poner(String x) {
        while (dato != null) esperar();
        dato = x; notifyAll();
      }
      synchronized String sacar() {
        while (dato == null) esperar();
        String x = dato; dato = null; notifyAll(); return x;
      }
      private void esperar() {
        try { wait(); } catch (InterruptedException e) {
          Thread.currentThread().interrupt(); throw new RuntimeException(e);
        }
      }
    }

    ReentrantLock, tryLock, fairness y ReadWriteLock

    ReentrantLock ofrece exclusión mutua, reentrancia, espera interrumpible, tryLock y justicia opcional (new ReentrantLock(true)). Siempre liberalo en finally.

    ReentrantReadWriteLock deja entrar a múltiples lectores o a un único escritor. Sirve cuando las lecturas dominan y duran lo suficiente para compensar la gestión.

    import java.util.concurrent.TimeUnit;
    import java.util.concurrent.locks.*;
    class Caja {
      private final ReentrantLock lock = new ReentrantLock(true);
      void usar() throws InterruptedException {
        if (!lock.tryLock(50, TimeUnit.MILLISECONDS)) return;
        try { /* sección crítica */ }
        finally { lock.unlock(); }
      }
    }

    Semaphore binario y no binario

    Un semáforo mantiene permisos. acquire() toma uno o espera; release() lo devuelve. Con 1 permiso puede dar exclusión mutua; con N limita concurrencia (por ejemplo, lugares de estacionamiento).

    No es reentrante ni asocia permiso con dueño: un release extra rompe el cupo. Usá try/finally sólo después de adquirir.

    Demo 3 · Estacionamiento con 3 permisos

    Disponibles: 3/3
    import java.util.concurrent.Semaphore;
    class Impresoras {
      private final Semaphore cupos = new Semaphore(3, true);
      void imprimir() throws InterruptedException {
        cupos.acquire();
        try { /* usar una impresora */ }
        finally { cupos.release(); }
      }
    }

    CountDownLatch, CyclicBarrier y Phaser

    CountDownLatchCyclicBarrierPhaser
    Ideaesperar N finalesN pares se encuentranetapas con participantes dinámicos
    ReutilizaNoSí, automáticoSí, por fases
    Cambian participantesNoNoSí: register/deregister
    MétodoscountDown/awaitbarrier.await()arriveAndAwaitAdvance

    Latch: una largada o esperar trabajadores. Barrier: pares esperan entre sí. Phaser: pipeline de varias etapas.

    Demo 4 · Barrera de 4 hilos

    0 de 4 llegaron.

    import java.util.concurrent.CyclicBarrier;
    class Etapa {
      public static void main(String[] a) {
        CyclicBarrier barrier = new CyclicBarrier(3, () -> System.out.println("fase lista"));
        Runnable r = () -> { try { barrier.await(); } catch (Exception e) { throw new RuntimeException(e); } };
        for (int i=0;i<3;i++) new Thread(r).start();
      }
    }

    ¿synchronized, Lock o Semaphore?

    ElegíCuandoCuidado
    synchronizedexclusión simple y estructuradamantener bloque corto
    ReentrantLocktryLock, timeout, interrupción o fairnessunlock en finally
    Semaphorelimitar N accesosbalance acquire/release

    Módulo

    05 · Tareas, resultados y colas

    Callable, FutureTask y Future

    Runnable no devuelve valor; Callable<T> sí y puede lanzar excepciones. Future<T> representa un resultado pendiente: get() espera, cancel() intenta cancelar. FutureTask une Callable con Runnable para ejecutarlo en un Thread.

    Asíncrono significa desacoplar inicio y resultado. Sólo será paralelo si realmente se ejecuta simultáneamente en otro núcleo.

    import java.util.concurrent.*;
    class Resultado {
      public static void main(String[] a) throws Exception {
        Callable<Integer> suma = () -> 20 + 22;
        FutureTask<Integer> ft = new FutureTask<>(suma);
        new Thread(ft, "calculadora").start();
        System.out.println(ft.get()); // espera si aún no terminó
      }
    }

    Executor, ExecutorService, thread pool y shutdown

    Executor separa enviar una tarea de ejecutarla. ExecutorService administra un pool, acepta execute y submit, y devuelve Future. Reutilizar hilos evita crear/destruir uno por tarea.

    Analogía: una bandeja de pedidos alimenta a un equipo fijo de cocineros.

    Siempre cerrá el pool. shutdown() deja terminar lo aceptado; luego podés awaitTermination y, como último recurso, shutdownNow().

    import java.util.concurrent.*;
    class Pool {
      public static void main(String[] a) throws Exception {
        ExecutorService pool = Executors.newFixedThreadPool(2);
        Future<Integer> f = pool.submit(() -> 6 * 7);
        try { System.out.println(f.get()); }
        finally {
          pool.shutdown();
          if (!pool.awaitTermination(1, TimeUnit.SECONDS)) pool.shutdownNow();
        }
      }
    }

    BlockingQueue, ConcurrentLinkedQueue y Exchanger

    BlockingQueueConcurrentLinkedQueue
    Vacíatake() esperapoll() devuelve null
    Llenaput() puede esperarNo tiene límite propio
    BackpressureSí con capacidad acotadaHay que diseñarla aparte
    Usoproductor-consumidoralta concurrencia sin bloqueo

    ArrayBlockingQueue es FIFO y tamaño fijo. LinkedBlockingQueue usa nodos y conviene darle capacidad explícita. Exchanger<T> hace que exactamente dos hilos se encuentren e intercambien objetos con exchange().

    Demo 5 · Productor–Consumidor

    Capacidad 4: producir se bloquea si está llena; consumir, si está vacía.

    Cola vacía.

    import java.util.concurrent.*;
    class Cola {
      public static void main(String[] a) {
        BlockingQueue<String> q = new ArrayBlockingQueue<>(4);
        new Thread(() -> { try { q.put("pedido"); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }).start();
        new Thread(() -> { try { System.out.println(q.take()); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }).start();
      }
    }

    Exchanger: encuentro de dos

    Cada hilo presenta un objeto y se bloquea hasta que llega el otro. Entonces ambos reciben el objeto del compañero. Sirve para intercambiar buffers por pares.

    Si el segundo participante nunca llega, el primero puede esperar para siempre; considerá la variante con timeout.

    import java.util.concurrent.Exchanger;
    class Intercambio {
      static final Exchanger<String> x = new Exchanger<>();
      static void pasar(String mensaje) throws InterruptedException {
        String recibido = x.exchange(mensaje);
        System.out.println(recibido);
      }
    }

    Módulo

    06 · Patrones concurrentes

    Patrones: soluciones con nombre

    Un patrón no es código listo: es una estructura conocida para razonar, comunicar decisiones y evitar fallas repetidas.

    Monitor

    Encapsula recurso y exclusión en un objeto. Herramientas: synchronized o ReentrantLock. Usalo para invariantes locales; riesgo: lock demasiado amplio.

    Productor–Consumidor

    Productores dejan trabajo en un búfer; consumidores lo procesan a otra velocidad. Preferí BlockingQueue; alternativas: cola + semáforo o wait/notify.

    Working Threads / Thread Pool

    Una cola alimenta hilos precreados y reutilizables. Herramienta: ExecutorService. Acotá pool y cola para evitar saturación.

    Read–Write Lock

    Muchos lectores o un escritor exclusivo. Herramienta: ReentrantReadWriteLock. Útil sólo si predominan lecturas suficientemente largas.

    Barrier

    Los participantes esperan un punto común antes de la próxima fase. Latch para una vez, CyclicBarrier para grupo fijo, Phaser para etapas dinámicas.

    Maestro–Trabajadores

    El maestro divide, asigna y combina; workers calculan partes. Coordiná con join o Future. Riesgo: maestro cuello de botella.

    Future–Promise

    Future es el resultado que llegará; Promise es quien lo completa. En el temario Java, FutureTask representa ambos lados. get() puede bloquear.

    Actor

    Cada actor tiene estado privado, buzón, dirección y procesa mensajes secuencialmente. Comunicación asíncrona, sin compartir estado. Útil en alta concurrencia/distribuidos y workflows complejos. Se implementa con Akka (Maven) o manualmente con clase + cola + executor.

    Módulo

    07 · Repaso y práctica

    Hoja de repaso: síntoma → problema → herramienta

    SíntomaProblema probableHerramienta/acción
    Incrementos perdidosRace / no atomicidadAtomicInteger o lock
    Cambio no se observaVisibilidadvolatile, synchronized o atómica
    Todos esperan para siempreDeadlockorden de locks, tryLock/timeout
    Uno nunca entraStarvationfair lock, reducir contención
    Se mueven sin avanzarLivelockbackoff aleatorio/política de prioridad
    Limitar 3 conexionesCupo simultáneoSemaphore(3)
    Esperar N finales una vezCoordinaciónCountDownLatch
    Grupo fijo por rondasBarrera reusableCyclicBarrier
    Participantes cambian por faseFases dinámicasPhaser
    Productor demasiado rápidoFalta backpressureBlockingQueue acotada
    Crear demasiados hilosSobrecargaExecutorService/pool acotado

    Glosario navegable

    Atomicidad

    Operación indivisible desde afuera: sucede completa o no sucede.

    Backpressure

    Hacer que el productor reduzca el ritmo cuando el consumidor no alcanza.

    Deadlock

    Espera circular permanente entre tareas.

    Exclusión mutua

    Una sola tarea por vez entra a una sección crítica.

    Visibilidad

    Garantía de que una escritura de un hilo sea observable por otro según reglas de sincronización.

    Monitor

    Lock intrínseco de un objeto más mecanismo de espera/notificación.

    No determinismo

    El orden de ejecución puede variar entre corridas.

    Reentrancia

    El hilo dueño puede adquirir otra vez el mismo lock.

    Race condition

    Resultado incorrecto dependiente del intercalado.

    Starvation

    Una tarea nunca obtiene el recurso aunque otras sí progresan.

    Thread-safe

    Uso concurrente correcto sin corrupción ni sorpresas.

    Quiz final

    Elegí una opción por pregunta y corregí. Se guarda tu mejor puntaje.

    Mejor puntaje: —