Laboratorio 10 — Programación Concurrente en Java - jorgealbertopineda78-del/java GitHub Wiki

Laboratorio 10 — Programación Concurrente en Java

Unidad: 10 — Programación Concurrente
Duración estimada: 90 minutos
Nivel: Intermedio-Avanzado


Conexión con laboratorios anteriores

En todos los labs anteriores el sistema RPG ejecutaba las acciones una por una, en orden:

motor.ejecutarTurno(sylva, malachar);   // espera a terminar
motor.ejecutarTurno(legolas, thorin);   // luego ejecuta este
motor.ejecutarTurno(druida, arquero);   // y luego este

En un torneo real, múltiples combates ocurren al mismo tiempo en distintos campos de batalla. Para simular eso, necesitamos que cada combate corra en su propio hilo (Thread), de forma paralela.


Objetivo

  • Crear hilos extendiendo Thread y usando la interfaz Runnable
  • Entender el ciclo de vida de un hilo
  • Usar synchronized para proteger datos compartidos
  • Simular un torneo RPG con combates concurrentes

Contexto del problema

El Gran Torneo del Gremio enfrenta parejas de personajes en distintos campos de batalla simultáneamente. Cada combate es un hilo independiente que:

  1. Ejecuta rondas de ataque y defensa
  2. Registra el resultado en una tabla de resultados compartida
  3. Termina cuando uno de los dos personajes queda derrotado

El desafío: varios hilos escriben en la misma tabla → hay que protegerla con synchronized.


Diagrama UML

┌────────────────────────────────────────────┐
│  «interface» Runnable                      │
│  + run()                                   │
└────────────────────┬───────────────────────┘
                     │ implements
                     ▼
┌────────────────────────────────────────────┐
│           CombateConcurrente               │
│  - nombre: String                          │
│  - atacante: Personaje                     │
│  - defensor: Personaje                     │
│  - resultados: TablaResultados             │
│                                            │
│  + run()   ← lógica del combate            │
└────────────────────────────────────────────┘

┌────────────────────────────────────────────┐
│           TablaResultados                  │
│  - registros: ArrayList<String>            │
│                                            │
│  + synchronized registrar(String resultado)│
│  + mostrarResultados()                     │
└────────────────────────────────────────────┘

┌────────────────────────────────────────────┐
│  java.lang.Thread   (extiende directamente)│
│           MonitorTorneo                    │
│  - nombreCampo: String                     │
│                                            │
│  + run()  ← reporta estado del campo       │
└────────────────────────────────────────────┘

Ciclo de vida de un hilo

  new Thread()        t.start()         run() termina
  ─────────────  →  ───────────  →  →  ─────────────
    NEW             RUNNABLE         TERMINATED
                       │
                    (scheduler
                     del SO)
                       ↓
                   RUNNING  ←──────────────────
                       │                       │
               sleep() / wait()         notify() / tiempo
                       ↓
                   BLOCKED/WAITING

Instrucciones

Parte 1 — TablaResultados y CombateConcurrente con Runnable

Crea TablaResultados.java — recurso compartido entre hilos:

import java.util.ArrayList;

public class TablaResultados {

    private ArrayList<String> registros = new ArrayList<>();

    // synchronized garantiza que solo un hilo a la vez modifica la lista
    public synchronized void registrar(String resultado) {
        registros.add(resultado);
        System.out.println("[Tabla] Registrado: " + resultado);
    }

    public void mostrarResultados() {
        System.out.println("\n=== Resultados del Torneo ===");
        for (int i = 0; i < registros.size(); i++) {
            System.out.println((i + 1) + ". " + registros.get(i));
        }
        System.out.println("Total de combates: " + registros.size());
    }
}

Crea CombateConcurrente.java — implementa Runnable:

public class CombateConcurrente implements Runnable {

    private String           nombre;
    private Personaje        atacante;
    private Personaje        defensor;
    private TablaResultados  resultados;

    public CombateConcurrente(String nombre,
                               Personaje atacante,
                               Personaje defensor,
                               TablaResultados resultados) {
        this.nombre     = nombre;
        this.atacante   = atacante;
        this.defensor   = defensor;
        this.resultados = resultados;
    }

