Introdução
O multithreading foi incorporado ao Java desde seus primeiros dias. Então, vamos dar uma olhada rápida no que é multithreading.
Vamos tomar como ponto de partida a lição oficial da Oracle: "
Lição: A aplicação "Hello World!" ". Vamos mudar um pouco o código da nossa aplicação Hello World para o seguinte:
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. Vamos salvar esse código em um arquivo com um nome que corresponda ao nome da classe e à extensão
.java
. Vamos compilar usando o utilitário
javac :
javac HelloWorldApp.java
Depois disso, chame nosso código com algum parâmetro, por exemplo, Roger:
java HelloWorldApp Roger
Nosso código agora tem uma falha séria. Se não passarmos nenhum argumento (ou seja, apenas executar 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) em um thread chamado
main
. Acontece que existe algum tipo de thread em Java? É aqui que nossa jornada começa.
Java e threads
Para entender o que é um thread, você precisa entender como um aplicativo Java é iniciado. Vamos alterar nosso código da seguinte forma:
class HelloWorldApp {
public static void main(String[] args) {
while (true) {
}
}
}
Agora vamos compilá-lo novamente usando javac. A seguir, por conveniência, executaremos nosso código Java em uma janela separada. No Windows você pode fazer assim:
start java HelloWorldApp
. Agora, usando o utilitário
jps , vamos ver quais informações o Java nos dirá:
O primeiro número é o PID ou Process ID, o identificador do processo. O que é um processo?
Процесс — это совокупность codeа и данных, разделяющих общее виртуальное addressное пространство.
Com a ajuda de processos, a execução de diferentes programas é isolada umas das outras: cada aplicação utiliza sua própria área de memória sem interferir em outros programas. Aconselho você a ler o artigo com mais detalhes: "
https://habr.com/post/164487/ ". Um processo não pode existir sem threads, portanto, se existe um processo, pelo menos um thread existe nele. Como isso acontece em Java? Quando executamos um programa Java, sua execução começa com a extensão
main
. Nós meio que entramos no programa, então esse método especial
main
é chamado de ponto de entrada, ou “ponto de entrada”. O método
main
deve ser sempre
public static void
tal que a Java Virtual Machine (JVM) possa começar a executar nosso programa. Consulte "
Por que o método principal Java é estático? " para obter mais detalhes. Acontece que o launcher java (java.exe ou javaw.exe) é um aplicativo simples (aplicativo C simples): ele carrega diversas DLLs, que na verdade são a JVM. O iniciador Java faz um conjunto específico de chamadas Java Native Interface (JNI). JNI é o mecanismo que une o mundo da Java Virtual Machine e o mundo do C++. Acontece que o launcher não é a JVM, mas sim o seu carregador. Ele conhece os comandos corretos a serem executados para iniciar a JVM. Sabe organizar todo o ambiente necessário utilizando chamadas JNI. Essa organização do ambiente também inclui a criação de uma thread principal, que normalmente é chamada de
main
. Para ver mais claramente quais threads residem em um processo java, usamos o programa
jvisualvm , que está incluído no JDK. Conhecendo o pid de um processo, podemos abrir dados sobre ele imediatamente:
jvisualvm --openpid айдипроцесса
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 é o ponto de chamada de um método, ponto de execução. Um quadro também pode ser representado como StackTraceElement (consulte API Java para
StackTraceElement ). Você pode ler mais sobre a memória alocada para cada thread
aqui . Se olharmos
a API Java e procurarmos a palavra Thread, veremos que existe uma classe
java.lang.Thread . É essa classe que representa um stream em Java, e é com ela que temos que trabalhar.
java.lang.Thread
Um thread em Java é representado como uma instância da classe
java.lang.Thread
. Vale a pena entender imediatamente que as instâncias da classe Thread em Java não são threads em si. Este é apenas um tipo de API para threads de baixo nível gerenciados pela JVM e pelo sistema operacional. Quando iniciamos a JVM usando o iniciador Java, ela cria um thread principal com um nome
main
e vários outros threads de serviço. Conforme declarado no JavaDoc da 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. Threads daemon são threads de segundo plano (threads de serviço) que realizam algum trabalho em segundo plano. Este termo interessante é uma referência ao “demônio de Maxwell”, sobre o qual você pode ler mais no artigo da Wikipedia sobre “
demônios ”. Conforme indicado na documentação, a JVM continua executando o programa (processo) até:
- O método Runtime.exit não é chamado
- Todos os threads não-daemon concluíram seu trabalho (sem erros e com exceções lançadas)
Daí o detalhe importante: threads daemon podem ser encerrados em qualquer comando sendo executado. Portanto, a integridade dos dados neles contidos não é garantida. Portanto, os threads daemon são adequados para algumas tarefas de serviço. Por exemplo, em Java existe um thread que é responsável por processar métodos de finalização ou threads relacionados ao Garbage Collector (GC). Cada thread pertence a algum grupo (
ThreadGroup ). E os grupos podem entrar uns nos outros, formando alguma 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 permitem agilizar o gerenciamento dos fluxos e acompanhá-los. Além dos grupos, os threads possuem seu próprio manipulador de exceções. Vejamos 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 o manipulador, a implementação do manipulador padrão funcionará, exibindo a pilha de erros em StdError. Você pode ler mais na revisão
http://pro-java.ru/java-dlya-opytnyx/obrabotchik-neperexvachennyx-isklyuchenij-java/ ". Além disso, o tópico tem uma prioridade. Você pode ler mais sobre prioridades no artigo "
Prioridade de thread Java em multithreading ".
Criando um tópico
Conforme declarado na documentação, temos 2 maneiras de criar um thread. A primeira é criar seu próprio herdeiro. 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, a tarefa é iniciada no método
run
e o thread é iniciado no método
start
. Eles não devem ser confundidos, porque... se executarmos o método
run
diretamente, nenhum novo thread será iniciado. É o método
start
que pede à JVM para criar um novo thread. A opção com descendente de Thread é ruim porque incluímos Thread na hierarquia de classes. A segunda desvantagem é que estamos começando a violar o princípio da “Responsabilidade Exclusiva” SOLID, porque nossa classe se torna simultaneamente responsável por gerenciar o thread e por alguma tarefa que deve ser executada neste thread. Qual é correto? A resposta está no próprio método
run
que substituímos:
public void run() {
if (target != null) {
target.run();
}
}
Aqui
target
estão alguns
java.lang.Runnable
que podemos passar para Thread ao criar uma instância da classe. Portanto, 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();
}
}
Também é
Runnable
uma interface funcional desde Java 1.8. Isso permite que você escreva código de tarefa para threads de maneira ainda mais bonita:
public static void main(String []args){
Runnable task = () -> {
System.out.println("Hello, World!");
};
Thread thread = new Thread(task);
thread.start();
}
Total
Então, espero que com esta história fique claro o que é um stream, como eles existem e quais operações básicas podem ser realizadas com eles. Na
próxima parte , vale entender como as threads interagem entre si e qual é o seu ciclo de vida. #Viacheslav
GO TO FULL VERSION