Um guia para o Java ExecutorService
1. Visão geral
ExecutorService é uma estrutura fornecida pelo JDK que simplifica a execução de tarefas no modo assíncrono. De modo geral,ExecutorService fornece automaticamente um pool de threads e API para atribuir tarefas a ele.
Leitura adicional:
Guia do Framework Fork / Join em Java
Uma introdução à estrutura de junção / junção apresentada no Java 7 e as ferramentas para ajudar a acelerar o processamento paralelo, tentando usar todos os núcleos de processador disponíveis.
Guia para java.util.concurrent.Locks
Neste artigo, exploramos várias implementações da interface Lock e a recém-introduzida na classe StampedLock do Java 9.
2. InstanciandoExecutorService
2.1. Métodos de fábrica da classeExecutors
A maneira mais fácil de criarExecutorService é usar um dos métodos de fábrica da classeExecutors.
Por exemplo, a seguinte linha de código criará um pool de threads com 10 threads:
ExecutorService executor = Executors.newFixedThreadPool(10);
Existem vários outros métodos de fábrica para criarExecutorService predefinidos que atendem a casos de uso específicos. Para encontrar o melhor método para suas necessidades, consulteOracle’s official documentation.
2.2. Crie diretamente umExecutorService
ComoExecutorService é uma interface, uma instância de qualquer uma de suas implementações pode ser usada. Existem várias implementações para escolher no pacotejava.util.concurrent ou você pode criar a sua própria.
Por exemplo, a classeThreadPoolExecutor tem alguns construtores que podem ser usados para configurar um serviço do executor e seu pool interno.
ExecutorService executorService =
new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue());
Você pode notar que o código acima é muito semelhante aosource code do método de fábricanewSingleThreadExecutor().. Para a maioria dos casos, uma configuração manual detalhada não é necessária.
3. Atribuindo tarefas aoExecutorService
ExecutorService pode executar tarefasRunnableeCallable. Para simplificar as coisas neste artigo, duas tarefas primitivas serão usadas. Observe que expressões lambda são usadas aqui em vez de classes internas anônimas:
Runnable runnableTask = () -> {
try {
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
};
Callable callableTask = () -> {
TimeUnit.MILLISECONDS.sleep(300);
return "Task's execution";
};
List> callableTasks = new ArrayList<>();
callableTasks.add(callableTask);
callableTasks.add(callableTask);
callableTasks.add(callableTask);
As tarefas podem ser atribuídas aExecutorService usando vários métodos, incluindoexecute(), que é herdado da interfaceExecutor, e tambémsubmit(),invokeAny(), invokeAll().
O métodoexecute() évoid,e não dá qualquer possibilidade de obter o resultado da execução da tarefa ou de verificar o status da tarefa (se está em execução ou foi executada).
executorService.execute(runnableTask);
submit() envia uma tarefaCallable ouRunnable para umExecutorServicee retorna um resultado do tipoFuture.
Future future =
executorService.submit(callableTask);
invokeAny() atribui uma coleção de tarefas aExecutorService, fazendo com que cada uma seja executada e retorna o resultado de uma execução bem-sucedida de uma tarefa (se houve uma execução bem-sucedida).
String result = executorService.invokeAny(callableTasks);
invokeAll() atribui uma coleção de tarefas a umExecutorService, fazendo com que cada uma seja executada e retorna o resultado de todas as execuções de tarefas na forma de uma lista de objetos do tipoFuture.
List> futures = executorService.invokeAll(callableTasks);
Agora, antes de prosseguir, mais duas coisas devem ser discutidas: encerrar umExecutorServicee lidar com os tipos de retornoFuture.
4. Desligando umExecutorService
Em geral, oExecutorService não será destruído automaticamente quando não houver tarefa para processar. Ele permanecerá vivo e aguardará o novo trabalho.
Em alguns casos, isso é muito útil; por exemplo, se um aplicativo precisar processar tarefas que aparecem de maneira irregular ou a quantidade dessas tarefas não for conhecida no momento da compilação.
Por outro lado, um aplicativo pode chegar ao fim, mas não será interrompido porque umExecutorService em espera fará com que a JVM continue em execução.
Para desligar corretamente umExecutorService, temos as APIsshutdown()eshutdownNow().
O métodoshutdown() não causa uma destruição imediata doExecutorService. Ele fará com queExecutorService pare de aceitar novas tarefas e feche após todos os threads em execução concluírem seu trabalho atual.
executorService.shutdown();
O métodoshutdownNow() tenta destruir oExecutorService imediatamente, mas não garante que todos os threads em execução serão interrompidos ao mesmo tempo. Este método retorna uma lista de tarefas que estão aguardando para serem processadas. Cabe ao desenvolvedor decidir o que fazer com essas tarefas.
List notExecutedTasks = executorService.shutDownNow();
Uma boa maneira de encerrar oExecutorService (que também érecommended by Oracle) é usar ambos os métodos combinados com o métodoawaitTermination(). Com essa abordagem, oExecutorService primeiro interromperá a execução de novas tarefas e esperará até um determinado período de tempo para que todas as tarefas sejam concluídas. Se esse tempo expirar, a execução será interrompida imediatamente:
executorService.shutdown();
try {
if (!executorService.awaitTermination(800, TimeUnit.MILLISECONDS)) {
executorService.shutdownNow();
}
} catch (InterruptedException e) {
executorService.shutdownNow();
}
5. A interfaceFuture
Os métodossubmit()einvokeAll() retornam um objeto ou uma coleção de objetos do tipoFuture, o que nos permite obter o resultado da execução de uma tarefa ou verificar o status da tarefa (está em execução ou executado).
A interfaceFuture fornece um método de bloqueio especialget() que retorna um resultado real da execução da tarefaCallable ounull no caso da tarefaRunnable. Chamar o métodoget() enquanto a tarefa ainda está em execução fará com que a execução seja bloqueada até que a tarefa seja executada corretamente e o resultado esteja disponível.
Future future = executorService.submit(callableTask);
String result = null;
try {
result = future.get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
Com um bloqueio muito longo causado pelo métodoget(), o desempenho de um aplicativo pode degradar. Se os dados resultantes não forem cruciais, é possível evitar esse problema usando tempos limite:
String result = future.get(200, TimeUnit.MILLISECONDS);
Se o período de execução for maior que o especificado (neste caso, 200 milissegundos), umTimeoutException será lançado.
O métodoisDone() pode ser usado para verificar se a tarefa atribuída já foi processada ou não.
A interfaceFuture também permite o cancelamento da execução da tarefa com o métodocancel(), e para verificar o cancelamento com o métodoisCancelled():
boolean canceled = future.cancel(true);
boolean isCancelled = future.isCancelled();
6. A interfaceScheduledExecutorService
OScheduledExecutorService executa tarefas após algum atraso predefinido e / ou periodicamente. Mais uma vez, a melhor maneira de instanciarScheduledExecutorService é usar os métodos de fábrica da classeExecutors.
Para esta seção, umScheduledExecutorService com um thread será usado:
ScheduledExecutorService executorService = Executors
.newSingleThreadScheduledExecutor();
Para agendar a execução de uma única tarefa após um atraso fixo, use o métodoscheduled() doScheduledExecutorService. Existem dois métodosscheduled() que permitem a você executar tarefasRunnable ouCallable:
Future resultFuture =
executorService.schedule(callableTask, 1, TimeUnit.SECONDS);
O métodoscheduleAtFixedRate() permite executar uma tarefa periodicamente após um atraso fixo. O código acima demora um segundo antes de executarcallableTask.
O seguinte bloco de código executará uma tarefa após um atraso inicial de 100 milissegundos e, em seguida, executará a mesma tarefa a cada 450 milissegundos. Se o processador precisa de mais tempo para executar uma tarefa atribuída do que o parâmetroperiod do métodoscheduleAtFixedRate(), oScheduledExecutorService vai esperar até que a tarefa atual seja concluída antes de iniciar a próxima:
Future resultFuture = service
.scheduleAtFixedRate(runnableTask, 100, 450, TimeUnit.MILLISECONDS);
Se for necessário ter um atraso de comprimento fixo entre as iterações da tarefa,scheduleWithFixedDelay() deve ser usado. Por exemplo, o código a seguir garantirá uma pausa de 150 milissegundos entre o final da execução atual e o início de outra.
service.scheduleWithFixedDelay(task, 100, 150, TimeUnit.MILLISECONDS);
De acordo com os contratos de métodoscheduleAtFixedRate()escheduleWithFixedDelay(), a execução do período da tarefa terminará no término deExecutorService ou se uma exceção for lançada durante a execução da tarefa.
7. ExecutorService vs. Fork/Join
Após o lançamento do Java 7, muitos desenvolvedores decidiram que a estruturaExecutorService deveria ser substituída pela estrutura fork / join. Porém, essa nem sempre é a decisão certa. Apesar da simplicidade de uso e dos frequentes ganhos de desempenho associados ao fork / join, também há uma redução na quantidade de controle do desenvolvedor sobre a execução simultânea.
ExecutorService dá ao desenvolvedor a capacidade de controlar o número de threads gerados e a granularidade das tarefas que devem ser executadas por threads separadas. O melhor caso de uso paraExecutorService é o processamento de tarefas independentes, como transações ou solicitações de acordo com o esquema “um encadeamento para uma tarefa”.
Em contraste,according to Oracle’s documentation, fork / join foi projetado para acelerar o trabalho que pode ser quebrado em pedaços menores recursivamente.
8. Conclusão
Mesmo apesar da relativa simplicidade deExecutorService, existem algumas armadilhas comuns. Vamos resumi-los:
Keeping an unused ExecutorService alive: Há uma explicação detalhada na seção 4 deste artigo sobre como desligar umExecutorService;
Wrong thread-pool capacity while using fixed length thread-pool: É muito importante determinar de quantos threads o aplicativo precisará para executar tarefas com eficiência. Um conjunto de encadeamentos muito grande causará sobrecarga desnecessária apenas para criar encadeamentos, que geralmente estarão no modo de espera. Poucos podem fazer com que um aplicativo pareça não responder por causa de longos períodos de espera para tarefas na fila;
Calling a Future‘s get() method after task cancellation: Uma tentativa de obter o resultado de uma tarefa já cancelada irá disparar umCancellationException.
Unexpectedly-long blocking with Future‘s get() method: Timeouts devem ser usados para evitar esperas inesperadas.
O código deste artigo está disponível ema GitHub repository.