Ejercicios mixtos de concurrencia
En cada ejercicio sigue: predicción → ejecución repetida → evidencia → explicación → práctica. La solución evalúa garantías, no una salida afortunada.
1. Concurrencia o paralelismo
Situación. Dos tareas progresan intercaladas en un único núcleo.
Predicción. ¿Hay concurrencia, paralelismo o ambos?
A1 → B1 → A2 → B2
Garantizado: ambas progresan durante el mismo periodo. No garantizado: ejecución simultánea.
Criterio. Distinguir progreso solapado de simultaneidad física.
Solución y explicación
Hay concurrencia. Esa secuencia no demuestra paralelismo; un solo núcleo puede producirla mediante intercalación.
2. Órdenes posibles
Situación. A imprime A1, A2; B imprime B1, B2, sin coordinación.
Predicción. Clasifica A1 B1 B2 A2 y A2 A1 B1 B2.
new Thread(() -> { System.out.println("A1"); System.out.println("A2"); }).start();
new Thread(() -> { System.out.println("B1"); System.out.println("B2"); }).start();
Garantizado: orden interno A1→A2 y B1→B2. No garantizado: orden relativo.
Criterio. No exigir una salida exacta.
Solución y explicación
La primera secuencia es posible; la segunda viola el orden del hilo A. Repetir no convierte el orden observado en garantía.
3. start() frente a run()
Situación. El autor espera dos hilos, pero ambos mensajes muestran main.
Thread hilo = new Thread(() -> System.out.println(Thread.currentThread().getName()));
hilo.run();
Predicción. ¿Qué hilo ejecuta la tarea? Garantizado: run() es una llamada normal. No garantizado: nada crea un hilo nuevo.
Criterio. Usar start() una sola vez.
Solución y explicación
hilo.start();
hilo.join();
start() inicia el nuevo hilo; join() solo garantiza que el llamador espera su final.
4. Executor que no se cierra
Situación. El programa puede quedar vivo tras imprimir.
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.submit(() -> System.out.println("hecho"));
Predicción. ¿Qué recurso falta cerrar? Garantizado: los trabajadores del pool no se cierran por terminar una tarea. No garantizado: que el proceso finalice solo.
Criterio. Cierre en finally.
Solución y explicación
ExecutorService executor = Executors.newFixedThreadPool(2);
try {
executor.submit(() -> System.out.println("hecho")).get();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (ExecutionException e) {
System.err.println("La tarea falló: " + e.getCause().getMessage());
} finally {
executor.shutdown();
}
En código robusto se añade awaitTermination y fallback a shutdownNow().
5. Detectar una race
Situación. Cuatro tareas ejecutan contador++.
class Contador { int valor; void incrementar() { valor++; } }
Predicción. ¿Debe fallar siempre? Garantizado: no hay atomicidad para read-modify-write. No garantizado: que una ejecución manifieste pérdida.
Criterio. Identificar el defecto por intercalaciones, no por un test que espera fallar.
Solución y explicación
Usa el mismo monitor para todos los accesos o AtomicInteger.incrementAndGet(). Un resultado correcto ocasional no elimina la race.
6. La falsa solución volatile
Situación. Se cambia a volatile int contador y se mantiene contador++.
Predicción. ¿Es atómico? Garantizado: visibilidad/orden para accesos volatile. No garantizado: atomicidad del incremento ni invariantes compuestas.
Criterio. Elegir el mecanismo por la garantía necesaria.
Solución y explicación
AtomicInteger contador = new AtomicInteger();
contador.incrementAndGet();
volatile no equivale a synchronized ni vuelve thread-safe cualquier uso.
7. Interrupción preservada
Situación. Una espera traga la señal.
try { cola.take(); } catch (InterruptedException e) { }
Predicción. ¿Podrá observarla el llamador? Garantizado: la operación suele limpiar el flag al lanzar. No garantizado: que la tarea se cancele si ignora la excepción.
Criterio. Propagar o restaurar y terminar.
Solución y explicación
try {
cola.take();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
8. Timeout y cancelación
Situación. El coordinador no quiere esperar indefinidamente.
Future<String> future = executor.submit(tarea);
String valor = future.get();
Predicción. ¿Qué ocurre si tarda? Garantizado: get() puede bloquear. No garantizado: que un timeout termine la tarea.
Criterio. Política explícita y tarea cooperativa.
Solución y explicación
try {
String valor = future.get(1, TimeUnit.SECONDS);
} catch (TimeoutException e) {
future.cancel(true);
} catch (InterruptedException e) {
future.cancel(true);
Thread.currentThread().interrupt();
} catch (ExecutionException e) {
System.err.println("La tarea falló: " + e.getCause().getMessage());
}
cancel(true) solicita interrupción; no mata la tarea ni garantiza fin inmediato.
9. Diagnosticar un bloqueo simple
Situación. Dos rutas adquieren locks en orden contrario.
// tarea A: synchronized (uno) { synchronized (dos) { ... } }
// tarea B: synchronized (dos) { synchronized (uno) { ... } }
Predicción. Existe una intercalación donde cada tarea retiene un monitor y espera el otro. Garantizado: nada obliga a que esa intercalación aparezca. No garantizado: que una prueba quede bloqueada.
Criterio. Razonar sin ejecutar un deadlock indefinido.
Solución y explicación
Adquiere siempre uno y después dos, o rediseña para necesitar un solo monitor. No automatices una prueba que dependa de que el deadlock se manifieste.
Reto integrado: proceso controlado desde una tarea
Situación. Ejecuta una clase Java hija mediante un pool de un hilo, consume su salida combinada y aplica timeout.
Predicción. Enumera cuándo pueden quedar vivos el Future, el proceso y el executor.
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<Integer> future = executor.submit(() -> ejecutarProcesoJava());
Garantizado: cada argumento de ProcessBuilder conserva sus límites; el código de salida existe tras terminar. No garantizado: duración ni terminación inmediata tras cancelar.
Criterio. Argumentos separados, streams consumidos, timeout explícito, proceso y executor cerrados.
Solución y explicación
La tarea debe crear el hijo, combinar o consumir ambos streams y usar waitFor(timeout, ...). Si vence, llama destroy(), espera brevemente y usa destroyForcibly() solo si continúa vivo. El coordinador aplica future.get(timeout, ...), solicita cancelación si procede y cierra el executor en finally. La tarea debe reaccionar a interrupción y destruir el proceso que controla.
La solución no promete un orden de scheduling: demuestra propiedades mediante coordinación explícita y cierre verificable.