Como iniciar um thread em Java
1. Introdução
Neste tutorial, vamos explorar diferentes maneiras de iniciar um thread e executar tarefas paralelas.
This is very useful, in particular when dealing with long or recurring operations that can’t run on the main thread, ou onde a interação da IU não pode ser colocada em espera enquanto aguarda os resultados da operação.
Para aprender mais sobre os detalhes dos threads, definitivamente leia nosso tutorial sobre oLife Cycle of a Thread in Java.
2. Noções básicas de execução de um thread
Podemos facilmente escrever alguma lógica que seja executada em uma thread paralela usando a estruturaThread.
Vamos tentar um exemplo básico, estendendo a classeThread:
public class NewThread extends Thread {
public void run() {
long startTime = System.currentTimeMillis();
int i = 0;
while (true) {
System.out.println(this.getName() + ": New Thread is running..." + i++);
try {
//Wait for one sec so it doesn't print too fast
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
...
}
}
}
E agora escrevemos uma segunda classe para inicializar e iniciar nosso thread:
public class SingleThreadExample {
public static void main(String[] args) {
NewThread t = new NewThread();
t.start();
}
}
Agora, vamos supor que precisamos iniciar vários threads:
public class MultipleThreadsExample {
public static void main(String[] args) {
NewThread t1 = new NewThread();
t1.setName("MyThread-1");
NewThread t2 = new NewThread();
t2.setName("MyThread-2");
t1.start();
t2.start();
}
}
Nosso código ainda parece bastante simples e muito semelhante aos exemplos que podemos encontrar online.
Claro,this is far from production-ready code, where it’s of critical importance to manage resources in the correct way, to avoid too much context switching or too much memory usage.
So, to get production-ready we now need to write additional boilerplate para lidar com:
-
a criação consistente de novos threads
-
o número de threads ativos simultâneos
-
a desalocação de threads: muito importante para threads daemon, a fim de evitar vazamentos
Se quisermos, podemos escrever nosso próprio código para todos esses cenários e até mais, mas por que devemos reinventar a roda?
3. A estruturaExecutorService
OExecutorService implementa o padrão de design Thread Pool (também chamado de modelo de trabalhador replicado ou de equipe de trabalho) e cuida do gerenciamento de thread que mencionamos acima, além de adicionar alguns recursos muito úteis, como reutilização de thread e filas de tarefas.
A reutilização de threads, em particular, é muito importante: em um aplicativo de grande escala, alocar e desalocar muitos objetos de thread cria uma sobrecarga significativa de gerenciamento de memória.
Com threads de trabalho, minimizamos a sobrecarga causada pela criação de thread.
Para facilitar a configuração do pool,ExecutorService vem com um construtor fácil e algumas opções de personalização, como o tipo de fila, o número mínimo e máximo de threads e sua convenção de nomenclatura.
Para obter mais detalhes sobreExecutorService,, leia nossoGuide to the Java ExecutorService.
4. Iniciando uma tarefa com executores
Graças a esta estrutura poderosa, podemos mudar nossa mentalidade de iniciar tópicos para enviar tarefas.
Vejamos como podemos enviar uma tarefa assíncrona para nosso executor:
ExecutorService executor = Executors.newFixedThreadPool(10);
...
executor.submit(() -> {
new Task();
});
Existem dois métodos que podemos usar:execute, que não retorna nada, esubmit, que retorna umFuture encapsulando o resultado do cálculo.
Para obter mais informações sobreFutures,, leia nossoGuide to java.util.concurrent.Future.
5. Iniciando uma tarefa comCompletableFutures
Para recuperar o resultado final de um objetoFuture, podemos usar o métodoget disponível no objeto, mas isso bloquearia o thread pai até o final do cálculo.
Como alternativa, poderíamos evitar o bloqueio adicionando mais lógica à nossa tarefa, mas precisamos aumentar a complexidade do nosso código.
Java 1.8 introduziu uma nova estrutura no topo da construçãoFuture para trabalhar melhor com o resultado do cálculo: oCompletableFuture.
CompletableFuture implementaCompletableStage, que adiciona uma vasta seleção de métodos para anexar callbacks e evitar todo o encanamento necessário para executar operações no resultado depois que ele estiver pronto
A implementação para enviar uma tarefa é muito mais simples:
CompletableFuture.supplyAsync(() -> "Hello");
supplyAsync leva umSupplier contendo o código que queremos executar de forma assíncrona - em nosso caso, o parâmetro lambda.
A tarefa agora é submetida implicitamente aoForkJoinPool.commonPool(), ou podemos especificar oExecutor que preferimos como um segundo parâmetro.
Para saber mais sobreCompletableFuture,, leia nossoGuide To CompletableFuture.
6. Execução de tarefas atrasadas ou periódicas
Ao trabalhar com aplicativos da web complexos, podemos precisar executar tarefas em horários específicos, talvez regularmente.
Java possui poucas ferramentas que podem nos ajudar a executar operações atrasadas ou recorrentes:
-
java.util.Timer
-
java.util.concurrent.ScheduledThreadPoolExecutor
6.1. Timer
Timer é um recurso para agendar tarefas para execução futura em um thread de segundo plano.
As tarefas podem ser agendadas para execução única ou repetida em intervalos regulares.
Vamos ver como fica o código se quisermos executar uma tarefa após um segundo de atraso:
TimerTask task = new TimerTask() {
public void run() {
System.out.println("Task performed on: " + new Date() + "n"
+ "Thread's name: " + Thread.currentThread().getName());
}
};
Timer timer = new Timer("Timer");
long delay = 1000L;
timer.schedule(task, delay);
Agora vamos adicionar uma programação recorrente:
timer.scheduleAtFixedRate(repeatedTask, delay, period);
Desta vez, a tarefa será executada após o atraso especificado e será recorrente após o período de tempo passado.
Para obter mais informações, leia nosso guia paraJava Timer.
6.2. ScheduledThreadPoolExecutor
ScheduledThreadPoolExecutor tem métodos semelhantes à classeTimer:
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(2);
ScheduledFuture
Para finalizar nosso exemplo, usamosscheduleAtFixedRate() para tarefas recorrentes:
ScheduledFuture
O código acima executará uma tarefa após um atraso inicial de 100 milissegundos e, depois disso, executará a mesma tarefa a cada 450 milissegundos.
Se o processador não conseguir terminar o processamento da tarefa a tempo antes da próxima ocorrência, oScheduledExecutorService irá esperar até que a tarefa atual seja concluída, antes de iniciar a próxima.
Para evitar esse tempo de espera, podemos usarscheduleWithFixedDelay(), que, conforme descrito por seu nome, garante um atraso de comprimento fixo entre as iterações da tarefa.
Para obter mais detalhes sobreScheduledExecutorService,, leia nossoGuide to the Java ExecutorService.
6.3. Qual ferramenta é melhor?
Se executarmos os exemplos acima, o resultado do cálculo parecerá o mesmo.
Então,how do we choose the right tool?
Quando uma estrutura oferece várias opções, é importante entender a tecnologia subjacente para tomar uma decisão informada.
Vamos tentar mergulhar um pouco mais fundo sob o capô.
Timer:
-
não oferece garantias em tempo real: ele agenda tarefas usando o métodoObject.wait(long)
-
há um único thread de segundo plano, então as tarefas são executadas sequencialmente e uma tarefa de longa duração pode atrasar outras
-
exceções de tempo de execução lançadas emTimerTask matariam o único encadeamento disponível, matando assimTimer
ScheduledThreadPoolExecutor:
-
pode ser configurado com qualquer número de threads
-
pode tirar proveito de todos os núcleos de CPU disponíveis
-
captura exceções de tempo de execução e nos permite tratá-las se quisermos (substituindo o métodoafterExecute deThreadPoolExecutor)
-
cancela a tarefa que lançou a exceção, enquanto permite que outros continuem executando
-
confia no sistema de agendamento do SO para acompanhar fusos horários, atrasos, hora solar etc.
-
fornece API colaborativa se precisarmos de coordenação entre várias tarefas, como aguardar a conclusão de todas as tarefas enviadas
-
fornece melhor API para gerenciamento do ciclo de vida do encadeamento
A escolha agora é óbvia, certo?
7. Diferença entreFuture eScheduledFuture
Em nossos exemplos de código, podemos observar queScheduledThreadPoolExecutor retorna um tipo específico deFuture:ScheduledFuture.
ScheduledFuture extends as interfacesFutureeDelayed, herdando assim o método adicionalgetDelay que retorna o atraso restante associado à tarefa atual. É estendido porRunnableScheduledFuture que adiciona um método para verificar se a tarefa é periódica.
ScheduledThreadPoolExecutor implementa todas essas construções por meio da classe internaScheduledFutureTask e as usa para controlar o ciclo de vida da tarefa.
8. Conclusões
Neste tutorial, experimentamos as diferentes estruturas disponíveis para iniciar threads e executar tarefas em paralelo.
Em seguida, aprofundamos as diferenças entreTimer eScheduledThreadPoolExecutor.
O código-fonte do artigo está disponívelover on GitHub.