¿Por qué podría necesitar un ExecutorService para 1 subproceso?

Puede usar el método Executors.newSingleThreadExecutor para crear un ExecutorService con un grupo que incluya un único subproceso. La lógica del pool es la siguiente:

  • El servicio ejecuta solo una tarea a la vez.
  • Si enviamos N tareas para su ejecución, todas las N tareas serán ejecutadas una tras otra por el hilo único.
  • Si se interrumpe el subproceso, se creará un nuevo subproceso para ejecutar las tareas restantes.

Imaginemos una situación en la que nuestro programa requiere la siguiente funcionalidad:

Necesitamos procesar las solicitudes de los usuarios en 30 segundos, pero no más de una solicitud por unidad de tiempo.

Creamos una clase de tarea para procesar una solicitud de usuario:


class Task implements Runnable {
   private final int taskNumber;

   public Task(int taskNumber) {
       this.taskNumber = taskNumber;
   }

   @Override
   public void run() {
       try {
           Thread.sleep(1000);
       } catch (InterruptedException ignored) {
       }
       System.out.printf("Processed request #%d on thread id=%d\\n", taskNumber, Thread.currentThread().getId());
   }
}
    

La clase modela el comportamiento de procesar una solicitud entrante y muestra su número.

Luego, en el método principal , creamos un ExecutorService para 1 subproceso, que usaremos para procesar secuencialmente las solicitudes entrantes. Dado que las condiciones de la tarea estipulan "dentro de 30 segundos", agregamos una espera de 30 segundos y luego detenemos por la fuerza el ExecutorService .


public static void main(String[] args) throws InterruptedException {
   ExecutorService executorService = Executors.newSingleThreadExecutor();

   for (int i = 0; i < 1_000; i++) {
       executorService.execute(new Task(i));
   }
   executorService.awaitTermination(30, TimeUnit.SECONDS);
   executorService.shutdownNow();
}
    

Después de iniciar el programa, la consola muestra mensajes sobre el procesamiento de solicitudes:

Solicitud procesada #0 en hilo id=16
Solicitud procesada #1 en hilo id=16
Solicitud procesada #2 en hilo id=16

Solicitud procesada #29 en hilo id=16

Después de procesar las solicitudes durante 30 segundos, executorService llama al método shutdownNow() , que detiene la tarea actual (la que se está ejecutando) y cancela todas las tareas pendientes. Después de eso, el programa finaliza con éxito.

Pero no siempre todo es tan perfecto, porque nuestro programa fácilmente podría tener una situación en la que una de las tareas recogidas por el único subproceso de nuestro grupo funcione incorrectamente e incluso termine nuestro subproceso. Podemos simular esta situación para descubrir cómo funciona executorService con un solo hilo en este caso.

Para hacer esto, mientras se ejecuta una de las tareas, terminamos nuestro hilo usando el inseguro y obsoleto método Thread.currentThread().stop() . Estamos haciendo esto intencionalmente para simular la situación en la que una de las tareas finaliza el hilo.

Cambiaremos el método de ejecución en la clase Task :


@Override
public void run() {
   try {
       Thread.sleep(1000);
   } catch (InterruptedException ignored) {
   }

   if (taskNumber == 5) {
       Thread.currentThread().stop();
   }

   System.out.printf("Processed request #%d on thread id=%d\\n", taskNumber, Thread.currentThread().getId());
}
    

Interrumpiremos la tarea #5.

Veamos cómo se ve la salida con el hilo interrumpido al final de la tarea #5:

Solicitud procesada #0 en hilo id=16
Solicitud procesada #1 en hilo id=16
Solicitud procesada #2 en hilo id=16
Solicitud procesada #3 en hilo id=16
Solicitud procesada #4 en hilo id=16
Solicitud procesada #6 en ID de hilo = 17
Solicitud procesada # 7 en ID de hilo = 17

Solicitud procesada # 29 en ID de hilo = 17

Vemos que después de que se interrumpe el hilo al final de la tarea 5, las tareas comienzan a ejecutarse en un hilo cuyo identificador es 17, aunque previamente se habían ejecutado en el hilo cuyo identificador es 16. Y debido a que nuestro pool tiene un hilo único, esto solo puede significar una cosa: executorService reemplazó el hilo detenido por uno nuevo y continuó ejecutando las tareas.

Por lo tanto, debemos usar newSingleThreadExecutor con un grupo de un solo subproceso cuando queremos procesar tareas secuencialmente y solo una a la vez, y queremos continuar procesando tareas de la cola independientemente de la finalización de la tarea anterior (por ejemplo, el caso en que uno de nuestras tareas mata el hilo).

ThreadFactory

Cuando hablamos de crear y recrear hilos, no podemos dejar de mencionar ThreadFactory.

A ThreadFactory es un objeto que crea nuevos hilos a pedido.

Podemos crear nuestra propia fábrica de creación de subprocesos y pasar una instancia de la misma al método Executors.newSingleThreadExecutor(ThreadFactory threadFactory) .


ExecutorService executorService = Executors.newSingleThreadExecutor(new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                return new Thread(r, "MyThread");
            }
        });
                    
Anulamos el método para crear un nuevo hilo, pasando un nombre de hilo al constructor.

ExecutorService executorService = Executors.newSingleThreadExecutor(new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                Thread thread = new Thread(r, "MyThread");
                thread.setPriority(Thread.MAX_PRIORITY);
                return thread;
            }
        });
                    
Cambiamos el nombre y la prioridad del hilo creado.

Entonces vemos que tenemos 2 métodos Executors.newSingleThreadExecutor sobrecargados. Uno sin parámetros y otro con un parámetro ThreadFactory .

Con ThreadFactory , puede configurar los subprocesos creados según sea necesario, por ejemplo, estableciendo prioridades, usando subclases de subprocesos, agregando un UncaughtExceptionHandler al subproceso, etc.