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.
Proceso
Hilo
Memoria
Aislada
Compartida dentro del proceso
Creación/cambio
Más costoso
Más liviano
Falla
Mejor aislamiento
Puede afectar al proceso
Comunicación
IPC/mensajes
Memoria 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
Criterio
Memoria compartida
Mensajes
Idea
Varios hilos leen/escriben el mismo objeto
Se envían datos por cola, canal o buzón
Ventaja
Acceso directo y rápido
Aislamiento y razonamiento local
Costo
Sincronización delicada
Copia/cola y protocolo
Ejemplo
Contador compartido
Pedidos 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:
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
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
CountDownLatch
CyclicBarrier
Phaser
Idea
esperar N finales
N pares se encuentran
etapas con participantes dinámicos
Reutiliza
No
Sí, automático
Sí, por fases
Cambian participantes
No
No
Sí: register/deregister
Métodos
countDown/await
barrier.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í
Cuando
Cuidado
synchronized
exclusión simple y estructurada
mantener bloque corto
ReentrantLock
tryLock, timeout, interrupción o fairness
unlock en finally
Semaphore
limitar N accesos
balance 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
BlockingQueue
ConcurrentLinkedQueue
Vacía
take() espera
poll() devuelve null
Llena
put() puede esperar
No tiene límite propio
Backpressure
Sí con capacidad acotada
Hay que diseñarla aparte
Uso
productor-consumidor
alta 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íntoma
Problema probable
Herramienta/acción
Incrementos perdidos
Race / no atomicidad
AtomicInteger o lock
Cambio no se observa
Visibilidad
volatile, synchronized o atómica
Todos esperan para siempre
Deadlock
orden de locks, tryLock/timeout
Uno nunca entra
Starvation
fair lock, reducir contención
Se mueven sin avanzar
Livelock
backoff aleatorio/política de prioridad
Limitar 3 conexiones
Cupo simultáneo
Semaphore(3)
Esperar N finales una vez
Coordinación
CountDownLatch
Grupo fijo por rondas
Barrera reusable
CyclicBarrier
Participantes cambian por fase
Fases dinámicas
Phaser
Productor demasiado rápido
Falta backpressure
BlockingQueue acotada
Crear demasiados hilos
Sobrecarga
ExecutorService/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.