Fundo

Programas de servidor, como bancos de dados e servidores da Web, executam repetidamente requests de vários clientes e são orientados para o processamento de um grande número de tarefas curtas. Uma abordagem para construir um aplicativo de servidor seria criar um novo encadeamento cada vez que uma solicitação chegar e atender a essa nova solicitação no encadeamento recém-criado. Embora essa abordagem pareça simples de implementar, ela tem desvantagens significativas. Um servidor que cria um novo encadeamento para cada solicitação gastaria mais tempo e consumiria mais recursos do sistema na criação e destruição de encadeamentos do que no processamento de requests reais.

Como os encadeamentos ativos consomem recursos do sistema, uma JVM criando muitos encadeamentos ao mesmo tempo pode fazer com que o sistema fique sem memória. Isso exige a necessidade de limitar o número de threads que estão sendo criados.

O que é ThreadPool em Java?

Um pool de encadeamentos reutiliza encadeamentos criados anteriormente para executar tarefas atuais e oferece uma solução para o problema de sobrecarga do ciclo de encadeamento e fragmentação de recursos. Como o encadeamento já existe quando a solicitação chega, o atraso introduzido pela criação do encadeamento é eliminado, tornando o aplicativo mais responsivo.

  • Java fornece a estrutura do Executor que está centrada na interface do Executor, sua subinterface - ExecutorService e a classe - ThreadPoolExecutor , que implementa ambas as interfaces. Ao usar o executor, basta implementar os objetos Runnable e enviá-los ao executor para execução.
  • Eles permitem que você tire vantagem do encadeamento, mas concentre-se nas tarefas que deseja que o encadeamento execute, em vez da mecânica do encadeamento.
  • Para usar pools de threads, primeiro criamos um objeto de ExecutorService e passamos um conjunto de tarefas para ele. A classe ThreadPoolExecutor permite definir o tamanho máximo e o tamanho máximo do pool. Os executáveis ​​que são executados por um determinado thread são executados sequencialmente.
    TP Init

    Inicialização do pool de threads com tamanho = 3 threads. Fila de tarefas = 5 objetos executáveis

    Métodos de pool de threads de execução

                             Descrição do Método
    newFixedThreadPool (int) Cria um pool de threads de tamanho fixo.
    newCachedThreadPool() Cria um pool de threads que cria novos 
                                      threads conforme necessário, mas irá reutilizar anteriormente 
                                      roscas construídas quando estão disponíveis
    newSingleThreadExecutor() Cria um único thread. 
    

    No caso de um pool de thread fixo, se todos os threads estiverem sendo executados atualmente pelo executor, as tarefas pendentes serão colocadas em uma fila e executadas quando um thread ficar ocioso.

    Exemplo de pool de threads

    No tutorial a seguir, veremos um exemplo básico de executor de pool de threads - FixedThreadPool.

    Passos a serem seguidos

    1. Crie uma tarefa (Objeto Executável) para executar
    2. Crie um pool de executores usando executores
    3. Passe tarefas para Executor Pool
    4. Desligue o pool de executores
    




    // Java program to illustrate 
    // ThreadPool
    import java.text.SimpleDateFormat; 
    import java.util.Date;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
      
    // Task class to be executed (Step 1)
    class Task implements Runnable   
    {
        private String name;
          
        public Task(String s)
        {
            name = s;
        }
          
        // Prints task name and sleeps for 1s
        // This Whole process is repeated 5 times
        public void run()
        {
            try
            {
                for (int i = 0; i<=5; i++)
                {
                    if (i==0)
                    {
                        Date d = new Date();
                        SimpleDateFormat ft = new SimpleDateFormat("hh:mm:ss");
                        System.out.println("Initialization Time for"
                                + " task name - "+ name +" = " +ft.format(d));   
                        //prints the initialization time for every task 
                    }
                    else
                    {
                        Date d = new Date();
                        SimpleDateFormat ft = new SimpleDateFormat("hh:mm:ss");
                        System.out.println("Executing Time for task name - "+
                                name +" = " +ft.format(d));   
                        // prints the execution time for every task 
                    }
                    Thread.sleep(1000);
                }
                System.out.println(name+" complete");
            }
              
            catch(InterruptedException e)
            {
                e.printStackTrace();
            }
        }
    }
    public class Test
    {
         // Maximum number of threads in thread pool
        static final int MAX_T = 3;             
      
        public static void main(String[] args)
        {
            // creates five tasks
            Runnable r1 = new Task("task 1");
            Runnable r2 = new Task("task 2");
            Runnable r3 = new Task("task 3");
            Runnable r4 = new Task("task 4");
            Runnable r5 = new Task("task 5");      
              
            // creates a thread pool with MAX_T no. of 
            // threads as the fixed pool size(Step 2)
            ExecutorService pool = Executors.newFixedThreadPool(MAX_T);  
             
            // passes the Task objects to the pool to execute (Step 3)
            pool.execute(r1);
            pool.execute(r2);
            pool.execute(r3);
            pool.execute(r4);
            pool.execute(r5); 
              
            // pool shutdown ( Step 4)
            pool.shutdown();    
        }
    }

    Execução de amostra

    Saída:
    Tempo de inicialização para o nome da tarefa - tarefa 2 = 02:32:56
    Tempo de inicialização para o nome da tarefa - tarefa 1 = 02:32:56
    Tempo de inicialização para o nome da tarefa - tarefa 3 = 02:32:56
    Tempo de execução para o nome da tarefa - tarefa 1 = 02:32:57
    Tempo de execução para o nome da tarefa - tarefa 2 = 02:32:57
    Tempo de execução para o nome da tarefa - tarefa 3 = 02:32:57
    Tempo de execução para o nome da tarefa - tarefa 1 = 02:32:58
    Tempo de execução para o nome da tarefa - tarefa 2 = 02:32:58
    Tempo de execução para o nome da tarefa - tarefa 3 = 02:32:58
    Tempo de execução para o nome da tarefa - tarefa 1 = 02:32:59
    Tempo de execução para o nome da tarefa - tarefa 2 = 02:32:59
    Tempo de execução para o nome da tarefa - tarefa 3 = 02:32:59
    Tempo de execução para o nome da tarefa - tarefa 1 = 02:33:00
    Tempo de execução para o nome da tarefa - tarefa 3 = 02:33:00
    Tempo de execução para o nome da tarefa - tarefa 2 = 02:33:00
    Tempo de execução para o nome da tarefa - tarefa 2 = 02:33:01
    Tempo de execução para o nome da tarefa - tarefa 1 = 02:33:01
    Tempo de execução para o nome da tarefa - tarefa 3 = 02:33:01
    tarefa 2 completa
    tarefa 1 completa
    tarefa 3 completa
    Tempo de inicialização para o nome da tarefa - tarefa 5 = 02:33:02
    Tempo de inicialização para o nome da tarefa - tarefa 4 = 02:33:02
    Tempo de execução para o nome da tarefa - tarefa 4 = 02:33:03
    Tempo de execução para o nome da tarefa - tarefa 5 = 02:33:03
    Tempo de execução para o nome da tarefa - tarefa 5 = 02:33:04
    Tempo de execução para o nome da tarefa - tarefa 4 = 02:33:04
    Tempo de execução para o nome da tarefa - tarefa 4 = 02:33:05
    Tempo de execução para o nome da tarefa - tarefa 5 = 02:33:05
    Tempo de execução para o nome da tarefa - tarefa 5 = 02:33:06
    Tempo de execução para o nome da tarefa - tarefa 4 = 02:33:06
    Tempo de execução para o nome da tarefa - tarefa 5 = 02:33:07
    Tempo de execução para o nome da tarefa - tarefa 4 = 02:33:07
    tarefa 5 completa
    tarefa 4 completa
    

    Como visto na execução do programa, a tarefa 4 ou tarefa 5 são executadas apenas quando um thread no pool fica ocioso. Até então, as tarefas extras são colocadas em uma fila.

    TP Exec 1

    Pool de threads executando as três primeiras tarefas

    TP Exec 2

    Thread Pool executando tarefas 4 e 5

    Uma das principais vantagens de usar essa abordagem é quando você deseja processar 100 requests por vez, mas não deseja criar 100 Threads para as mesmas, de modo a reduzir a sobrecarga da JVM. Você pode usar essa abordagem para criar um ThreadPool de 10 Threads e enviar 100 requests a esse ThreadPool.
    ThreadPool criará no máximo 10 threads para processar 10 requests por vez. Após a conclusão do processo de qualquer Thread único,
    ThreadPool irá alocar internamente a 11ª solicitação para este Thread 
    e continuará fazendo o mesmo com todas as requests restantes.
    

    Riscos no uso de Thread Pools

      1. Deadlock : Embora o deadlock possa ocorrer em qualquer programa multi-threaded, os thread pools introduzem outro caso de deadlock, no qual todos os threads em execução estão aguardando os resultados dos threads bloqueados que aguardam na fila devido à indisponibilidade de threads para execução.
      2. Thread Leakage: Thread Leakage ocorre se um thread é removido do pool para executar uma tarefa, mas não retornou a ele quando a tarefa foi concluída. Por exemplo, se o encadeamento lançar uma exceção e a classe de pool não capturar essa exceção, o encadeamento simplesmente sairá, reduzindo o tamanho do conjunto de encadeamentos em um. Se isso se repetir muitas vezes, o pool eventualmente ficará vazio e nenhum encadeamento estará disponível para executar outras requests.
      3. Recurso Thrashing: Se o tamanho do pool de threads for muito grande, tempo será perdido na alternância de contexto entre as threads. Ter mais encadeamentos do que o número ideal pode causar problemas de fome levando à sobrecarga de recursos, conforme explicado.

    Pontos importantes

      1. Não enfileire tarefas que aguardam simultaneamente os resultados de outras tarefas. Isso pode levar a uma situação de conflito, conforme descrito acima.
      2. Tenha cuidado ao usar threads para uma operação de longa duração. Isso pode fazer com que o thread fique esperando para sempre e, eventualmente, levará ao vazamento de recursos.
      3. O Thread Pool deve ser encerrado explicitamente no final. Se isso não for feito, o programa continua em execução e nunca termina. Chame shutdown() no pool para encerrar o executor. Se você tentar enviar outra tarefa ao executor após o desligamento, ele lançará uma RejectedExecutionException.
      4. É preciso entender as tarefas para ajustar efetivamente o pool de threads. Se as tarefas forem muito contrastantes, faz sentido usar diferentes conjuntos de encadeamentos para diferentes tipos de tarefas, a fim de ajustá-los adequadamente.
      5. Você pode restringir o número máximo de encadeamentos que podem ser executados na JVM, reduzindo as chances de JVM ficar sem memória.
      6. Se você precisar implementar seu loop para criar novos threads para processamento, o uso de ThreadPool ajudará a processar mais rápido, já que ThreadPool não cria novos Threads depois de atingir seu limite máximo.
      7. Após a conclusão do Processamento de Thread, ThreadPool pode usar o mesmo Thread para fazer outro processo (economizando tempo e recursos para criar outro Thread.)

      Tuning Thread Pool

      • O tamanho ideal do pool de threads depende do número de processadores disponíveis e da natureza das tarefas. Em um sistema de processador N para uma fila de apenas processos de tipo de computação, um tamanho máximo de pool de thread de N ou N + 1 alcançará a eficiência máxima. Mas as tarefas podem esperar por E / S e, nesse caso, levamos em consideração a proporção de tempo de espera (W) e tempo de atendimento (S) por uma solicitação; resultando em um tamanho máximo de pool de N * (1+ W / S) para máxima eficiência.

      O pool de threads é uma ferramenta útil para organizar aplicativos de servidor. É um conceito bastante simples, mas há vários problemas a serem observados ao implementar e usar um, como deadlock, fragmentação de recursos. O uso do serviço do executor facilita a implementação.

      Este artigo é uma contribuição de Abhishek . Se você gosta de GeeksforGeeks e gostaria de contribuir, você também pode escrever um artigo usando contribute.geeksforgeeks.org ou enviar o seu artigo para contribute@geeksforgeeks.org. Veja o seu artigo na página principal do GeeksforGeeks e ajude outros Geeks.

      Escreva comentários se encontrar algo incorreto ou se quiser compartilhar mais informações sobre o tópico discutido acima.