Introdução
O multithreading foi incorporado ao Java desde o início. Então, vamos dar uma breve olhada nessa coisa chamada multithreading.
Tomamos como ponto de referência a lição oficial da Oracle: "
Lesson: The "Hello World!" Application ". Vamos alterar um pouco o código do nosso programa Hello World da seguinte forma:
class HelloWorldApp {
public static void main(String[] args) {
System.out.println("Hello, " + args[0]);
}
}
args
é uma matriz de parâmetros de entrada passados quando o programa é iniciado. Salve este código em um arquivo com um nome que corresponda ao nome da classe e tenha a extensão
.java
. Compile-o usando o utilitário
javacjavac HelloWorldApp.java
: . Então, rodamos nosso código com algum parâmetro, por exemplo, "Roger":
java HelloWorldApp Roger
Nosso código atualmente está com uma falha grave. Se você não passar nenhum argumento (ou seja, executar apenas "java HelloWorldApp"), obteremos um erro:
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 0
at HelloWorldApp.main(HelloWorldApp.java:3)
Ocorreu uma exceção (ou seja, um erro) no thread denominado "principal". Então, Java tem threads? É aqui que nossa jornada começa.
Java e tópicos
Para entender o que é um thread, você precisa entender como um programa Java é iniciado. Vamos alterar nosso código da seguinte forma:
class HelloWorldApp {
public static void main(String[] args) {
while (true) {
// Do nothing
}
}
}
Agora vamos compilá-lo novamente com
javac
. Por conveniência, executaremos nosso código Java em uma janela separada. No Windows, isso pode ser feito assim:
start java HelloWorldApp
. Agora usaremos o utilitário
jps para ver quais informações o Java pode nos fornecer:
O primeiro número é o PID ou ID do processo. O que é um processo?
A process is a combination of code and data sharing a common virtual address space.
Com os processos, diferentes programas são isolados uns dos outros enquanto são executados: cada aplicativo usa sua própria área na memória sem interferir em outros programas. Para saber mais, recomendo a leitura deste tutorial:
Processos e Threads . Um processo não pode existir sem um thread, portanto, se um processo existir, ele terá pelo menos um thread. Mas como isso acontece em Java? Quando iniciamos um programa Java, a execução começa com o
main
método. É como se estivéssemos entrando no programa, então esse
main
método especial é chamado de ponto de entrada. O
main
método deve ser sempre "public static void", para que a máquina virtual Java (JVM) possa começar a executar nosso programa. Para obter mais informações,
Por que o método principal Java é estático?. Acontece que o iniciador Java (java.exe ou javaw.exe) é um aplicativo C simples: ele carrega as várias DLLs que realmente compõem a JVM. O ativador Java faz um conjunto específico de chamadas Java Native Interface (JNI). JNI é um mecanismo para conectar o mundo da máquina virtual Java com o mundo do C++. Portanto, o iniciador não é a própria JVM, mas sim um mecanismo para carregá-la. Ele conhece os comandos corretos a serem executados para iniciar a JVM. Ele sabe como usar chamadas JNI para configurar o ambiente necessário. A configuração desse ambiente inclui a criação do thread principal, que é chamado de "principal", é claro. Para ilustrar melhor quais threads existem em um processo Java, usamos o
jvisualvmferramenta, que está incluída no JDK. Conhecendo o pid de um processo, podemos ver imediatamente informações sobre esse processo:
jvisualvm --openpid <process id>
Curiosamente, cada thread tem sua própria área separada na memória alocada para o processo. Essa estrutura de memória é chamada de pilha. Uma pilha consiste em quadros. Um quadro representa a ativação de um método (uma chamada de método inacabada). Um quadro também pode ser representado como um StackTraceElement (consulte a API Java para
StackTraceElement ). Você pode encontrar mais informações sobre a memória alocada para cada thread na discussão aqui: "
Como Java (JVM) aloca pilha para cada thread ". Se você observar a
API Java e pesquisar a palavra "Thread", encontrará o
java.lang.Threadaula. Esta é a classe que representa um thread em Java e precisaremos trabalhar com ela.
java.lang.Thread
Em Java, um thread é representado por uma instância da
java.lang.Thread
classe. Você deve entender imediatamente que as instâncias da classe Thread não são threads de execução. Este é apenas um tipo de API para os encadeamentos de baixo nível gerenciados pela JVM e pelo sistema operacional. Quando iniciamos a JVM usando o iniciador Java, ela cria um
main
encadeamento chamado "principal" e alguns outros encadeamentos de manutenção. Conforme declarado no JavaDoc para a classe Thread:
When a Java Virtual Machine starts up, there is usually a single non-daemon thread
. Existem 2 tipos de threads: daemons e não-daemons. Os encadeamentos daemon são encadeamentos de segundo plano (manutenção) que realizam algum trabalho em segundo plano. A palavra "daemon" refere-se ao demônio de Maxwell. Você pode aprender mais neste
artigo da Wikipédia . Conforme declarado na documentação, a JVM continua executando o programa (processo) até:
- O método Runtime.exit() é chamado
- Todos os threads NÃO-daemon terminam seu trabalho (sem erros ou com exceções lançadas)
Um detalhe importante decorre disso: os encadeamentos do daemon podem ser encerrados a qualquer momento. Como resultado, não há garantias sobre a integridade de seus dados. Conseqüentemente, os encadeamentos daemon são adequados para certas tarefas de manutenção. Por exemplo, Java possui uma thread que é responsável por processar
finalize()
chamadas de métodos, ou seja, threads envolvidas com o Garbage Collector (gc). Cada thread faz parte de um grupo (
ThreadGroup ). E os grupos podem fazer parte de outros grupos, formando uma certa hierarquia ou estrutura.
public static void main(String[] args) {
Thread currentThread = Thread.currentThread();
ThreadGroup threadGroup = currentThread.getThreadGroup();
System.out.println("Thread: " + currentThread.getName());
System.out.println("Thread Group: " + threadGroup.getName());
System.out.println("Parent Group: " + threadGroup.getParent().getName());
}
Os grupos trazem ordem ao gerenciamento de encadeamentos. Além dos grupos, os threads têm seu próprio manipulador de exceções. Dê uma olhada em um exemplo:
public static void main(String[] args) {
Thread th = Thread.currentThread();
th.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.println("An error occurred: " + e.getMessage());
}
});
System.out.println(2/0);
}
A divisão por zero causará um erro que será detectado pelo manipulador. Se você não especificar seu próprio manipulador, a JVM invocará o manipulador padrão, que produzirá o rastreamento de pilha da exceção para StdError. Cada thread também tem uma prioridade. Você pode ler mais sobre prioridades neste artigo:
Java Thread Priority in Multithreading .
Criando um tópico
Conforme declarado na documentação, temos 2 maneiras de criar um thread. A primeira maneira é criar sua própria subclasse. Por exemplo:
public class HelloWorld{
public static class MyThread extends Thread {
@Override
public void run() {
System.out.println("Hello, World!");
}
}
public static void main(String[] args) {
Thread thread = new MyThread();
thread.start();
}
}
Como você pode ver, o trabalho da tarefa acontece no
run()
método, mas o próprio thread é iniciado no
start()
método. Não confunda esses métodos: se chamarmos o
un()
método r diretamente, nenhum novo thread será iniciado. É o
start()
método que solicita à JVM a criação de um novo thread. Esta opção onde herdamos Thread já é ruim porque incluímos Thread em nossa hierarquia de classes. A segunda desvantagem é que estamos começando a violar o princípio da "responsabilidade única". Ou seja, nossa classe é simultaneamente responsável por controlar a thread e por alguma tarefa a ser executada nesta thread. Qual é o caminho certo? A resposta é encontrada no mesmo
run()
método, que sobrescrevemos:
public void run() {
if (target != null) {
target.run();
}
}
Aqui
target
está algum
java.lang.Runnable
, que podemos passar ao criar uma instância da classe Thread. Isso significa que podemos fazer isso:
public class HelloWorld{
public static void main(String[] args) {
Runnable task = new Runnable() {
public void run() {
System.out.println("Hello, World!");
}
};
Thread thread = new Thread(task);
thread.start();
}
}
Runnable
também tem sido uma interface funcional desde o Java 1.8. Isso torna possível escrever um código ainda mais bonito para a tarefa de um thread:
public static void main(String[] args) {
Runnable task = () -> {
System.out.println("Hello, World!");
};
Thread thread = new Thread(task);
thread.start();
}
Conclusão
Espero que esta discussão esclareça o que é um thread, como os threads surgem e quais operações básicas podem ser executadas com threads. Na
próxima parte , tentaremos entender como os threads interagem uns com os outros e exploraremos o ciclo de vida do thread.
Melhor juntos: Java e a classe Thread. Parte II — Sincronização melhor juntos: Java e a classe Thread. Parte III — Interação melhor juntos: Java e a classe Thread. Parte IV — Callable, Future e amigos Melhor juntos: Java e a classe Thread. Parte V — Executor, ThreadPool, Fork/Join Better juntos: Java e a classe Thread. Parte VI — Atire!
GO TO FULL VERSION