O método newFixedThreadPool da classe Executors cria um executorService com um número fixo de threads. Ao contrário do método newSingleThreadExecutor , especificamos quantos threads queremos no pool. Sob o capô, o seguinte código é chamado:


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

Os parâmetros corePoolSize (o número de threads que estarão prontos (iniciados) quando o serviço executor for iniciado) e maximumPoolSize (o número máximo de threads que o serviço executor pode criar) recebem o mesmo valor — o número de threads passados ​​para newFixedThreadPool(nThreads ) . E podemos passar nossa própria implementação de ThreadFactory exatamente da mesma maneira.

Bem, vamos ver por que precisamos de tal ExecutorService .

Aqui está a lógica de um ExecutorService com um número fixo (n) de threads:

  • Um máximo de n threads estarão ativos para tarefas de processamento.
  • Se mais de n tarefas forem enviadas, elas serão mantidas na fila até que os encadeamentos sejam liberados.
  • Se uma das threads falhar e terminar, uma nova thread será criada para substituí-la.
  • Qualquer encadeamento no pool fica ativo até que o pool seja encerrado.

Por exemplo, imagine esperar para passar pela segurança no aeroporto. Todos ficam em uma fila até que imediatamente antes da verificação de segurança, os passageiros são distribuídos entre todos os postos de controle de trabalho. Caso haja atraso em um dos postos de controle, a fila será processada apenas pelo segundo até que o primeiro fique livre. E se um ponto de controle fechar totalmente, outro ponto de controle será aberto para substituí-lo e os passageiros continuarão sendo processados ​​por dois pontos de controle.

Notaremos desde já que mesmo que as condições sejam ideais — os n threads prometidos funcionam de forma estável, e os threads que terminam com erro são sempre substituídos (algo que recursos limitados impossibilitam de conseguir em um aeroporto real) — o sistema ainda possui vários funcionalidades desagradáveis, pois em hipótese alguma haverá mais threads, mesmo que a fila cresça mais rápido do que as threads podem processar as tarefas.

Sugiro obter uma compreensão prática de como o ExecutorService funciona com um número fixo de threads. Vamos criar uma classe que implemente Runnable . Os objetos desta classe representam nossas tarefas para o 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());
    }
}
    

No método run() , bloqueamos a thread por 2 segundos, simulando alguma carga de trabalho, e então exibimos o número da tarefa atual e o nome da thread que está executando a tarefa.


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

Para começar, no método main , criamos um ExecutorService e enviamos 30 tarefas para execução.

Pedido do usuário processado nº 1 no thread pool-1-thread-2
Pedido do usuário processado nº 0 no thread pool-1-thread-1
Pedido do usuário processado nº 2 no thread pool-1-thread-3
Pedido do usuário processado nº 5 no pool- 1-thread-3 thread
Pedido do usuário processado nº 3 no thread pool-1-thread-2
Pedido do usuário processado nº 4 no thread pool-1-thread-1
Pedido do usuário processado nº 8 no thread pool-1-thread-1
Usuário processado solicitação nº 6 no thread pool-1-thread-3
Solicitação de usuário processada nº 7 no thread pool-1-thread-2
Solicitação de usuário processada nº 10 no thread pool-1-thread-3
Solicitação de usuário processada nº 9 no pool-1- thread-1 thread
Solicitação de usuário processada nº 11 no thread pool-1-thread-2
Solicitação de usuário processada nº 12 no thread pool-1-thread-3
Solicitação de usuário processada nº 14 na thread pool-1-thread-2
Solicitação de usuário processada nº 13 na thread pool-1-thread-1
Solicitação de usuário processada nº 15 na thread pool-1-thread-3 Solicitação
de usuário processada nº 16 em pool- 1-thread-2 thread
Solicitação de usuário processada nº 17 no thread pool-1-thread-1
Solicitação de usuário processada nº 18 no thread pool-1-thread-3
Solicitação de usuário processada nº 19 no thread pool-1-thread-2
Usuário processado solicitação nº 20 no thread pool-1-thread-1
Solicitação de usuário processada nº 21 no thread pool-1-thread-3
Solicitação de usuário processada nº 22 no thread pool-1-thread-2
Solicitação de usuário processada nº 23 no pool-1- thread-1 thread
Pedido de usuário processado nº 25 no thread pool-1-thread-2
Pedido de usuário processado nº 24 no thread pool-1-thread-3
Solicitação de usuário processada nº 26 na thread pool-1-thread-1
Solicitação de usuário processada nº 27 na thread pool-1-thread-2
Solicitação de usuário processada nº 28 na thread pool-1-thread-3 Solicitação
de usuário processada nº 29 em pool- 1-thread-1 thread

