Saltar al contenido principal

Procesos, hilos y memoria

Antes de crear hilos necesitamos un modelo mental que permita explicar qué puede ocurrir. En concurrencia no basta con memorizar una API: debemos distinguir las garantías del programa de una salida observada por casualidad.

Proceso e hilo​

Un proceso es una instancia de un programa en ejecución. Tiene recursos propios gestionados por el sistema operativo, como un espacio de memoria y descriptores de archivos.

Al iniciar una aplicación Java normalmente arrancamos un proceso que ejecuta una JVM. Dentro puede haber varios hilos. Cada hilo es un flujo de ejecución y puede avanzar por instrucciones distintas mientras comparte recursos del proceso.

Dos procesos están aislados: uno no accede directamente a la memoria del otro y necesitan mecanismos explícitos para comunicarse. Los hilos del mismo proceso, en cambio, pueden acceder a objetos compartidos. Esto facilita la comunicación, pero exige coordinación.

ConceptoProcesoHilo
MemoriaAislada de otros procesosComparte recursos del proceso
Creación y gestiónGeneralmente más costosaGeneralmente más ligera
ComunicaciónMecanismo explícitoPuede usar objetos compartidos

El coste exacto depende del sistema, la JVM y la carga. Crear muchos hilos tampoco es gratis: consumen memoria y tiempo de planificación.

Stack por hilo y heap compartido​

Cada hilo tiene su propio stack, donde mantiene llamadas a métodos y variables locales. Los objetos suelen vivir en el heap, accesible desde los hilos que dispongan de una referencia.

Esto no significa «stack seguro, heap inseguro». Una referencia local puede apuntar a un objeto compartido:

List<String> nombres = obtenerListaCompartida();

La referencia nombres es local, pero el objeto puede ser accesible desde varios hilos. La seguridad depende de quién puede acceder al estado y cómo se coordina el acceso. Una estrategia sencilla consiste en evitar estado mutable compartido: cada tarea trabaja con sus datos y devuelve un resultado.

Concurrencia y paralelismo​

Hay concurrencia cuando varias tareas progresan durante el mismo periodo. Sus pasos pueden intercalarse aunque una CPU ejecute solo uno cada vez.

Hay paralelismo cuando varias tareas se ejecutan realmente al mismo tiempo, por ejemplo en núcleos distintos. Un programa puede ser concurrente sin ser paralelo, y disponer de varios núcleos no garantiza que dos tareas concretas coincidan.

Concurrencia posible: A1 → B1 → A2 → B2

Paralelismo posible:
Núcleo 1: A1 → A2
Núcleo 2: B1 → B2

Scheduler e interleaving​

El scheduler decide qué hilos pueden avanzar y cuándo. La intercalación (interleaving) es la secuencia concreta producida durante una ejecución.

Hilo A: A1 → A2
Hilo B: B1 → B2

Sin coordinación deben conservarse A1 antes de A2 y B1 antes de B2, pero son posibles, entre otras, estas intercalaciones:

A1, A2, B1, B2
A1, B1, A2, B2
B1, A1, B2, A2
B1, B2, A1, A2

No podemos predecir cuál aparecerá. La carga, el hardware o una ejecución anterior no añaden garantías al programa.

Experimento observable​

Todavía no estudiaremos Thread en detalle; aquí lo usamos para observar el modelo:

public class OrdenObservable {
public static void main(String[] args) throws InterruptedException {
Thread hiloA = new Thread(() -> {
System.out.println("A1");
System.out.println("A2");
});
Thread hiloB = new Thread(() -> {
System.out.println("B1");
System.out.println("B2");
});

hiloA.start();
hiloB.start();
hiloA.join();
hiloB.join();
System.out.println("Fin");
}
}

Aplica el patrón del curso:

  1. Predicción: enumera restricciones y resultados permitidos.
  2. Ejecución repetida: ejecuta varias veces.
  3. Evidencia: anota las salidas sin generalizarlas.
  4. Explicación: separa lo observado de lo garantizado.
  5. Práctica: cambia los mensajes y vuelve a razonar.

Está garantizado que A1 precede a A2, que B1 precede a B2 y que Fin aparece después de ambos hilos. No está garantizado el orden relativo entre A y B.

Observado no significa garantizado

Si diez ejecuciones muestran la misma salida, solo tenemos diez observaciones. Para afirmar un orden necesitamos una regla que lo garantice.

Compartir memoria: ventaja y riesgo​

Una operación que parece única en el código fuente puede implicar varios pasos intercalables. Más adelante separaremos tres preguntas:

  • Visibilidad: ¿un hilo tiene garantía de observar una escritura de otro?
  • Atomicidad: ¿una operación puede intercalarse con otra?
  • Exclusión mutua: ¿puede entrar más de un hilo a la vez en una región protegida?

Todavía no resolveremos estos problemas con synchronized, volatile ni tipos atómicos. Primero aprenderemos a observar hilos y después elegiremos mecanismos según sus garantías.

Práctica: órdenes posibles​

Hilo A: leer → validar → guardar
Hilo B: preparar → enviar

Clasifica estas secuencias:

  1. leer, validar, guardar, preparar, enviar
  2. preparar, leer, enviar, validar, guardar
  3. validar, leer, preparar, guardar, enviar
  4. leer, preparar, validar, enviar, guardar

Solución razonada​

Las secuencias 1, 2 y 4 son posibles porque respetan el orden interno de cada hilo. La 3 es imposible porque coloca validar antes de leer. No podemos elegir entre las posibles sin coordinación adicional y tampoco sabemos si habrá paralelismo real.

Mini reto​

¿Por qué es incorrecto decir «la variable es local, por tanto ningún otro hilo puede modificar sus datos»?

Solución​

La variable local pertenece al stack del hilo, pero puede guardar una referencia a un objeto también referenciado desde otros hilos. Hay que estudiar la accesibilidad del objeto y la coordinación aplicada a su estado.

Errores habituales​

  • Confundir concurrencia con paralelismo.
  • Deducir garantías de una salida repetida.
  • Pensar que todo objeto alcanzable desde una variable local es privado.
  • Crear un hilo por trabajo sin considerar su coste.
  • Suponer que más hilos significa automáticamente más rendimiento.

En la siguiente unidad utilizaremos Thread y Runnable para convertir este modelo en experimentos ejecutables.