    @Override
    public void run() {
        System.out.println("[" + nombre + "] INICIO — " +
                           atacante.getNombre() + " vs " +
                           defensor.getNombre());

        int ronda = 1;

        while (atacante.isEstaVivo() && defensor.isEstaVivo()) {

            System.out.println("[" + nombre + "] Ronda " + ronda);

            try {
                // Simula que el combate tarda un tiempo
                Thread.sleep(500);

                atacante.atacar();
                int danio = atacante.calcularDanio();
                defensor.recibirDanio(danio);

            } catch (RpgException e) {
                System.out.println("[" + nombre + "] ⚠ " + e.getMessage());
                break;

            } catch (InterruptedException e) {
                System.out.println("[" + nombre + "] Combate interrumpido.");
                Thread.currentThread().interrupt();
                break;
            }

            ronda++;
        }

        // Determinar ganador y registrar en la tabla compartida
        String ganador = atacante.isEstaVivo()
                       ? atacante.getNombre()
                       : defensor.getNombre();

        String registro = nombre + " → Ganador: " + ganador +
                          " (rondas: " + ronda + ")";

        resultados.registrar(registro);

        System.out.println("[" + nombre + "] FIN — " + ganador + " gana.");
    }
}

💡 Thread.sleep(500) pausa el hilo actual 500 milisegundos sin bloquear a los otros hilos. Simula que el combate tarda tiempo real. Si se interrumpe el hilo desde afuera, lanza InterruptedException.


Parte 2 — MonitorTorneo extendiendo Thread (en clase con el profesor)

Crea MonitorTorneo.java — usa la segunda forma de crear hilos, extendiendo Thread:

public class MonitorTorneo extends Thread {

    private String nombreCampo;
    private int    duracionSegundos;

    public MonitorTorneo(String nombreCampo, int duracionSegundos) {
        this.nombreCampo       = nombreCampo;
        this.duracionSegundos  = duracionSegundos;
    }

    @Override
    public void run() {
        System.out.println("[Monitor] Vigilando campo: " + nombreCampo);

        for (int i = 1; i <= duracionSegundos; i++) {
            try {
                Thread.sleep(1000);   // espera 1 segundo
                System.out.println("[Monitor] " + nombreCampo +
                                   " — segundo " + i +
                                   " de " + duracionSegundos);
            } catch (InterruptedException e) {
                System.out.println("[Monitor] Monitor interrumpido.");
                Thread.currentThread().interrupt();
                return;
            }
        }

        System.out.println("[Monitor] " + nombreCampo + " finalizado.");
    }
}

💡 extends Thread vs implements Runnable
Con extends Thread el objeto ES un hilo, pero Java no permite herencia múltiple — si ya extiendes Personaje, no puedes también extender Thread.
Con implements Runnable separas la lógica del hilo de la clase base: más flexible y más usado en código profesional.


Parte 3 — join() y sincronización (trabajo independiente)

Agrega a tu Main una demostración de join():

Crea ContadorCompartido.java — demuestra el problema de concurrencia sin synchronized:

public class ContadorCompartido {

    private int contador = 0;

    // SIN synchronized — puede dar resultados incorrectos
    public void incrementar() {
        contador++;
    }

    // CON synchronized — seguro entre hilos
    public synchronized void incrementarSeguro() {
        contador++;
    }

    public int getContador() {
        return contador;
    }

    public void resetear() {
        contador = 0;
    }
}

Tarea: en el Main, crea 5 hilos que cada uno llame incrementar() 1000 veces (sin synchronized). El resultado debería ser 5000 pero casi nunca lo es. Luego repite con incrementarSeguro() y observa la diferencia.

// Pista: crear e iniciar hilos en un loop
for (int i = 0; i < 5; i++) {
    Thread t = new Thread(() -> {
        for (int j = 0; j < 1000; j++) {
            contador.incrementar();   // sin sync
        }
    });
    t.start();
    // t.join() aquí espera a que este hilo termine antes de continuar
}

Parte 4 — Clase principal Main (trabajo independiente)

Demuestra el sistema con 4 bloques:

Bloque 1 — Torneo con 3 combates simultáneos:

TablaResultados tabla = new TablaResultados();

Personaje s1 = new Druida("Sylva",    10, 300, 100);
Personaje m1 = new Nigromante("Malachar", 8, 150, 120);
Personaje l1 = new Arquero("Legolas", 6, 200, "Arco", 10, 95);
Personaje t1 = new Guerrero("Thorin", 9, 400, "Hacha", 80);
Personaje b1 = new Druida("Brego",   7, 250, 80);
Personaje k1 = new Nigromante("Kael", 5, 180, 90);

// Crear los Runnable
CombateConcurrente c1 = new CombateConcurrente("Campo-1", s1, m1, tabla);
CombateConcurrente c2 = new CombateConcurrente("Campo-2", l1, t1, tabla);
CombateConcurrente c3 = new CombateConcurrente("Campo-3", b1, k1, tabla);

// Envolver en Thread e iniciar
Thread h1 = new Thread(c1);
Thread h2 = new Thread(c2);
Thread h3 = new Thread(c3);

