Saltar al contenido principal

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.