Rosqueamento refere-se à prática de executar processos de programação simultaneamente para melhorar o desempenho do aplicativo. Embora não seja tão comum trabalhar com threads diretamente em aplicativos de negócios, eles são usados o tempo todo em estruturas Java. Por exemplo, frameworks que processam um grande volume de informações utilizam threads para gerenciar dados. A manipulação simultânea de threads ou processos de CPU melhora o desempenho, resultando em programas mais rápidos e eficientes.
Este artigo apresenta alguns conceitos básicos de threads Java tradicionais e execução de threads na máquina virtual Java. Consulte a introdução do InfoWorld ao Project Loom para aprender sobre threads virtuais e o novo modelo de simultaneidade estruturada do Java.
Encontre seu primeiro tópico: método main() do Java
Mesmo que você nunca tenha trabalhado diretamente com threads Java, você trabalhou indiretamente com eles porque o método main() do Java contém um Thread principal. Sempre que você executou o main()
método, você também executou o método principal Thread
.
Estudando o Thread
class é muito útil para entender como o threading funciona em programas Java. Podemos acessar o thread que está sendo executado invocando o currentThread().getName()
método, conforme mostrado aqui:
public class MainThread {
public static void main(String... mainThread) {
System.out.println(Thread.currentThread().getName());
}
}
Este código imprimirá “main”, identificando o thread que está sendo executado no momento. Saber identificar o thread que está sendo executado é o primeiro passo para absorver os conceitos do thread.
O ciclo de vida do thread Java
Ao trabalhar com threads, é fundamental estar ciente do estado do thread. O ciclo de vida do thread Java consiste em seis estados de thread:
- Novo: Um novo
Thread()
foi instanciado. - Executável: O
Thread
destart()
método foi invocado. - Correndo: O
start()
O método foi invocado e o thread está em execução. - Suspenso: o thread está temporariamente suspenso e pode ser retomado por outro thread.
- Bloqueado: o thread está aguardando uma oportunidade para ser executado. Isso acontece quando um thread já invocou o
synchronized()
método e o próximo thread deve esperar até terminar. - Terminado: a execução do thread foi concluída.
Há mais para explorar e entender sobre estados de thread, mas as informações na Figura 1 são suficientes por enquanto.
Estendendo uma classe Thread
Na sua forma mais simples, o processamento simultâneo é feito estendendo um Thread
classe, como mostrado aqui:
public class InheritingThread extends Thread {
InheritingThread(String threadName) {
super(threadName);
}
public static void main(String... inheriting) {
System.out.println(Thread.currentThread().getName() + " is running");
new InheritingThread("inheritingThread").start();
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " is running");
}
}
Aqui, estamos executando dois threads: o MainThread
e a InheritingThread
. Quando invocamos o start()
método com o novo inheritingThread()
a lógica do run()
método é executado.
Passamos também o nome do segundo thread no Thread
construtor de classe, então a saída será:
main is running.
inheritingThread is running.
A interface executável
Em vez de usar herança, você poderia implementar a interface Runnable. Passagem Runnable
dentro de um Thread
construtor resulta em menos acoplamento e mais flexibilidade. Depois de passar Runnable
podemos invocar o start()
método exatamente como fizemos no exemplo anterior:
public class RunnableThread implements Runnable {
public static void main(String... runnableThread) {
System.out.println(Thread.currentThread().getName());
new Thread(new RunnableThread()).start();
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
Threads não-daemon vs.
Em termos de execução, existem dois tipos de threads:
- Threads não-daemon são executados até o final. O thread principal é um bom exemplo de thread não-daemon. Código em
main()
sempre será executado até o final, a menos que umSystem.exit()
força o programa a ser concluído. - A thread daemon é o oposto, basicamente um processo que não precisa ser executado até o final.
Lembre-se da regra: se um encadeamento não-daemon envolvente terminar antes de um encadeamento daemon, o encadeamento daemon não será executado até o final.
Para entender melhor o relacionamento entre threads daemon e não-daemon, estude este exemplo:
import java.util.stream.IntStream;
public class NonDaemonAndDaemonThread {
public static void main(String... nonDaemonAndDaemon) throws InterruptedException {
System.out.println("Starting the execution in the Thread " + Thread.currentThread().getName());
Thread daemonThread = new Thread(() -> IntStream.rangeClosed(1, 100000)
.forEach(System.out::println));
daemonThread.setDaemon(true);
daemonThread.start();
Thread.sleep(10);
System.out.println("End of the execution in the Thread " +
Thread.currentThread().getName());
}
}
Neste exemplo, usei um thread daemon para declarar um intervalo de 1 a 100.000, iterar todos eles e depois imprimir. Mas lembre-se, um thread daemon não completará a execução se o thread principal do não-daemon terminar primeiro.
A saída procederá da seguinte forma:
- Início da execução no thread principal.
- Imprima números de 1 a possivelmente 100.000.
- Fim da execução no thread principal, muito provavelmente antes da iteração para 100.000 conclusões.
O resultado final dependerá da implementação da sua JVM.
Como você pode ver, os threads são imprevisíveis.
Prioridade de thread e JVM
É possível priorizar a execução do thread com o setPriority
método, mas, novamente, como ele é tratado depende da implementação da JVM. Linux, macOS e Windows têm diferentes implementações de JVM e cada um lidará com a prioridade de thread de acordo com os padrões.
Entretanto, a prioridade do thread que você define influencia a ordem de invocação do thread. As três constantes declaradas no Thread
classe são:
/**
* The minimum priority that a thread can have.
*/
public static final int MIN_PRIORITY = 1;
/**
* The default priority that is assigned to a thread.
*/
public static final int NORM_PRIORITY = 5;
/**
* The maximum priority that a thread can have.
*/
public static final int MAX_PRIORITY = 10;
Tente executar testes no código a seguir para ver qual prioridade de execução você obtém:
public class ThreadPriority {
public static void main(String... threadPriority) {
Thread moeThread = new Thread(() -> System.out.println("Moe"));
Thread barneyThread = new Thread(() -> System.out.println("Barney"));
Thread homerThread = new Thread(() -> System.out.println("Homer"));
moeThread.setPriority(Thread.MAX_PRIORITY);
barneyThread.setPriority(Thread.NORM_PRIORITY);
homerThread.setPriority(Thread.MIN_PRIORITY);
homerThread.start();
barneyThread.start();
moeThread.start();
}
}
Mesmo se definirmos moeThread
como MAX_PRIORITY
, não podemos contar com que esse thread seja executado primeiro. Em vez disso, a ordem de execução será aleatória.
Uma nota sobre constantes vs enums
O Thread
classe foi introduzida com a primeira versão do Java. Naquela época, as prioridades eram definidas usando constantes, não enums. Porém, há um problema com o uso de constantes: se passarmos um número de prioridade que não esteja no intervalo de 1 a 10, o setPriority()
método lançará uma IllegalArgumentException. Hoje, podemos usar enums para contornar esse problema. O uso de enums impossibilita a passagem de um argumento ilegal, o que simplifica o código e nos dá mais controle sobre sua execução.
O que lembrar sobre threads Java
- Invoque o
start()
método para iniciar umThread
. - É possível estender o
Thread
classe diretamente para usar threads. - É possível implementar uma ação de thread dentro de um
Runnable
interface. - A prioridade do thread depende da implementação da JVM.
- O comportamento do thread também depende da implementação da JVM.
- Um thread daemon não será concluído se um thread não-daemon envolvente terminar primeiro.
Erros comuns com threads Java
- Invocando o
run()
método não é a maneira de iniciar um novo thread. - Tentar iniciar um tópico duas vezes causará um
IllegalThreadStateException
. - Evite permitir que vários processos alterem o estado de um objeto.
- Não escreva lógica de programa que dependa da prioridade do thread (você não pode prever isso).
- Não confie na ordem de execução do thread – mesmo se você iniciar um thread primeiro, não há garantia de que ele será executado primeiro.
Aceite o desafio dos threads Java!
Você aprendeu apenas algumas coisas sobre threads Java, então vamos tentar um desafio Java para testar o que você aprendeu.
public class ThreadChallenge {
private static int wolverineAdrenaline = 10;
public static void main(String... doYourBest) {
new Motorcycle("Harley Davidson").start();
Motorcycle fastBike = new Motorcycle("Dodge Tomahawk");
fastBike.setPriority(Thread.MAX_PRIORITY);
fastBike.setDaemon(false);
fastBike.start();
Motorcycle yamaha = new Motorcycle("Yamaha YZF");
yamaha.setPriority(Thread.MIN_PRIORITY);
yamaha.start();
}
static class Motorcycle extends Thread {
Motorcycle(String bikeName) { super(bikeName); }
@Override public void run() {
wolverineAdrenaline++;
if (wolverineAdrenaline == 13) {
System.out.println(this.getName());
}
}
}
}
Qual você acha que será a saída deste código? Aqui estão as opções:
A.Harley Davidson
B. Dodge Tomahawk
C. Yamaha YZF
D. Indeterminado
Resolvendo o desafio
No código acima, criamos três threads. O primeiro tópico é Harley Davidson
e atribuímos a esse thread a prioridade padrão. O segundo tópico é Dodge Tomahawk
atribuído MAX_PRIORITY
. O terceiro é Yamaha YZF
com MIN_PRIORITY
. Então começamos os tópicos.
Para determinar a ordem em que os threads serão executados, você pode primeiro observar que o Motorcycle
classe estende o Thread
class e que passamos o nome do thread no construtor. Também substituímos o run()
método com uma condição: if (wolverineAdrenaline == 13)
.
Embora Yamaha YZF
é o terceiro thread em nossa ordem de execução e tem MIN_PRIORITY
não há garantia de que ele será executado por último para todas as implementações de JVM.
Você também pode notar que neste exemplo definimos o Dodge Tomahawk
fio como daemon
. Porque é um thread daemon, Dodge Tomahawk
pode nunca concluir a execução. Mas os outros dois threads não são daemon por padrão, então o Harley Davidson
e Yamaha YZF
threads definitivamente completarão sua execução.
Para finalizar, o resultado será D: Indeterminado. Isso ocorre porque não há garantia de que o agendador de threads seguirá nossa ordem de execução ou prioridade de thread.
Lembre-se, não podemos confiar na lógica do programa (ordem dos threads ou prioridade dos threads) para prever a ordem de execução da JVM.
Desafio de vídeo! Depurando argumentos variáveis
A depuração é uma das maneiras mais fáceis de absorver totalmente os conceitos de programação e, ao mesmo tempo, melhorar seu código. Neste vídeo você pode acompanhar enquanto eu depuro e explico o desafio do comportamento do thread: