Conceptos de Procesos e Hilos
La mayoría de los sistemas operativos soportan el modelo de procesos. Una tarea generalmente se asocia a un proceso. Las características principales de un proceso incluyen:
- Ser independientes con recursos dedicados.
- Poseer un ciclo de vida y estados específicos.
- Permitir la ejecución concurrente en un procesador único sin interferencias mutuas.
Los hilos representan la unidad más pequeña de ejecución para la CPU. Cada proceso puede contener varios hilos, y todo hilo debe pertenecer a un proceso. Los hilos tienen su propia pila de ejecución, pero no controlan recursos del sistema. La CPU procesa un hilo a la vez; la ilusión de paralelismo surge del rápido cambio de contexto. La gestión y programación de los hilos recae en el proceso padre.
Estados del Ciclo de Vida de Hilos en Java
Los hilos en Java atraviesan cinco estados fundamentales:
- Nuevo (New): Se crea el objeto de hilo, como Thread h = new Tarea();
- Listo (Runnable): Al invocar el método start() del hilo, por ejemplo h.start();, el hilo queda preparado para ejecución, esperando asignación de la CPU.
- En Ejecución (Running): Cuando la CPU selecciona un hilo listo, este comienza su ejecución. Solo los hilos en estado listo pueden transicionar a este estado.
- Bloqueado (Blocked): Un hilo en ejecución puede suspenderse temporalmente y perder el acceso a la CPU. Las causas incluyen:
- Bloqueo por espera: Al ejecutar el método wait().
- Bloqueo por sincronización: Al no obtener un bloqueo synchronized porque otro hilo lo retiene.
- Otros bloqueos: Originados por sleep(), join() o solicitudes de E/S. El hilo regresa a estado listo al completar la operación.
- Terminado (Dead): El hilo finaliza su método run() normalmente o por una excepción.
Nota: Los métodos wait(), notify() y notifyAll() pertenecen a la clase Object.
Métodos para Crear y Ejecutar Hilos en Java
1. Extendiendo la Clase Thread
Definir una subclase de Thread y sobrescribir el método run() con la lógica del hilo. Crear instancias de esta clase y ejecutar start().
public class TrabajoA extends Thread {
@Override
public void run() {
for (int iteracion = 0; iteracion < 1000; iteracion++) {
System.out.println(getName() + " paso: " + iteracion);
}
}
public static void main(String[] args) {
new TrabajoA().start();
new TrabajoA().start();
}
}
2. Implementando la Interfaz Runnable
Crear una clase que implemente Runnable, definir run(), y pasar instancias de esta clase al constructor de Thread.
public class TrabajoB implements Runnable {
@Override
public void run() {
for (int indice = 0; indice < 1000; indice++) {
System.out.println(Thread.currentThread().getName() + " valor: " + indice);
}
}
public static void main(String[] args) {
TrabajoB tarea = new TrabajoB();
new Thread(tarea).start();
new Thread(tarea).start();
}
}
3. Utilizando Callable y Future
Implementar Callable con un tipo de retorno, encapsularlo en FutureTask, y luego iniciar un hilo. El valor de retorno se obtiene mediante get().
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class TrabajoC implements Callable<integer> {
@Override
public Integer call() throws Exception {
int acumulado = 0;
for (; acumulado < 1000; acumulado++) {
System.out.println(Thread.currentThread().getName() + " conteo: " + acumulado);
}
return acumulado;
}
public static void main(String[] args) {
TrabajoC callable = new TrabajoC();
FutureTask<integer> future = new FutureTask<>(callable);
new Thread(future).start();
try {
Thread.sleep(5000);
System.out.println("Resultado: " + future.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
}</integer></integer>
Aunque extender Thread es directo, la herencia única en Java limita su extensibilidad, por lo que se recomienda implementar Runnable o Callable para mayor flexibilidad. Siempre se deben iniciar hilos con start(); invocar run() directamente no crea un nuevo hilo.