Executors.newCachedThreadPool () versus Executors.newFixedThreadPool ()

Respostas:

202

Acho que os documentos explicam muito bem a diferença e o uso dessas duas funções:

newFixedThreadPool

Cria um conjunto de encadeamentos que reutiliza um número fixo de encadeamentos que operam em uma fila ilimitada compartilhada. A qualquer momento, no máximo os nThreads threads serão tarefas de processamento ativas. Se tarefas adicionais forem enviadas quando todos os encadeamentos estiverem ativos, eles aguardarão na fila até que um encadeamento esteja disponível. Se algum encadeamento terminar devido a uma falha durante a execução anterior ao encerramento, um novo substituirá, se necessário, para executar tarefas subseqüentes. Os encadeamentos no pool existirão até que seja explicitamente encerrado.

newCachedThreadPool

Cria um conjunto de encadeamentos que cria novos encadeamentos, conforme necessário, mas reutiliza encadeamentos construídos anteriormente quando estiverem disponíveis. Esses pools geralmente melhoram o desempenho dos programas que executam muitas tarefas assíncronas de curta duração. As chamadas a serem executadas reutilizarão os threads construídos anteriormente, se disponíveis. Se nenhum encadeamento existente estiver disponível, um novo encadeamento será criado e adicionado ao pool. Os segmentos que não foram usados ​​por sessenta segundos são encerrados e removidos do cache. Portanto, um pool que permanecer ocioso por tempo suficiente não consumirá nenhum recurso. Observe que conjuntos com propriedades semelhantes, mas detalhes diferentes (por exemplo, parâmetros de tempo limite) podem ser criados usando os construtores ThreadPoolExecutor.

Em termos de recursos, o newFixedThreadPoolmanterá todos os threads em execução até que sejam explicitamente finalizados. Nos newCachedThreadPoolThreads que não foram usados ​​por sessenta segundos, são encerrados e removidos do cache.

Diante disso, o consumo de recursos dependerá muito da situação. Por exemplo, se você tiver um grande número de tarefas de execução longa, sugiro o FixedThreadPool. Quanto CachedThreadPoolaos documentos, os documentos dizem que "esses pools geralmente melhoram o desempenho de programas que executam muitas tarefas assíncronas de curta duração".

bruno conde
fonte
1
sim, eu passei pela documentação .. o problema é ... fixedThreadPool está causando um erro de falta de memória @ 3 threads .. onde o cachedPool está internamente criando apenas um thread único .. ao aumentar o tamanho da pilha, estou obtendo o mesmo desempenho para ambos ... existe mais alguma coisa que estou perdendo !!
hakish 4/09/09
1
Você está fornecendo algum Threadfactory para o ThreadPool? Meu palpite é que pode estar armazenando algum estado nos threads que não estão sendo coletados de lixo. Caso contrário, talvez o seu programa esteja sendo executado tão perto do tamanho limite da pilha que, com a criação de 3 threads, causa um OutOfMemory. Além disso, se o cachedPool estiver criando internamente apenas um único encadeamento, isso poderá indicar que suas tarefas estão em execução sincronizadas.
bruno conde
@brunoconde Assim como @Louis F. indica, isso newCachedThreadPoolpode causar alguns problemas sérios , porque você deixa todo o controle para thread poole quando o serviço está trabalhando com outras pessoas no mesmo host , o que pode causar a falha de outras devido à espera de CPU por muito tempo. Então, acho que newFixedThreadPoolpode ser mais seguro nesse tipo de cenário. Além disso, este post esclarece as diferenças mais marcantes entre eles.
Hearen
75

Apenas para completar as outras respostas, gostaria de citar Effective Java, 2nd Edition, de Joshua Bloch, capítulo 10, Item 68:

"A escolha do serviço executor para um aplicativo específico pode ser complicada. Se você estiver escrevendo um programa pequeno ou um servidor com pouca carga , usando Executors.new- CachedThreadPool geralmente é uma boa opção , pois não requer configuração e geralmente" faz o coisa certa." Mas um conjunto de encadeamentos em cache não é uma boa opção para um servidor de produção muito carregado !

