Tema2: Creación y control de Threads en Java

Se puede tener de dos formas distintas, o extendiendo de la clase Thread o implementando la interfaz Runnable.

Razones para usar hilos: optimiza el uso de la CPU, se modelan mejor determinados problemas, el problema no admite otra solución razonable.

El método join() o condición de espera, hace que la ejecución espere para continuar hasta que todos los hilos hayan hecho sus tareas.

Las variables que sean static permiten compartir memoria entre los objetos de la clase.

Si hacemos uso de la implementación de la clase Runnable, cualquier clase que lo implemente expresa ejecución concurrente. Y estos objetos se pueden pasar como parámetros al constructor de la clase Thread.

Es posible crear concurrencia mediante expresiones lambda de la siguiente forma:

Runnable r = () -> {};
Thread th = new Thread(r);
th.start();

thread-lifecicle

API de control de thread:

  • t1.start() → lanza el hilo t1
  • t1.sleep(int t) → suspende a t1 durante t milisegundos
  • t1.yield() → pasa a t1 de ejecución a listo
  • t1.join() → suspende t0 y espera que termine t1

Control de prioridad

La prioridad del hilo hijo será igual a la del hilo padre.

Tiene sentido exclusivamente en el ámbito de la JVM, aunque se mapea a los hilos del sistema.

La clase Thread tiene un esquema de 10 niveles de prioridad.

  • Ver la prioridad: public int getPriority()
  • Establecer la prioridad: public void setPriority(int p)

El valor de la prioridad en la JVM indica al planificador del SO qué hilos van primero. Aunque NO es un contrato absoluto, ya que depende de:

  • La implementación de la JVM
  • El SO
  • El mapping de prioridad JVM-prioridad SO

Mapping de hilos

Prioridad Java:

Thread.MIN_PRIORITY; // 1
Thread.NORM_PRIORITY; // 5
Thread.MAX_PRIORITY; // 10

JVM-hilos nativos de Win32

El SO conoce el número de hilos que usa la JVM. Se aplican uno a uno. El secuencionamiento de hilos Java está sujeto al del SO. Se aplican 10 prioridades en la JVM sobre 7 en el SO + 5 prioridades de secuenciamiento.

Thread.MIN_PRIORITY; // THREAD.PRIORITY_LOWEST;
Thread.NORM_PRIORITY; // THREAD.PRIORITY_NORMAL;
Thread.MAX_PRIORITY; // THREAD.PRIORITY_HIGHEST;

JVM-hilos nativos de Linux

Núcleos recientes implementan Native Posix Thread Library. Aplican hilos JVM a hilos del núcleo uno-a-uno bajo el modelo de Solaris. La prioridad Java es un factor muy pequeño en el cálculo global del secuenciamiento.

Thread.MIN_PRIORITY; // 4
Thread.NORM_PRIORITY; // 0
Thread.MAX_PRIORITY; // -5

Conclusiones del control de prioridades

La actual especificación de la JVM no establece un modelo de planificación por prioridades válido. El comportamiento puede y debe variar en diferentes máquinas.

En secuencias de tareas estrictas, no es posible planificar con prioridades. Aunque si con co-rutinas, a nivel básico.

Ejecutores y Pool de Threads

Crear y destruir hilos tiene latencias, esta creación y control son responsabilidad del programador.

Hay determinadas aplicaciones que crean hilos de forma masiva, para las cuales el modelo de threads es ineficiente.

Pool de Threads

Hacen una reserva de hilos a la espera de recibir tareas para ejecutarlas. Solo se crea una vez de manaera que se reduce la latencia y reutiliza los hilos una y otra vez.

Efectúa de forma automática la gestión del ciclo de via de las tareas, recibiendo las tareas a ejecutar mediante objetos Runnable.

ExecutorService singleExecutor = Executors.newSingleThreadExecutor();

ExecutorService fixedExecutor = Executors.newFixedThreadPool(500);

for(...)
    executor.execute(new Thread());

executer.shutdown();
while(!executor.isTerminated());

También tenemos ejecutores que son altamente configurables, que implementan a ExecutorService.

public ThreadPoolExecutor(
    int corePoolSize, int maximumPoolSize, long keepAliveTime,
    TimeUnit unit, BlockingQueue<Runnable> workQueue
);

// Example
ThreadPoolExecutor miPool = new ThreadPoolExecutor(
    tamPool, tamPool, 60000L, TimeUnit.MILLISECONDS,
    new LinkedBlockingQueue<Runnable>()
);

Tarea[] tareas = new Tarea[nTareas];

for(int i=0; i < nTareas; i++){
    tareas[i] = new Tarea(i);
    miPool.execute(tareas[i]);
}

miPool.shutdown();

Ejecución asíncrona a futuro

Interfaz Callable<V>

Modela a tareas que admiten ejecución concurrente. Es similar al Runnable, pero no igual. Los objetos que la implementan retorna un resultado de clase V.

Concurrencia soportada por el método V call() que encapsula el código a ejecutar concurrentement, habitualmente procesado a través de un ejecutor. Si el resultado que debe retornar no puede ser computado, lanza una excepción.

Interfaz Future<V>

Representa el resultado de una computación asíncrona, tales computación y resultado son provistos por objetos que implementan la interfaz Callable. Es decir, el resultado de una computación asíncrona modelada mediante un objeto que implementa a Callable<V>, se obtiene "dentro" de un Future<V>. Las clases que implementan a Future disponen de métodos para:

  • Comprobar si la computación a futuro ha sido completada, boolean isDone().
  • Obtener la computación a futuro cuando ha sido completada, V get().

Este último método es sumamente interesante, provee de sincronización implícita por sí mismo, ya que tiene carácter bloqueante, es decir, se espera con bloqueo hasta que la tarea Callable<V> que tiene que computar el cálculo lo hace y retorna el resultado.

// FactorialTask implements Callable<Integer>, método call() que retorna un Integer
FactorialTask task = new FactorialTask(5);
ExecutorService exec = Executors.newFixedThreadPool(1);
Future <Integer > future = exec.submit(task);
exec.shutdown();
System.out.println(future.get().intValue());

Se puede hacer uso de expresiones lambda para el método V call(). Similar al Runnable, pero retornando un resultado de la clase.

Callable <Integer> computation = () → {
    int fact = 1;
    for(int count = number; count > 1;count--)
        fact = fact * count;
    return (new Integer(fact));
};