El método newFixedThreadPool de la clase Executors crea un executorService con un número fijo de subprocesos. A diferencia del método newSingleThreadExecutor , especificamos cuántos subprocesos queremos en el grupo. Debajo del capó, se llama el siguiente código:


new ThreadPoolExecutor(nThreads, nThreads,
                                      	0L, TimeUnit.MILLISECONDS,
                                      	new LinkedBlockingQueue());

Los parámetros corePoolSize (la cantidad de subprocesos que estarán listos (iniciados) cuando se inicie el servicio ejecutor ) y maximumPoolSize (la cantidad máxima de subprocesos que puede crear el servicio ejecutor ) reciben el mismo valor: la cantidad de subprocesos pasados ​​a newFixedThreadPool(nThreads ) . Y podemos pasar nuestra propia implementación de ThreadFactory exactamente de la misma manera.

Bueno, veamos por qué necesitamos un ExecutorService de este tipo .

Aquí está la lógica de un ExecutorService con un número fijo (n) de subprocesos:

  • Un máximo de n subprocesos estarán activos para las tareas de procesamiento.
  • Si se envían más de n tareas, se mantendrán en la cola hasta que se liberen subprocesos.
  • Si uno de los subprocesos falla y finaliza, se creará un nuevo subproceso para ocupar su lugar.
  • Cualquier subproceso en el grupo está activo hasta que se cierra el grupo.

Como ejemplo, imagina esperar para pasar por seguridad en el aeropuerto. Todos se paran en una línea hasta que inmediatamente antes del control de seguridad, los pasajeros se distribuyen entre todos los puntos de control en funcionamiento. Si hay un retraso en uno de los puntos de control, la cola será procesada solo por el segundo hasta que el primero esté libre. Y si un punto de control se cierra por completo, se abrirá otro punto de control para reemplazarlo, y los pasajeros seguirán siendo procesados ​​a través de dos puntos de control.

Notaremos de inmediato que incluso si las condiciones son ideales (los n subprocesos prometidos funcionan de manera estable y los subprocesos que terminan con un error siempre se reemplazan (algo que los recursos limitados hacen imposible de lograr en un aeropuerto real)) el sistema todavía tiene varios características desagradables, porque bajo ninguna circunstancia habrá más subprocesos, incluso si la cola crece más rápido de lo que los subprocesos pueden procesar tareas.

Sugiero obtener una comprensión práctica de cómo funciona ExecutorService con un número fijo de subprocesos. Vamos a crear una clase que implemente Runnable . Los objetos de esta clase representan nuestras tareas para ExecutorService .


public class Task implements Runnable {
    int taskNumber;
 
    public Task(int taskNumber) {
        this.taskNumber = taskNumber;
    }
 
    @Override
    public void run() {
try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Processed user request #" + taskNumber + " on thread " + Thread.currentThread().getName());
    }
}
    

En el método run() , bloqueamos el subproceso durante 2 segundos, simulando una carga de trabajo y luego mostramos el número de la tarea actual y el nombre del subproceso que ejecuta la tarea.


ExecutorService executorService = Executors.newFixedThreadPool(3);
 
        for (int i = 0; i < 30; i++) {
            executorService.execute(new Task(i));
        }
        
        executorService.shutdown();
    

Para empezar, en el método principal , creamos un ExecutorService y enviamos 30 tareas para su ejecución.

Solicitud de usuario procesada n.º 1 en grupo-1-hilo-2
solicitud de usuario procesada n.º 0 en grupo-1-hilo-1
solicitud de usuario procesada n.º 2 en grupo-1-hilo-3 solicitud
de usuario procesada n.º 5 en grupo- 1-subproceso-3 subproceso
Solicitud de usuario procesada n.° 3 en el subproceso pool-1-thread-2
Solicitud de usuario procesada n.° 4 en el subproceso pool-1-thread-1
Solicitud de usuario procesada n.° 8 en el subproceso pool-1-thread-1
Usuario procesado solicitud n.º 6 en grupo-1-hilo-3
solicitud de usuario procesada n.º 7 en grupo-1-hilo-2
solicitud de usuario procesada n.º 10 en grupo-1-hilo-3
solicitud de usuario procesada n.º 9 en grupo-1- subproceso-1 subproceso
Solicitud de usuario procesada n.° 11 en el subproceso pool-1-thread-2 Solicitud de
usuario procesada n.° 12 en el subproceso pool-1-thread-3
Solicitud de usuario procesada n.º 14 en grupo-1-hilo-2
solicitud de usuario procesada n.º 13 en grupo-1-hilo-1
solicitud de usuario procesada n.º 15 en grupo-1-hilo-3 solicitud
de usuario procesada n.º 16 en grupo- 1-hilo-2 hilo
Solicitud de usuario procesada n.° 17 en grupo-1-hilo-1 hilo
Solicitud de usuario procesada n.° 18 en grupo-1-hilo-3
solicitud de usuario procesada n.° 19 en grupo-1-hilo-2 usuario
procesado solicitud n.° 20 en grupo-1-hilo-1 proceso
Solicitud de usuario procesada n.° 21 en grupo-1-proceso-3
solicitud de usuario procesada n.° 22 en grupo-1-proceso-2 solicitud
de usuario procesada n.° 23 en grupo-1- subproceso-1 subproceso
Solicitud de usuario procesada #25 en subproceso pool-1-thread-2
Solicitud de usuario procesada #24 en subproceso pool-1-thread-3
Solicitud de usuario procesada n.° 26 en grupo-1-subproceso-1
solicitud de usuario procesada n.° 27 en grupo-1-proceso-2
solicitud de usuario procesada n.° 28 en grupo-1-proceso-3 solicitud
de usuario procesada n.° 29 en grupo- 1-hilo-1 hilo