Em um conjunto de encadeamentos em cache , as tarefas enviadas não são enfileiradas, mas são imediatamente entregues a um encadeamento para execução. Se nenhum encadeamento estiver disponível, um novo será criado . Se um servidor estiver tão carregado que todas as suas CPUs forem totalmente utilizadas e mais tarefas chegarem, mais threads serão criados, o que só tornará as coisas piores.

Portanto, em um servidor de produção muito carregado , é muito melhor usar Executors.newFixedThreadPool , que fornece um pool com um número fixo de threads ou usar a classe ThreadPoolExecutor diretamente, para obter o máximo controle. "

Louis F.
fonte
15

Se você olhar o código-fonte , verá que eles estão chamando ThreadPoolExecutor. internamente e definindo suas propriedades. Você pode criar o seu para ter um melhor controle de seus requisitos.

public static ExecutorService newFixedThreadPool(int nThreads) {
   return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}

public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
}
krmanish007
fonte
1
Exatamente, um executor de encadeamento em cache com um limite superior sensato e, digamos, 5 a 10 minutos de colheita ociosa é perfeito para a maioria das ocasiões.
Agoston Horvath 8/16
12

Se você não estiver preocupado com uma fila ilimitada de tarefas Chamadas / Executáveis , poderá usar uma delas. Como sugerido por bruno, eu também prefiro newFixedThreadPoolque newCachedThreadPoolao longo destes dois.

Mas ThreadPoolExecutor fornece recursos mais flexíveis em relação a um ou outro newFixedThreadPoolounewCachedThreadPool

ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, 
TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory,
RejectedExecutionHandler handler)

Vantagens:

  1. Você tem controle total do tamanho do BlockingQueue . Não é ilimitado, ao contrário das duas opções anteriores. Não receberei um erro de falta de memória devido a um grande acúmulo de tarefas pendentes de chamar / executar quando houver turbulência inesperada no sistema.

  2. Você pode implementar uma política de tratamento de rejeição personalizada OU usar uma das políticas:

    1. Por padrão ThreadPoolExecutor.AbortPolicy, o manipulador lança um RejectedExecutionException em tempo de execução após a rejeição.

    2. Em ThreadPoolExecutor.CallerRunsPolicy, o encadeamento que chama a execução executa a tarefa. Isso fornece um mecanismo simples de controle de feedback que diminuirá a taxa de envio de novas tarefas.

    3. Em ThreadPoolExecutor.DiscardPolicy, uma tarefa que não pode ser executada é simplesmente descartada.

    4. Em ThreadPoolExecutor.DiscardOldestPolicy, se o executor não for desligado, a tarefa na cabeça da fila de trabalho será descartada e a execução será tentada novamente (o que pode falhar novamente, fazendo com que isso se repita).

  3. Você pode implementar uma fábrica de threads personalizada para os casos de uso abaixo:

    1. Para definir um nome de segmento mais descritivo
    2. Para definir o status do daemon de encadeamento
    3. Para definir a prioridade do encadeamento
Ravindra babu
fonte
11

Isso mesmo, Executors.newCachedThreadPool()não é uma ótima opção para código de servidor que atende a vários clientes e solicitações simultâneas.

Por quê? Existem basicamente dois problemas relacionados:

  1. É ilimitado, o que significa que você está abrindo a porta para alguém prejudicar sua JVM simplesmente injetando mais trabalho no serviço (ataque de DoS). Os threads consomem uma quantidade não desprezível de memória e também aumentam o consumo de memória com base no trabalho em andamento; portanto, é muito fácil derrubar um servidor dessa maneira (a menos que você tenha outros disjuntores instalados).

  2. O problema ilimitado é exacerbado pelo fato de o Executor ser liderado por um, o SynchronousQueueque significa que há uma transferência direta entre o responsável pela tarefa e o pool de threads. Cada nova tarefa criará um novo thread se todos os threads existentes estiverem ocupados. Geralmente, essa é uma estratégia ruim para o código do servidor. Quando a CPU fica saturada, as tarefas existentes levam mais tempo para serem concluídas. Ainda mais tarefas estão sendo enviadas e mais threads são criados, portanto, as tarefas demoram mais e mais para serem concluídas. Quando a CPU está saturada, mais threads definitivamente não é o que o servidor precisa.

