Executores e Pool de Threads
Já exploramos uma variedade de mecanismos para suportar multithreading, e Executors é um deles!
O que são Executors e Thread Pooling?
Executors é um mecanismo que oferece abstrações de alto nível para o gerenciamento de threads. Permite criar e gerenciar um pool de threads, que consiste em um conjunto de threads pré-existentes prontas para executar tarefas. Em vez de criar uma nova thread para cada tarefa, as tarefas são enviadas para o pool, onde sua execução é distribuída entre as threads.
Então, o que exatamente é um thread pool? Trata-se de uma coleção de threads pré-existentes prontas para executar tarefas. Ao utilizar um thread pool, evita-se o custo de criar e destruir threads repetidamente, pois as mesmas threads podem ser reutilizadas para múltiplas tarefas.
Se houver mais tarefas do que threads, as tarefas aguardam na Task Queue. Uma tarefa da fila é processada por uma thread disponível do pool, e, assim que a tarefa é concluída, a thread busca uma nova tarefa na fila. Quando todas as tarefas da fila são finalizadas, as threads permanecem ativas e aguardam por novas tarefas.
Exemplo do Cotidiano
Pense em um restaurante onde cozinheiros (threads) preparam pedidos (tarefas). Em vez de contratar um novo cozinheiro para cada pedido, o restaurante emprega um número limitado de cozinheiros que atendem aos pedidos conforme eles chegam. Assim que um cozinheiro termina um pedido, ele assume o próximo, o que contribui para o uso eficiente dos recursos do restaurante.
Método Principal
newFixedThreadPool(int n): Cria um pool com um número fixo de threads igual a n.
Main.java
1ExecutorService executorService = Executors.newFixedThreadPool(20);
newCachedThreadPool(): Cria um pool que pode criar novas threads conforme necessário, mas irá reutilizar threads disponíveis se houver.
Main.java
1ExecutorService executorService = Executors.newCachedThreadPool();
newSingleThreadExecutor(): Cria um pool de thread único que garante que as tarefas sejam executadas sequencialmente, ou seja, uma após a outra. Útil para tarefas que precisam ser executadas em ordem estrita.
Main.java
1ExecutorService executorService = Executors.newSingleThreadExecutor();
Em todos os exemplos, os métodos de Executors retornam uma implementação da interface ExecutorService, que é utilizada para gerenciar threads.
ExecutorService fornece métodos para gerenciar um pool de threads. Por exemplo, submit(Runnable task) aceita uma tarefa como um objeto Runnable e a coloca em uma fila para execução. Ele retorna um objeto Future, que pode ser utilizado para verificar o status da tarefa e obter um resultado caso a tarefa produza um resultado.
Main.java
12345678910111213141516171819202122232425262728293031323334package com.example; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; public class Main { public static void main(String[] args) { // Create a thread pool with 5 threads ExecutorService executor = Executors.newFixedThreadPool(5); // Define the task to be executed Runnable task = () -> { System.out.println("Task is running: " + Thread.currentThread().getName()); try { Thread.sleep(2000); // Simulate some work } catch (InterruptedException e) { Thread.currentThread().interrupt(); } System.out.println("Task completed: " + Thread.currentThread().getName()); }; // Submit the task for execution and get a `Future` Future<?> future = executor.submit(task); // Check if the task is done System.out.println("Is task done? " + future.isDone()); // You can use `future` to check the status of the task or wait for its completion // Example: future.get() - blocks until the task is completed (not used in this example) // Initiate an orderly shutdown of the executor service executor.shutdown(); } }
O método shutdown() inicia um desligamento gracioso do pool de threads. Ele deixa de aceitar novas tarefas, mas irá concluir as tarefas atuais. Após chamar este método, o pool não pode ser reiniciado.
O método awaitTermination(long timeout, TimeUnit unit) aguarda que todas as tarefas no pool sejam finalizadas dentro do período de tempo especificado. Trata-se de uma espera bloqueante que permite garantir que todas as tarefas sejam concluídas antes de finalizar o pool.
Além disso, não mencionamos a principal interface que auxilia no acompanhamento do estado da thread, que é a interface Future. O método submit() da interface ExecutorService retorna uma implementação da interface Future.
Se for necessário obter o resultado da execução da thread, pode-se utilizar o método get(). Se a thread implementar Runnable, o método get() não retorna nada, mas se for Callable<T>, retorna o tipo T.
Main.java
12345678910111213141516171819202122232425262728293031package com.example; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; public class Main { public static void main(String[] args) { ExecutorService executor = Executors.newFixedThreadPool(3); // Callable task that returns a result Callable<String> task = () -> { Thread.sleep(1000); // Simulate some work return "Task result"; }; Future<String> future = executor.submit(task); try { // Get the result of the task String result = future.get(); System.out.println("Task completed with result: " + result); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } executor.shutdown(); } }
Também é possível utilizar o método cancel(boolean mayInterruptIfRunning) para tentar cancelar a execução de uma tarefa. Se a tarefa ainda não tiver iniciado, ela será cancelada. Se a tarefa já estiver em execução, ela pode ser interrompida com base no parâmetro mayInterruptIfRunning.
true: Se a tarefa estiver em execução, ela será interrompida por meio da chamada de Thread.interrupt() na thread em execução.
false: Se a tarefa estiver em execução, ela não será interrompida e a tentativa de cancelamento não terá efeito sobre a tarefa em andamento.
Bem como 2 métodos que intuitivamente indicam sua finalidade:
isCancelled(): Verifica se a tarefa foi cancelada;isDone(): Verifica se a tarefa foi concluída.
Exemplo de Uso
O tamanho do pool de threads depende da natureza das tarefas executadas. Normalmente, o tamanho do pool de threads não deve ser fixo; em vez disso, deve ser personalizável. O tamanho ideal é determinado pelo monitoramento da taxa de processamento das tarefas executadas.
É mais eficiente utilizar o número de threads = processor cores. Isso pode ser observado no código utilizando Runtime.getRuntime().availableProcessors().
Main.java
1int availableProcessors = Runtime.getRuntime().availableProcessors();
Diferenças entre Criar Threads Diretamente e Utilizar ExecutorService
As principais diferenças entre criar threads diretamente e utilizar ExecutorService são conveniência e gerenciamento de recursos. A criação manual de threads exige o gerenciamento individual de cada thread, o que complica o código e a administração.
O ExecutorService simplifica o gerenciamento ao utilizar um pool de threads, facilitando o tratamento das tarefas. Além disso, enquanto a criação manual de threads pode resultar em alto consumo de recursos, o ExecutorService permite personalizar o tamanho do pool de threads.
Obrigado pelo seu feedback!
Pergunte à IA
Pergunte à IA
Pergunte o que quiser ou experimente uma das perguntas sugeridas para iniciar nosso bate-papo
Incrível!
Completion taxa melhorada para 3.33
Executores e Pool de Threads
Deslize para mostrar o menu
Já exploramos uma variedade de mecanismos para suportar multithreading, e Executors é um deles!
O que são Executors e Thread Pooling?
Executors é um mecanismo que oferece abstrações de alto nível para o gerenciamento de threads. Permite criar e gerenciar um pool de threads, que consiste em um conjunto de threads pré-existentes prontas para executar tarefas. Em vez de criar uma nova thread para cada tarefa, as tarefas são enviadas para o pool, onde sua execução é distribuída entre as threads.
Então, o que exatamente é um thread pool? Trata-se de uma coleção de threads pré-existentes prontas para executar tarefas. Ao utilizar um thread pool, evita-se o custo de criar e destruir threads repetidamente, pois as mesmas threads podem ser reutilizadas para múltiplas tarefas.
Se houver mais tarefas do que threads, as tarefas aguardam na Task Queue. Uma tarefa da fila é processada por uma thread disponível do pool, e, assim que a tarefa é concluída, a thread busca uma nova tarefa na fila. Quando todas as tarefas da fila são finalizadas, as threads permanecem ativas e aguardam por novas tarefas.
Exemplo do Cotidiano
Pense em um restaurante onde cozinheiros (threads) preparam pedidos (tarefas). Em vez de contratar um novo cozinheiro para cada pedido, o restaurante emprega um número limitado de cozinheiros que atendem aos pedidos conforme eles chegam. Assim que um cozinheiro termina um pedido, ele assume o próximo, o que contribui para o uso eficiente dos recursos do restaurante.
Método Principal
newFixedThreadPool(int n): Cria um pool com um número fixo de threads igual a n.
Main.java
1ExecutorService executorService = Executors.newFixedThreadPool(20);
newCachedThreadPool(): Cria um pool que pode criar novas threads conforme necessário, mas irá reutilizar threads disponíveis se houver.
Main.java
1ExecutorService executorService = Executors.newCachedThreadPool();
newSingleThreadExecutor(): Cria um pool de thread único que garante que as tarefas sejam executadas sequencialmente, ou seja, uma após a outra. Útil para tarefas que precisam ser executadas em ordem estrita.
Main.java
1ExecutorService executorService = Executors.newSingleThreadExecutor();
Em todos os exemplos, os métodos de Executors retornam uma implementação da interface ExecutorService, que é utilizada para gerenciar threads.
ExecutorService fornece métodos para gerenciar um pool de threads. Por exemplo, submit(Runnable task) aceita uma tarefa como um objeto Runnable e a coloca em uma fila para execução. Ele retorna um objeto Future, que pode ser utilizado para verificar o status da tarefa e obter um resultado caso a tarefa produza um resultado.
Main.java
12345678910111213141516171819202122232425262728293031323334package com.example; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; public class Main { public static void main(String[] args) { // Create a thread pool with 5 threads ExecutorService executor = Executors.newFixedThreadPool(5); // Define the task to be executed Runnable task = () -> { System.out.println("Task is running: " + Thread.currentThread().getName()); try { Thread.sleep(2000); // Simulate some work } catch (InterruptedException e) { Thread.currentThread().interrupt(); } System.out.println("Task completed: " + Thread.currentThread().getName()); }; // Submit the task for execution and get a `Future` Future<?> future = executor.submit(task); // Check if the task is done System.out.println("Is task done? " + future.isDone()); // You can use `future` to check the status of the task or wait for its completion // Example: future.get() - blocks until the task is completed (not used in this example) // Initiate an orderly shutdown of the executor service executor.shutdown(); } }
O método shutdown() inicia um desligamento gracioso do pool de threads. Ele deixa de aceitar novas tarefas, mas irá concluir as tarefas atuais. Após chamar este método, o pool não pode ser reiniciado.
O método awaitTermination(long timeout, TimeUnit unit) aguarda que todas as tarefas no pool sejam finalizadas dentro do período de tempo especificado. Trata-se de uma espera bloqueante que permite garantir que todas as tarefas sejam concluídas antes de finalizar o pool.
Além disso, não mencionamos a principal interface que auxilia no acompanhamento do estado da thread, que é a interface Future. O método submit() da interface ExecutorService retorna uma implementação da interface Future.
Se for necessário obter o resultado da execução da thread, pode-se utilizar o método get(). Se a thread implementar Runnable, o método get() não retorna nada, mas se for Callable<T>, retorna o tipo T.
Main.java
12345678910111213141516171819202122232425262728293031package com.example; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; public class Main { public static void main(String[] args) { ExecutorService executor = Executors.newFixedThreadPool(3); // Callable task that returns a result Callable<String> task = () -> { Thread.sleep(1000); // Simulate some work return "Task result"; }; Future<String> future = executor.submit(task); try { // Get the result of the task String result = future.get(); System.out.println("Task completed with result: " + result); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } executor.shutdown(); } }
Também é possível utilizar o método cancel(boolean mayInterruptIfRunning) para tentar cancelar a execução de uma tarefa. Se a tarefa ainda não tiver iniciado, ela será cancelada. Se a tarefa já estiver em execução, ela pode ser interrompida com base no parâmetro mayInterruptIfRunning.
true: Se a tarefa estiver em execução, ela será interrompida por meio da chamada de Thread.interrupt() na thread em execução.
false: Se a tarefa estiver em execução, ela não será interrompida e a tentativa de cancelamento não terá efeito sobre a tarefa em andamento.
Bem como 2 métodos que intuitivamente indicam sua finalidade:
isCancelled(): Verifica se a tarefa foi cancelada;isDone(): Verifica se a tarefa foi concluída.
Exemplo de Uso
O tamanho do pool de threads depende da natureza das tarefas executadas. Normalmente, o tamanho do pool de threads não deve ser fixo; em vez disso, deve ser personalizável. O tamanho ideal é determinado pelo monitoramento da taxa de processamento das tarefas executadas.
É mais eficiente utilizar o número de threads = processor cores. Isso pode ser observado no código utilizando Runtime.getRuntime().availableProcessors().
Main.java
1int availableProcessors = Runtime.getRuntime().availableProcessors();
Diferenças entre Criar Threads Diretamente e Utilizar ExecutorService
As principais diferenças entre criar threads diretamente e utilizar ExecutorService são conveniência e gerenciamento de recursos. A criação manual de threads exige o gerenciamento individual de cada thread, o que complica o código e a administração.
O ExecutorService simplifica o gerenciamento ao utilizar um pool de threads, facilitando o tratamento das tarefas. Além disso, enquanto a criação manual de threads pode resultar em alto consumo de recursos, o ExecutorService permite personalizar o tamanho do pool de threads.
Obrigado pelo seu feedback!