h1.start();
h2.start();
h3.start();

Bloque 2 — Monitor del torneo (hilo independiente):

MonitorTorneo monitor = new MonitorTorneo("Torneo Principal", 3);
monitor.start();

Bloque 3 — Esperar a que todos terminen con join():

try {
    h1.join();   // espera a que Campo-1 termine
    h2.join();   // espera a que Campo-2 termine
    h3.join();   // espera a que Campo-3 termine
    monitor.join();
} catch (InterruptedException e) {
    System.out.println("Espera interrumpida.");
}

System.out.println("\nTodos los combates han terminado.");
tabla.mostrarResultados();

Bloque 4 — Demostración de race condition:

ContadorCompartido contador = new ContadorCompartido();

// Sin synchronized — resultado incorrecto
Thread[] hilos = new Thread[5];
for (int i = 0; i < 5; i++) {
    hilos[i] = new Thread(() -> {
        for (int j = 0; j < 1000; j++) contador.incrementar();
    });
    hilos[i].start();
}
for (Thread t : hilos) t.join();
System.out.println("Sin sync  — esperado: 5000 | real: " +
                   contador.getContador());

// Con synchronized — resultado correcto
contador.resetear();
for (int i = 0; i < 5; i++) {
    hilos[i] = new Thread(() -> {
        for (int j = 0; j < 1000; j++) contador.incrementarSeguro();
    });
    hilos[i].start();
}
for (Thread t : hilos) t.join();
System.out.println("Con sync  — esperado: 5000 | real: " +
                   contador.getContador());

Ejemplo de salida esperada

=== Gran Torneo del Gremio ===

[Campo-1] INICIO — Sylva vs Malachar
[Campo-2] INICIO — Legolas vs Thorin
[Campo-3] INICIO — Brego vs Kael
[Monitor] Vigilando campo: Torneo Principal

[Campo-3] Ronda 1
[Campo-1] Ronda 1
[Campo-2] Ronda 1
[Monitor] Torneo Principal — segundo 1 de 3

[Campo-3] Brego recibe 150 de daño. Vida: 100
[Campo-1] Malachar recibe 240 de daño. Vida: 0
[Campo-2] Thorin recibe 72 de daño. Vida: 328

[Campo-1] FIN — Sylva gana.
[Tabla] Registrado: Campo-1 → Ganador: Sylva (rondas: 2)
[Monitor] Torneo Principal — segundo 2 de 3

...

Todos los combates han terminado.

=== Resultados del Torneo ===
1. Campo-1 → Ganador: Sylva (rondas: 2)
2. Campo-3 → Ganador: Brego (rondas: 3)
3. Campo-2 → Ganador: Thorin (rondas: 4)
Total de combates: 3

Sin sync  — esperado: 5000 | real: 4732   ← varía cada ejecución
Con sync  — esperado: 5000 | real: 5000   ← siempre correcto

💡 El orden de los mensajes de los combates varía en cada ejecución — eso es completamente normal y es la esencia de la concurrencia. Lo que importa es que el resultado final en la tabla siempre sea correcto.


Criterios de evaluación

Criterio Puntos
CombateConcurrente implementa Runnable correctamente con run() 20
MonitorTorneo extiende Thread y usa Thread.sleep() 15
TablaResultados usa synchronized en el método registrar 20
ContadorCompartido demuestra race condition sin y con synchronized 20
Main inicia hilos con start(), espera con join(), muestra resultados 15
Manejo correcto de InterruptedException en todos los hilos 10
Total 100

Preguntas de reflexión

Responde en respuestas.txt:

  1. ¿Cuál es la diferencia entre extends Thread e implements Runnable? ¿Cuándo preferirías uno sobre el otro?
  2. ¿Qué es una race condition? Explica con el ejemplo del ContadorCompartido por qué el resultado sin synchronized puede ser menor a 5000.
  3. ¿Para qué sirve join()? ¿Qué pasaría si quitaras los join() del bloque 3 antes de llamar tabla.mostrarResultados()?

Entregable

Sube al repositorio en la carpeta lab10_concurrencia:

lab10_concurrencia/
├── Personaje.java
├── Guerrero.java
├── Arquero.java
├── Druida.java
├── Nigromante.java
├── (excepciones del Lab07)
├── TablaResultados.java
├── CombateConcurrente.java
├── MonitorTorneo.java
├── ContadorCompartido.java
└── Main.java

Laboratorio preparado para el curso de Programación Orientada a Objetos — Ago-Dic 2025

⚠️ **GitHub.com Fallback** ⚠️