Aqui estão as minhas recomendações:

Use um conjunto de encadeamentos de tamanho fixo Executors.newFixedThreadPool ou ThreadPoolExecutor. com um número máximo definido de threads;

Prashant Gautam
fonte
6

A ThreadPoolExecutorclasse é a implementação básica para os executores retornados de muitos dos Executorsmétodos de fábrica. Então, vamos abordar os conjuntos de encadeamentos fixos e armazenados em cache da ThreadPoolExecutorperspectiva.

ThreadPoolExecutor

O construtor principal desta classe é semelhante a este:

public ThreadPoolExecutor(
                  int corePoolSize,
                  int maximumPoolSize,
                  long keepAliveTime,
                  TimeUnit unit,
                  BlockingQueue<Runnable> workQueue,
                  ThreadFactory threadFactory,
                  RejectedExecutionHandler handler
)

Tamanho do Pool Principal

O corePoolSizedetermina o tamanho mínimo do conjunto de encadeamentos de destino. A implementação manteria um pool desse tamanho, mesmo que não haja tarefas a serem executadas.

Tamanho máximo da piscina

O maximumPoolSizeé o número máximo de encadeamentos que podem estar ativos ao mesmo tempo.

Depois que o conjunto de encadeamentos cresce e se torna maior que o corePoolSizelimite, o executor pode encerrar encadeamentos ociosos e alcançar o corePoolSizenovo. Se allowCoreThreadTimeOutfor verdade, o executor pode até finalizar os encadeamentos do pool principal se eles estiverem ociosos mais do que o keepAliveTimelimite.

Portanto, o ponto principal é que, se os threads permanecerem ociosos mais do que o keepAliveTimelimite, eles poderão ser encerrados, pois não há demanda por eles.

Filas

O que acontece quando uma nova tarefa entra e todos os threads principais são ocupados? As novas tarefas serão enfileiradas dentro dessa BlockingQueue<Runnable>instância. Quando um encadeamento fica livre, uma dessas tarefas na fila pode ser processada.

Existem implementações diferentes da BlockingQueueinterface em Java, para que possamos implementar abordagens de enfileiramento diferentes, como:

  1. Fila limitada : Novas tarefas seriam colocadas em fila dentro de uma fila de tarefas limitada.

  2. Fila não vinculada : Novas tarefas seriam colocadas em fila dentro de uma fila de tarefas ilimitada. Portanto, essa fila pode crescer tanto quanto o tamanho da pilha permitir.

  3. Entrega Síncrona : Também podemos usar o SynchronousQueuepara enfileirar as novas tarefas. Nesse caso, ao enfileirar uma nova tarefa, outro encadeamento já deve estar esperando por essa tarefa.

Submissão do Trabalho

Aqui está como ele ThreadPoolExecutorexecuta uma nova tarefa:

  1. Se menos de corePoolSizethreads estiverem em execução, tentará iniciar um novo thread com a tarefa especificada como seu primeiro trabalho.
  2. Caso contrário, ele tenta enfileirar a nova tarefa usando o BlockingQueue#offermétodo O offermétodo não será bloqueado se a fila estiver cheia e retornar imediatamente false.
  3. Se ele não conseguir enfileirar a nova tarefa (ou seja, offerretornar false), ele tentará adicionar um novo encadeamento ao pool de encadeamentos com essa tarefa como seu primeiro trabalho.
  4. Se falhar ao adicionar o novo encadeamento, o executor será encerrado ou saturado. De qualquer maneira, a nova tarefa seria rejeitada usando o fornecido RejectedExecutionHandler.

A principal diferença entre os conjuntos de encadeamentos fixo e em cache se resume a esses três fatores:

  1. Tamanho do Pool Principal
  2. Tamanho máximo da piscina
  3. Filas