A saída do console nos mostra como as tarefas são executadas em diferentes threads assim que são liberadas pela tarefa anterior.

Agora vamos aumentar o número de tarefas para 100 e, após enviar 100 tarefas, chamaremos o método awaitTermination(11, SECONDS) . Passamos um número e uma unidade de tempo como argumentos. Este método bloqueará o thread principal por 11 segundos. Em seguida, chamaremos shutdownNow() para forçar o ExecutorService a desligar sem esperar que todas as tarefas sejam concluídas.


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);
    

Ao final, exibiremos informações sobre o estado do executorService .

Aqui está a saída do console que obtemos:

Solicitação de usuário processada nº 0 na thread pool-1-thread-1
Solicitação de usuário processada nº 2 na thread pool-1-thread-3
Solicitação de usuário processada nº 1 na thread pool-1-thread-2
Solicitação de usuário processada nº 4 em pool- 1-thread-3 thread
Solicitação de usuário processada nº 5 no thread pool-1-thread-2
Solicitação de usuário processada nº 3 no thread pool-1-thread-1
Solicitação de usuário processada nº 6 no thread pool-1-thread-3
Usuário processado solicitação nº 7 no thread pool-1-thread-2
Solicitação de usuário processada nº 8 no thread pool-1-thread-1
Solicitação de usuário processada nº 9 no thread pool-1-thread-3
Solicitação de usuário processada nº 11 no pool-1- thread-1 thread
Solicitação de usuário processada nº 10 no thread pool-1-thread-2
Solicitação de usuário processada nº 13 no thread pool-1-thread-1
Solicitação de usuário processada nº 14 no thread pool-1-thread-2
Solicitação de usuário processada nº 12 no thread pool-1-thread-3
java.util.concurrent.ThreadPoolExecutor@452b3a41[Desligando, tamanho do pool = 3, threads ativos = 3 , tarefas enfileiradas = 0, tarefas concluídas = 15]
Solicitação de usuário processada nº 17 no thread pool-1-thread-3
Solicitação de usuário processada nº 15 no thread pool-1-thread-1
Solicitação de usuário processada nº 16 no thread pool-1 -2 fios

Isso é seguido por 3 InterruptedExceptions , lançadas por métodos de suspensão de 3 tarefas ativas.

Podemos ver que quando o programa termina, 15 tarefas foram concluídas, mas o pool ainda tinha 3 threads ativas que não terminaram de executar suas tarefas. O método interrupt() é chamado nesses três threads, o que significa que a tarefa será concluída, mas, em nosso caso, o método sleep lança uma InterruptedException . Também vemos que depois que o método shutdownNow() é chamado, a fila de tarefas é limpa.

Portanto, ao usar um ExecutorService com um número fixo de threads no pool, lembre-se de como ele funciona. Este tipo é adequado para tarefas com uma carga constante conhecida.

Aqui está outra pergunta interessante: se você precisar usar um executor para um único thread, qual método você deve chamar? newSingleThreadExecutor() ou newFixedThreadPool(1) ?

Ambos os executores terão comportamento equivalente. A única diferença é que o método newSingleThreadExecutor() retornará um executor que não pode ser reconfigurado posteriormente para usar threads adicionais.