Laboratorio 10 — Programación Concurrente en Java - jorgealbertopineda78-del/java GitHub Wiki
Unidad: 10 — Programación Concurrente
Duración estimada: 90 minutos
Nivel: Intermedio-Avanzado
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 esteEn 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.
- Crear hilos extendiendo
Thready usando la interfazRunnable - Entender el ciclo de vida de un hilo
- Usar
synchronizedpara proteger datos compartidos - Simular un torneo RPG con combates concurrentes
El Gran Torneo del Gremio enfrenta parejas de personajes en distintos campos de batalla simultáneamente. Cada combate es un hilo independiente que:
- Ejecuta rondas de ataque y defensa
- Registra el resultado en una tabla de resultados compartida
- Termina cuando uno de los dos personajes queda derrotado
El desafío: varios hilos escriben en la misma tabla → hay que protegerla con synchronized.
┌────────────────────────────────────────────┐
│ «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 │
└────────────────────────────────────────────┘
new Thread() t.start() run() termina
───────────── → ─────────── → → ─────────────
NEW RUNNABLE TERMINATED
│
(scheduler
del SO)
↓
RUNNING ←──────────────────
│ │
sleep() / wait() notify() / tiempo
↓
BLOCKED/WAITING
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, lanzaInterruptedException.
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 Threadvsimplements Runnable
Conextends Threadel objeto ES un hilo, pero Java no permite herencia múltiple — si ya extiendesPersonaje, no puedes también extenderThread.
Conimplements Runnableseparas la lógica del hilo de la clase base: más flexible y más usado en código profesional.
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
}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());=== 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.
| 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 |
Responde en respuestas.txt:
- ¿Cuál es la diferencia entre
extends Threadeimplements Runnable? ¿Cuándo preferirías uno sobre el otro? - ¿Qué es una race condition? Explica con el ejemplo del
ContadorCompartidopor qué el resultado sinsynchronizedpuede ser menor a 5000. - ¿Para qué sirve
join()? ¿Qué pasaría si quitaras losjoin()del bloque 3 antes de llamartabla.mostrarResultados()?
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