+ ----------- + ----------- + ------------------- + ----- ---------------------------- +
| Tipo de piscina | Tamanho do núcleo | Tamanho máximo | Estratégia de filas |
+ ----------- + ----------- + ------------------- + ----- ---------------------------- +
| Fixo | n (fixo) | n (fixo) | Não vinculado `LinkedBlockingQueue` |
+ ----------- + ----------- + ------------------- + ----- ---------------------------- +
| Em cache | 0 Inteiro.MAX_VALUE | `SynchronousQueue` |
+ ----------- + ----------- + ------------------- + ----- ---------------------------- +


Conjunto de segmentos fixos


Eis como Excutors.newFixedThreadPool(n)funciona:

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

Como você pode ver:

  • O tamanho do conjunto de encadeamentos é fixo.
  • Se houver alta demanda, ela não crescerá.
  • Se os threads ficarem ociosos por algum tempo, ele não encolherá.
  • Suponha que todos esses encadeamentos estejam ocupados com algumas tarefas de longa execução e a taxa de chegada ainda seja bastante alta. Como o executor está usando uma fila ilimitada, ele pode consumir uma grande parte do heap. Sendo bastante infelizes, podemos experimentar um OutOfMemoryError.

Quando devo usar um ou outro? Qual estratégia é melhor em termos de utilização de recursos?

Um conjunto de encadeamentos de tamanho fixo parece ser um bom candidato quando vamos limitar o número de tarefas simultâneas para fins de gerenciamento de recursos .

Por exemplo, se vamos usar um executor para lidar com solicitações de servidor da Web, um executor fixo pode lidar com as explosões de solicitações de maneira mais razoável.

Para um gerenciamento de recursos ainda melhor, é altamente recomendável criar um costume ThreadPoolExecutorcom uma BlockingQueue<T>implementação limitada juntamente com razoável RejectedExecutionHandler.


Conjunto de threads em cache


Eis como Executors.newCachedThreadPool()funciona:

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

Como você pode ver:

  • O conjunto de threads pode crescer de zero threads para Integer.MAX_VALUE. Praticamente, o conjunto de encadeamentos é ilimitado.
  • Se algum encadeamento estiver ocioso por mais de 1 minuto, ele poderá ser encerrado. Portanto, a piscina pode encolher se as linhas permanecerem muito ociosas.
  • Se todos os encadeamentos alocados estiverem ocupados enquanto uma nova tarefa chegar, ele criará um novo encadeamento, pois oferecer uma nova tarefa SynchronousQueuesempre falha quando não há ninguém do outro lado para aceitá-la!

Quando devo usar um ou outro? Qual estratégia é melhor em termos de utilização de recursos?

Use-o quando você tiver muitas tarefas previsíveis de execução curta.

Ali Dehghani
fonte
5

Você deve usar newCachedThreadPool apenas quando tiver tarefas assíncronas de curta duração, conforme declarado no Javadoc. Se você enviar tarefas que demoram mais tempo para serem processadas, você acabará criando muitos threads. Você pode atingir 100% da CPU se enviar tarefas de execução longa a uma taxa mais rápida para o newCachedThreadPool ( http://rashcoder.com/be-careful- while-using-executors-newcachedthreadpool/ ).

Dhanaraj Durairaj
fonte
1

Faço alguns testes rápidos e tenho as seguintes descobertas:

1) se estiver usando SynchronousQueue:

Depois que os encadeamentos atingirem o tamanho máximo, qualquer novo trabalho será rejeitado, com a exceção abaixo.

Exceção no encadeamento "main" java.util.concurrent.RejectedExecutionException: tarefa java.util.concurrent.FutureTask@3fee733d rejeitada de java.util.concurrent.ThreadPoolExecutor@5acf9800 [Em execução, tamanho do pool = 3, segmentos ativos = 3, tarefas em fila = 0, tarefas concluídas = 0]

em java.util.concurrent.ThreadPoolExecutor $ AbortPolicy.rejectedExecution (ThreadPoolExecutor.java:2047)

2) se estiver usando o LinkedBlockingQueue:

Os encadeamentos nunca aumentam do tamanho mínimo para o tamanho máximo, o que significa que o conjunto de encadeamentos é tamanho fixo como o tamanho mínimo.

Mike Lin
fonte