La salida de la consola nos muestra cómo se ejecutan las tareas en diferentes subprocesos una vez que la tarea anterior las libera.

Ahora aumentaremos la cantidad de tareas a 100 y, después de enviar 100 tareas, llamaremos al método awaitTermination(11, SECONDS) . Pasamos un número y una unidad de tiempo como argumentos. Este método bloqueará el hilo principal durante 11 segundos. Luego llamaremos a shutdownNow() para forzar el cierre de ExecutorService sin esperar a que se completen todas las tareas.


ExecutorService executorService = Executors.newFixedThreadPool(3);
 
        for (int i = 0; i < 100; i++) {
            executorService.execute(new Task(i));
        }
 
        executorService.awaitTermination(11, SECONDS);
 
        executorService.shutdownNow();
        System.out.println(executorService);
    

Al final, mostraremos información sobre el estado del executorService .

Aquí está la salida de la consola que obtenemos:

Solicitud de usuario procesada n.° 0 en grupo-1-subproceso-1
Solicitud de usuario procesada n.° 2 en grupo-1-subproceso-3
Solicitud de usuario procesada n.° 1 en grupo-1-subproceso-2
Solicitud de usuario procesada n.° 4 en grupo- 1-subproceso-3 subproceso
Solicitud de usuario procesada n.° 5 en el subproceso pool-1-thread-2
Solicitud de usuario procesada n.° 3 en el subproceso pool-1-thread-1
Solicitud de usuario procesada n.° 6 en el subproceso pool-1-thread-3
Usuario procesado solicitud n.º 7 en grupo-1-hilo-2
solicitud de usuario procesada n.º 8 en grupo-1-hilo-1
solicitud de usuario procesada n.º 9 en grupo-1-hilo-3
solicitud de usuario procesada n.º 11 en grupo-1- subproceso-1 subproceso
Solicitud de usuario procesada n.º 10 en el subproceso pool-1-thread-2
Solicitud de usuario procesada n.º 13 en el subproceso pool-1-thread-1
Solicitud de usuario procesada n.º 14 en el subproceso pool-1-thread-2
Solicitud de usuario procesada n.º 12 en el subproceso pool-1-thread-3
java.util.concurrent.ThreadPoolExecutor@452b3a41[Apagando, tamaño del grupo = 3, subprocesos activos = 3 , tareas en cola = 0, tareas completadas = 15]
Solicitud de usuario procesada n.º 17 en grupo-1-hilo-3
solicitud de usuario procesada n.º 15 en grupo-1-hilo-1
solicitud de usuario procesada n.º 16 en grupo-1-hilo -2 hilos

Esto es seguido por 3 InterruptedExceptions , lanzadas por métodos de suspensión de 3 tareas activas.

Podemos ver que cuando finaliza el programa, se realizan 15 tareas, pero el grupo aún tenía 3 subprocesos activos que no terminaron de ejecutar sus tareas. Se llama al método interrupt() en estos tres subprocesos, lo que significa que la tarea se completará, pero en nuestro caso, el método sleep genera una InterruptedException . También vemos que después de llamar al método shutdownNow() , la cola de tareas se borra.

Entonces, cuando use un ExecutorService con un número fijo de subprocesos en el grupo, asegúrese de recordar cómo funciona. Este tipo es adecuado para tareas con una carga constante conocida.

Aquí hay otra pregunta interesante: si necesita usar un ejecutor para un solo hilo, ¿a qué método debe llamar? newSingleThreadExecutor() o newFixedThreadPool(1) ?

Ambos ejecutores tendrán un comportamiento equivalente. La única diferencia es que el método newSingleThreadExecutor() devolverá un ejecutor que no se puede reconfigurar más tarde para usar subprocesos adicionales.