Programación de Servicios y Procesos en DAM: hilos, sockets y ejercicios de examen resueltos

Programación de Servicios y Procesos (PSP) es el módulo de segundo de DAM en el que aprendes a hacer programas que hacen varias cosas a la vez: lanzar otros procesos, repartir el trabajo entre hilos sin que se pisen los datos y comunicar dos programas por la red con sockets. Para aprobarlo necesitas dominar cinco ejercicios tipo: lanzar un proceso y leer su salida, crear hilos y esperarlos con join, arreglar una condición de carrera con synchronized, resolver el productor-consumidor con wait y notifyAll, y montar un servidor TCP que atienda a varios clientes a la vez.
Lo que hace difícil PSP no es la cantidad de código, que suele ser corto. Es que el mismo programa puede funcionar bien diez veces y fallar en la undécima, y con lo que aprendiste en primero no sabes explicar por qué. Aquí tienes qué entra, los cinco ejercicios resueltos y explicados, los errores que más vemos en clase y cómo prepararlo.
Qué entra en PSP
PSP es el módulo 0490 del título de DAM y se da en segundo curso. El reparto de horas y el orden de los temas cambian según la comunidad y el centro, y aunque lo normal es darlo en Java, algunos centros lo dan en C# o Python. Los bloques son casi siempre estos:
| Bloque | Qué te piden | Lo que más falla |
|---|---|---|
| Programación multiproceso | Lanzar procesos con ProcessBuilder, leer su salida, esperar con waitFor |
No leer la salida del hijo y que el programa se quede colgado |
| Programación multihilo | Thread, Runnable, start, join, sleep, estados de un hilo |
Llamar a run() en vez de start() |
| Sincronización | synchronized, wait, notifyAll, productor-consumidor, semáforos |
if en lugar de while alrededor de wait() |
| Comunicaciones en red | Sockets TCP y UDP, cliente-servidor, servidor multihilo | Los dos lados esperando a leer a la vez |
| Servicios en red | Protocolos con estado, clientes HTTP, FTP o correo según el centro | Diseñar el protocolo sobre la marcha |
| Programación segura | Hash, cifrado simétrico y asimétrico, firma, SSL/TLS | Confundir hash con cifrado |
Los tres primeros bloques suelen concentrar la mayor parte de la nota práctica, y son los que más suspenden. Los ejercicios de abajo los cubren.
Proceso e hilo: la diferencia que te van a preguntar
Un proceso es un programa en ejecución con su propia memoria: si lanzas dos procesos, no comparten variables y para comunicarse necesitan algo externo (su entrada y salida, un fichero, un socket). Un hilo es una línea de ejecución dentro de un proceso: todos los hilos de un mismo proceso comparten la memoria, por eso se comunican fácilmente y, por eso mismo, pueden estropearse los datos unos a otros.
Si entiendes esa frase, entiendes por qué existe cada tema del módulo. Los procesos se lanzan y se espera su resultado. Los hilos comparten datos y hay que sincronizarlos. Los programas en máquinas distintas se hablan por sockets.
Cinco ejercicios típicos resueltos
Los enunciados cambian según el profesor, pero casi todos los exámenes de PSP son una variación de estos. Intenta hacer cada uno antes de mirar la solución.
1. Lanzar un proceso y leer su salida
Enunciado: el programa ejecuta el comando ping contra localhost, muestra cada línea que imprime el comando y, al final, el código con el que ha terminado.
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.List;
public class LanzarProceso {
public static void main(String[] args) throws IOException, InterruptedException {
// En Windows, cambia "-c" por "-n"
ProcessBuilder pb = new ProcessBuilder(List.of("ping", "-c", "3", "localhost"));
pb.redirectErrorStream(true); // junta la salida de error con la normal
Process proceso = pb.start();
try (BufferedReader salida = new BufferedReader(
new InputStreamReader(proceso.getInputStream()))) {
String linea;
while ((linea = salida.readLine()) != null) {
System.out.println("[hijo] " + linea);
}
}
int codigo = proceso.waitFor();
System.out.println("El proceso terminó con código " + codigo);
}
}
Lo que hay que entender: el comando y cada argumento van por separado en la lista, no en un único texto con espacios. getInputStream() es, desde el punto de vista del padre, la salida del hijo, y ese nombre confunde a casi todo el mundo. Lee la salida antes de waitFor(): si el hijo escribe mucho y nadie lo lee, el búfer se llena, el hijo se bloquea y tu programa se queda esperando para siempre. Un código de salida 0 significa que el proceso terminó bien; cualquier otro valor, que algo falló.
2. Crear hilos y esperar a que terminen
Enunciado: lanza dos hilos que cuenten del 1 al 5 con una pausa entre número y número. El programa principal debe mostrar un mensaje solo cuando los dos hayan terminado.
public class Contador implements Runnable {
private final String nombre;
public Contador(String nombre) {
this.nombre = nombre;
}
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println(nombre + ": " + i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread a = new Thread(new Contador("Hilo A"));
Thread b = new Thread(new Contador("Hilo B"));
a.start();
b.start();
a.join();
b.join();
System.out.println("Los dos hilos han terminado");
}
}
Lo que hay que entender: start() crea un hilo nuevo y ese hilo ejecuta run(). Si llamas tú a run(), el código se ejecuta en el hilo principal, uno detrás de otro, y no hay concurrencia. Las líneas de A y B salen mezcladas y en un orden distinto en cada ejecución: eso es lo normal, no un error. join() bloquea al hilo principal hasta que el otro termina. Sin los join, el mensaje final puede salir antes que los números.
Implementar Runnable es preferible a heredar de Thread, porque tu clase sigue pudiendo heredar de otra. En el examen te pueden pedir las dos formas, así que conviene saber escribirlas.
3. Condición de carrera en un contador compartido
Enunciado: cuatro hilos ingresan 1 € diez mil veces cada uno en la misma cuenta. El saldo final debería ser 40.000. Explica por qué no lo es y corrígelo.
public class Cuenta {
private int saldo = 0;
public void ingresar(int cantidad) {
saldo += cantidad;
}
public int getSaldo() {
return saldo;
}
public static void main(String[] args) throws InterruptedException {
Cuenta cuenta = new Cuenta();
Runnable tarea = () -> {
for (int i = 0; i < 10_000; i++) {
cuenta.ingresar(1);
}
};
Thread[] hilos = new Thread[4];
for (int i = 0; i < hilos.length; i++) {
hilos[i] = new Thread(tarea);
hilos[i].start();
}
for (Thread h : hilos) {
h.join();
}
System.out.println("Saldo final: " + cuenta.getSaldo()); // casi nunca da 40000
}
}
Por qué falla: saldo += cantidad parece una sola operación, pero son tres: leer el saldo, sumarle uno y guardarlo. Si dos hilos leen el mismo valor, por ejemplo 100, los dos guardan 101 y se pierde un ingreso. Eso es una condición de carrera: el resultado depende de en qué orden se intercalen los hilos.
La solución: convertir la operación en una sección crítica, que solo puede ejecutar un hilo a la vez:
public synchronized void ingresar(int cantidad) {
saldo += cantidad;
}
public synchronized int getSaldo() {
return saldo;
}
synchronized en un método bloquea el objeto (this) mientras se ejecuta. Funciona porque los cuatro hilos usan la misma cuenta. Otra solución válida es usar AtomicInteger con incrementAndGet(). Si te piden explicarlo, usa siempre las palabras "condición de carrera", "sección crítica" y "exclusión mutua": es lo que el profesor busca en la respuesta.
4. Productor-consumidor con wait y notifyAll
Enunciado: un hilo productor genera los números del 1 al 10 y los deja en un búfer con capacidad para 3. Un hilo consumidor los saca y los muestra. El productor debe esperar si el búfer está lleno y el consumidor, si está vacío.
Es el ejercicio más típico de PSP. La clave está en el objeto compartido:
import java.util.LinkedList;
import java.util.Queue;
public class Buffer {
private final Queue<Integer> cola = new LinkedList<>();
private final int capacidad;
public Buffer(int capacidad) {
this.capacidad = capacidad;
}
public synchronized void poner(int valor) throws InterruptedException {
while (cola.size() == capacidad) {
wait(); // lleno: suelta el bloqueo y espera
}
cola.add(valor);
notifyAll(); // avisa a quien esté esperando
}
public synchronized int sacar() throws InterruptedException {
while (cola.isEmpty()) {
wait(); // vacío: suelta el bloqueo y espera
}
int valor = cola.remove();
notifyAll();
return valor;
}
}
public class ProductorConsumidor {
public static void main(String[] args) {
Buffer buffer = new Buffer(3);
Thread productor = new Thread(() -> {
try {
for (int i = 1; i <= 10; i++) {
buffer.poner(i);
System.out.println("Producido " + i);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
Thread consumidor = new Thread(() -> {
try {
for (int i = 1; i <= 10; i++) {
System.out.println("Consumido " + buffer.sacar());
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
productor.start();
consumidor.start();
}
}
Lo que hay que entender:
wait()ynotifyAll()solo se pueden llamar dentro de un bloquesynchronizedsobre el mismo objeto. Si no, saltaIllegalMonitorStateException.wait()suelta el bloqueo mientras espera. Por eso el otro hilo puede entrar aponerosacar.- La condición va en un
while, no en unif. Cuando un hilo despierta, otro puede haberse adelantado y vaciar o llenar el búfer otra vez, así que hay que volver a comprobarlo. notifyAll()despierta a todos los que esperan. Connotify()puede despertar a un hilo del mismo tipo y que el programa se quede bloqueado cuando hay varios productores o consumidores.- A veces verás "Consumido 1" antes que "Producido 1". No es un error: los
printlnestán fuera del bloque sincronizado y el orden en que se imprimen no está garantizado.
En la práctica real se usa ArrayBlockingQueue, que ya hace todo esto con put() y take(). Muchos profesores lo aceptan, pero en el examen suelen pedir la versión con wait/notifyAll para ver que entiendes el mecanismo.
5. Servidor TCP que atiende a varios clientes
Enunciado: crea un servidor que escuche en el puerto 6000 y devuelva a cada cliente lo que este le envía, precedido de "ECO:". Debe atender a varios clientes a la vez. El cliente termina enviando "fin".
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
public class ServidorEco {
public static void main(String[] args) throws IOException {
try (ServerSocket servidor = new ServerSocket(6000)) {
System.out.println("Servidor escuchando en el puerto 6000");
while (true) {
Socket cliente = servidor.accept(); // espera a un cliente
new Thread(() -> atender(cliente)).start(); // un hilo por cliente
}
}
}
private static void atender(Socket cliente) {
try (cliente;
BufferedReader entrada = new BufferedReader(
new InputStreamReader(cliente.getInputStream()));
PrintWriter salida = new PrintWriter(cliente.getOutputStream(), true)) {
String linea;
while ((linea = entrada.readLine()) != null) {
if (linea.equalsIgnoreCase("fin")) {
break;
}
salida.println("ECO: " + linea);
}
} catch (IOException e) {
System.err.println("Error con un cliente: " + e.getMessage());
}
}
}
import java.io.*;
import java.net.Socket;
import java.util.Scanner;
public class ClienteEco {
public static void main(String[] args) throws IOException {
try (Socket socket = new Socket("localhost", 6000);
BufferedReader entrada = new BufferedReader(
new InputStreamReader(socket.getInputStream()));
PrintWriter salida = new PrintWriter(socket.getOutputStream(), true);
Scanner teclado = new Scanner(System.in)) {
while (true) {
System.out.print("> ");
String linea = teclado.nextLine();
salida.println(linea);
if (linea.equalsIgnoreCase("fin")) {
break;
}
System.out.println(entrada.readLine());
}
}
}
}
Lo que hay que entender: accept() bloquea hasta que llega un cliente y devuelve un Socket para hablar solo con él. Si atiendes al cliente en el mismo hilo que hace accept(), el segundo cliente se queda esperando hasta que el primero se vaya. Por eso se crea un hilo por cliente. El true del PrintWriter activa el autoflush: sin él, el mensaje se queda en el búfer, el otro lado nunca lo recibe y los dos programas se quedan esperando.
Lo más importante de este ejercicio es el protocolo: quién habla primero, qué envía cada uno y cuándo se termina. Antes de escribir código, apúntalo en tres líneas. La mayoría de servidores que "se cuelgan" en el examen tienen a los dos lados esperando a leer a la vez.
La parte de programación segura
Suele ser la parte más teórica del módulo y la que menos tiempo recibe en clase, pero cae en el examen. Lo mínimo que tienes que tener claro:
| Concepto | Para qué sirve | En Java |
|---|---|---|
| Hash (resumen) | Comprobar que algo no ha cambiado o guardar contraseñas. No se puede revertir | MessageDigest.getInstance("SHA-256") |
| Cifrado simétrico | Cifrar y descifrar con la misma clave. Rápido | Cipher con AES |
| Cifrado asimétrico | Clave pública para cifrar, privada para descifrar | KeyPairGenerator con RSA |
| Firma digital | Demostrar quién envía algo y que no se ha modificado | Signature con SHA256withRSA |
| SSL/TLS | Cifrar una conexión por sockets | SSLSocket y SSLServerSocket |
Un hash no es cifrado: no hay clave y no se puede "descifrar". Esa confusión aparece en casi todos los exámenes teóricos. Calcular el hash de un texto se reduce a esto:
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] hash = md.digest(texto.getBytes(StandardCharsets.UTF_8));
String hex = HexFormat.of().formatHex(hash); // HexFormat existe desde Java 17
Los errores que más suspenden en PSP
Llamar a run() en lugar de start(). El programa funciona, pero en un solo hilo. Si el enunciado pide concurrencia, está mal aunque el resultado coincida.
No esperar a los hilos. Sin join(), el main lee los resultados antes de que estén calculados. Da números que cambian en cada ejecución y parece una condición de carrera, pero no lo es.
Sincronizar sobre objetos distintos. Si cada hilo tiene su propia instancia de una clase con un método synchronized, cada uno bloquea un objeto diferente y no hay exclusión mutua. El bloqueo tiene que estar en el objeto compartido, como la Cuenta o el Buffer de los ejercicios.
if alrededor de wait(). Funciona con un productor y un consumidor y se rompe en cuanto el profesor añade un segundo consumidor. Usa siempre while.
Usar Thread.sleep() para "sincronizar". Poner un sleep(1000) para que el otro hilo "ya haya terminado" no garantiza nada. Funciona en tu ordenador y falla en el del profesor. Para esperar se usa join(), wait() o una estructura de sincronización.
Mensajes que nunca llegan por el socket. Enviar con print (sin salto de línea) cuando el otro lado lee con readLine(), o escribir sin flush. El receptor se queda esperando el fin de línea para siempre.
BindException: Address already in use. El servidor de la ejecución anterior sigue vivo y ocupa el puerto. Páralo desde el IDE antes de volver a lanzarlo. Si te pasa a la primera y usas Mac, el puerto 5000 lo ocupa el receptor AirPlay del sistema: usa otro, como el 6000 del ejemplo.
Interbloqueo (deadlock). Dos hilos que necesitan dos bloqueos y los cogen en orden contrario se esperan el uno al otro para siempre. Si tu programa se queda parado sin errores, piensa en esto. La regla para evitarlo es pedir siempre los bloqueos en el mismo orden.
Cómo depurar un programa concurrente
El depurador paso a paso ayuda menos que en primero, porque al pararte en un punto de interrupción cambias el orden en el que se intercalan los hilos y el fallo puede desaparecer. Lo que funciona mejor:
- Imprime el nombre del hilo en cada mensaje con
Thread.currentThread().getName(). Así ves quién hace qué y en qué orden. - Haz que el fallo salga más a menudo. Más hilos y más iteraciones (100.000 en lugar de 10) convierten un error que sale una vez de cada veinte en uno que sale siempre.
- Si se queda colgado, mira dónde. En IntelliJ y Eclipse puedes pausar el programa y ver en qué línea está cada hilo. Con
jstackdesde la terminal también. Si dos hilos están enwait()o esperando un bloqueo, ya sabes por dónde va el problema. - Prueba el servidor con un cliente sencillo primero. Asegúrate de que un solo cliente funciona antes de pasar a varios.
El método general, que también sirve aquí, lo tienes en cómo depurar código paso a paso.
Cómo prepararlo
Asegura la base de Java. PSP da por hecho que dominas clases, interfaces, excepciones y lambdas. Si un NullPointerException o un try-catch todavía te bloquean, repasa primero los errores de Java más comunes en DAM y DAW.
Escribe de memoria las cinco plantillas. Hilo con Runnable y join, método synchronized, búfer con wait/notifyAll, servidor con accept en bucle y un hilo por cliente, y cliente con PrintWriter y BufferedReader. Si las escribes sin mirar, el examen es adaptarlas al enunciado.
Haz variaciones. Coge el productor-consumidor y añade dos consumidores. Coge el servidor de eco y conviértelo en un chat que reenvíe cada mensaje a todos los clientes conectados. Es casi siempre lo que piden en la práctica final.
Ejecuta cada programa varias veces. Una ejecución correcta no demuestra que el programa funcione. Si el resultado cambia entre ejecuciones, tienes un problema de sincronización.
Repasa la teoría con tus palabras. Proceso frente a hilo, estados de un hilo, condición de carrera, sección crítica, interbloqueo, TCP frente a UDP, hash frente a cifrado. Son las preguntas cortas que más se repiten. Para organizar las últimas semanas, mira cómo preparar un examen práctico de programación.
En segundo también tienes Acceso a Datos, el otro módulo que más suspende del curso. Si lo tienes a la vez, aquí tienes cómo aprobar Acceso a Datos con ejercicios de ficheros, JDBC e Hibernate resueltos.
Cuándo pedir ayuda
Si haces los ejercicios, entiendes por qué fallan cuando fallan y sabes explicar la solución, vas bien. Merece la pena buscar ayuda cuando:
- Tu programa da resultados distintos en cada ejecución y no sabes por qué.
- Copias el productor-consumidor de los apuntes, funciona, pero no sabrías explicar qué hace
wait(). - Cliente y servidor se quedan colgados y no sabes cuál de los dos está esperando.
- Tienes PSP pendiente y necesitas aprobarlo para empezar las prácticas en empresa o el proyecto final.
En nuestras clases de refuerzo para DAM y DAW trabajamos sobre tus enunciados y tu código: ejecutamos juntos, buscamos dónde se rompe la sincronización y lo arreglas tú, para que en el examen no dependas de nadie. Si lo que te falta es base de Java, empezamos por ahí con clases de Java online. Son clases online uno a uno a 25 € la hora, sin matrícula ni permanencia.
Preguntas frecuentes
¿Qué es PSP en DAM?
PSP es Programación de Servicios y Procesos, el módulo 0490 de segundo de DAM. Enseña a programar aplicaciones que ejecutan varias tareas a la vez (procesos e hilos), a sincronizarlas, a comunicar programas por red con sockets y a aplicar técnicas básicas de programación segura como el hash y el cifrado.
¿Es difícil PSP?
Es uno de los módulos que más cuesta de segundo de DAM, sobre todo por la concurrencia: los errores no siempre aparecen y el depurador ayuda poco. El código suele ser corto. Si llegas con una buena base de Java y practicas los ejercicios tipo, se aprueba sin problema; si llegas arrastrando Programación de primero, se hace cuesta arriba.
¿PSP se da en Java, C# o Python?
Lo más habitual es Java, porque es el lenguaje de primero de DAM en la mayoría de centros. Algunos centros lo dan en C# y existen libros en Python. Los conceptos (hilos, sincronización, sockets) son los mismos en los tres lenguajes y solo cambia la sintaxis.
¿Qué diferencia hay entre un proceso y un hilo?
Un proceso es un programa en ejecución con su propia memoria, aislada de los demás procesos. Un hilo es una línea de ejecución dentro de un proceso y comparte la memoria con los otros hilos de ese proceso. Por eso los hilos se comunican fácilmente, pero necesitan sincronización para no corromper los datos compartidos.
¿Qué diferencia hay entre TCP y UDP en PSP?
TCP establece una conexión y garantiza que los datos llegan completos y en orden; en Java se usa con ServerSocket y Socket. UDP envía paquetes sueltos sin conexión ni garantía de entrega; en Java se usa con DatagramSocket y DatagramPacket. En el examen, la mayoría de ejercicios son con TCP.
¿Puedo usar ChatGPT para las prácticas de PSP?
Para que te explique un concepto o te proponga ejercicios parecidos, sí. Para que te escriba la práctica, no: en concurrencia es muy habitual que el código generado funcione en una ejecución y tenga una condición de carrera escondida, y en el examen tendrás que explicar por qué tu código es correcto.