A Guide to the Java ExecutorService

Overview

ExecutorService é uma API JDK que simplifica a execução de tarefas em modo assíncrono. De um modo geral, ExecutorService fornece automaticamente um conjunto de threads e uma API para a atribuição de tarefas.

Outra leitura:

Guia para o Fork/Join Framework em Java

Uma introdução ao fork/join framework apresentado em Java 7 e as ferramentas para ajudar a acelerar o processamento paralelo através da tentativa de utilizar todos os núcleos de processador disponíveis.
Leia mais →

Visão geral do java.util.concurrent

Descubra o conteúdo do pacote java.util.concurrent.
Leia mais →

Guia para java.util.concurrent.Locks

Neste artigo, exploramos várias implementações da interface Lock e a recém introduzida na classe Java 9 StampedLock.
Leia mais →

Instanciating ExecutorService

2.1. Métodos de Fábrica da Classe Executors

A forma mais fácil de criar ExecutorService é usar um dos métodos de fábrica da classe Executors.

Por exemplo, a seguinte linha de código criará um conjunto de linhas com 10 linhas:

ExecutorService executor = Executors.newFixedThreadPool(10);

Existem vários outros métodos de fábrica para criar um ExecutorService pré-definido que satisfaz casos específicos de uso. Para encontrar o melhor método para as suas necessidades, consulte a documentação oficial da Oracle.

2.2. Criar directamente um ExecutorService

Porque ExecutorService é uma interface, uma instância de quaisquer das suas implementações pode ser utilizada. Existem várias implementações para escolher no pacote java.util.concurrent, ou pode criar a sua própria.

Por exemplo, a classe ThreadPoolExecutor tem alguns construtores que podemos usar para configurar um serviço executor e o seu pool interno:

ExecutorService executorService = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());

P>Pode notar que o código acima é muito semelhante ao código fonte do método de fábrica newSingleThreadExecutor(). Na maioria dos casos, não é necessária uma configuração manual detalhada.

Assigning Tasks to the ExecutorService

ExecutorService pode executar tarefas Executáveis e Chamáveis. Para manter as coisas simples neste artigo, serão utilizadas duas tarefas primitivas. Note que usamos aqui expressões lambda em vez de classes internas anónimas:

Runnable runnableTask = () -> { try { TimeUnit.MILLISECONDS.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); }};Callable<String> callableTask = () -> { TimeUnit.MILLISECONDS.sleep(300); return "Task's execution";};List<Callable<String>> callableTasks = new ArrayList<>();callableTasks.add(callableTask);callableTasks.add(callableTask);callableTasks.add(callableTask);

Podemos atribuir tarefas ao ExecutorService usando vários métodos incluindo executar(), que é herdado da interface do Executor, e também submeter(), invocarAny() e invocarAll().

O método execute() é nulo e não dá qualquer possibilidade de obter o resultado da execução de uma tarefa ou de verificar o estado da tarefa (está a correr):

executorService.execute(runnableTask);

submit() submete uma tarefa Chamável ou Executável a um ExecutorService e retorna um resultado do tipo Futuro:

Future<String> future = executorService.submit(callableTask);

invokeAny() atribui uma colecção de tarefas a um ExecutorService, 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 colecção de tarefas a um ExecutorService, provocando a execução de cada uma, e retorna o resultado de todas as execuções de tarefas na forma de uma lista de objectos do tipo Futuro:

List<Future<String>> futures = executorService.invokeAll(callableTasks);

Antes de ir mais longe, precisamos de discutir mais dois itens: encerrar um ExecutorService e lidar com os tipos de retorno Futuro.

Desactivar um ExecutorService

Em geral, o ExecutorService não será automaticamente destruído quando não houver tarefa a processar. Permanecerá vivo e à espera de novo trabalho.

Em alguns casos, isto é muito útil, tais como quando uma aplicação precisa de processar tarefas que aparecem numa base irregular ou a quantidade de tarefa não é conhecida no momento da compilação.

Por outro lado, uma aplicação pode chegar ao seu fim mas não ser parada porque um ExecutorService em espera fará com que a JVM continue a correr.

Para desligar correctamente um ExecutorService, temos as APIs shutdown() e shutdownNow().

O método shutdown() não causa a destruição imediata do ExecutorService. Fará com que o ExecutorService deixe de aceitar novas tarefas e encerre depois de todos os threads em execução terminarem o seu trabalho actual:

executorService.shutdown();

O método shutdownNow() tenta destruir o ExecutorService imediatamente, mas não garante que todos os threads em execução sejam interrompidos ao mesmo tempo:

List<Runnable> notExecutedTasks = executorService.shutDownNow();

Este método devolve uma lista de tarefas que estão à espera de serem processadas. Cabe ao programador decidir o que fazer com estas tarefas.

Uma boa maneira de desligar o ExecutorService (que também é recomendado pela Oracle) é usar ambos os métodos combinados com o método awaitTermination():

executorService.shutdown();try { if (!executorService.awaitTermination(800, TimeUnit.MILLISECONDS)) { executorService.shutdownNow(); } } catch (InterruptedException e) { executorService.shutdownNow();}

Com esta abordagem, o ExecutorService deixará primeiro de executar novas tarefas e depois esperará até um período de tempo especificado para que todas as tarefas sejam concluídas. Se esse tempo expirar, a execução é imediatamente interrompida.

A Interface Futura

Os métodos submit() e invokeAll() devolvem um objecto ou uma colecção de objectos do tipo Futuro, o que nos permite obter o resultado da execução de uma tarefa ou verificar o estado da tarefa (está em execução).

A interface Futura fornece um método de bloqueio especial get(), que retorna um resultado real da execução da tarefa Chamável ou nulo no caso de uma tarefa Executável:

Future<String> future = executorService.submit(callableTask);String result = null;try { result = future.get();} catch (InterruptedException | ExecutionException e) { e.printStackTrace();}

Chamar o método get() enquanto a tarefa ainda está em execução fará com que a execução seja bloqueada até que a tarefa seja executada correctamente e o resultado esteja disponível.

Com um bloqueio muito longo causado pelo método get(), o desempenho de uma aplicação pode degradar-se. Se os dados resultantes não forem cruciais, é possível evitar tal problema usando timeouts:

String result = future.get(200, TimeUnit.MILLISECONDS);

Se o período de execução for mais longo do que o especificado (neste caso, 200 milissegundos), será lançado um TimeoutException.

Podemos usar o método isDone() para verificar se a tarefa atribuída já foi processada ou não.

A futura interface também prevê o cancelamento da execução da tarefa com o método cancel() e a verificação do cancelamento com o método isCancelled():

A interface ScheduledExecutorService

O ScheduledExecutorService executa tarefas após algum atraso pré-definido e/ou periodicamente.

Após uma vez mais, a melhor forma de instanciar um ScheduledExecutorService é utilizar os métodos de fábrica da classe Executors.

Para esta secção, utilizamos um ScheduledExecutorService com uma thread:

ScheduledExecutorService executorService = Executors .newSingleThreadScheduledExecutor();

Para agendar a execução de uma única tarefa após um atraso fixo, utilizar o método scheduled() do ScheduledExecutorService.

Dois métodos scheduled() permitem executar tarefas Executáveis ou Chamáveis:

Future<String> resultFuture = executorService.schedule(callableTask, 1, TimeUnit.SECONDS);

O método scheduleAtFixedRate() permite-nos executar uma tarefa periodicamente após um atraso fixo. O código acima atrasa por um segundo antes de executar a tarefa chamávelTask.

O seguinte bloco de código executará uma tarefa após um atraso inicial de 100 milissegundos. E depois disso, executará a mesma tarefa a cada 450 milissegundos:

Future<String> resultFuture = service .scheduleAtFixedRate(runnableTask, 100, 450, TimeUnit.MILLISECONDS);

Se o processador necessitar de mais tempo para executar uma tarefa atribuída do que o parâmetro de período do método ScheduledAtFixedRate(), o ScheduledExecutorService aguardará até que a tarefa actual esteja concluída antes de iniciar a próxima.

Se for necessário ter um intervalo de tempo fixo entre iterações da tarefa, deve ser utilizado o método ScheduledExecutorServiceWithFixedDelay().

Por exemplo, o seguinte código garantirá uma pausa de 150 milissegundos entre o fim da execução actual e o início de outra:

service.scheduleWithFixedDelay(task, 100, 150, TimeUnit.MILLISECONDS);

De acordo com os contratos do método scheduleAtFixedRate() e scheduleWithFixedDelay(), o período de execução da tarefa terminará no fim do ExecutorService ou se for lançada uma excepção durante a execução da tarefa.

ExecutorService vs Fork/Join

Após o lançamento do Java 7, muitos programadores decidiram substituir a estrutura ExecutorService pela estrutura fork/join.

Esta nem sempre é a decisão correcta, no entanto. Apesar da simplicidade e dos frequentes ganhos de desempenho associados ao fork/join, reduz o controlo do programador sobre a execução concorrente.

ExecutorService dá ao programador a capacidade de controlar o número de threads gerados e a granularidade das tarefas que devem ser executadas por threads separados. O melhor caso de utilização para ExecutorService é o processamento de tarefas independentes, tais como transacções ou pedidos de acordo com o esquema “um fio para uma tarefa”

Em contraste, de acordo com a documentação da Oracle, o garfo/join foi concebido para acelerar o trabalho que pode ser quebrado em peças mais pequenas recursivamente.

Conclusão

Apesar da relativa simplicidade do ExecutorService, existem algumas armadilhas comuns.

P>Vamos resumi-las:

A manter vivo um ExecutorService não utilizado: Ver a explicação detalhada na Secção 4 sobre como desligar um ExecutorService.

Capacidade errada da piscina de roscas enquanto se utiliza uma piscina de roscas de comprimento fixo: É muito importante determinar quantas roscas a aplicação necessitará para executar tarefas de forma eficiente. Um pool de roscas demasiado grande causará sobrecarga desnecessária apenas para criar roscas que estarão na sua maioria no modo de espera. Muito poucos podem fazer com que uma aplicação pareça não responder devido a longos períodos de espera para tarefas na fila.

Método get() Call a Future’s após o cancelamento da tarefa: A tentativa de obter o resultado de uma tarefa já cancelada desencadeia um método CancellationException.

Bloqueio inesperadamente longo com o método get() de Future’s: Devemos usar timeouts para evitar esperas inesperadas.

Como sempre, o código para este artigo está disponível no repositório GitHub.

Comece com Spring 5 e Spring Boot 2, através do curso Learn Spring:

>> VERIFIQUE O CURSO

Deixe uma resposta

O seu endereço de email não será publicado. Campos obrigatórios marcados com *