Dominando Multithreading em Java: Programação Concorrente Eficiente
Java é uma linguagem de programação multi-threaded, o que significa que podemos desenvolver programas multi-threaded usando Java. Um programa multi-threaded contém duas ou mais partes que podem ser executadas concorrentemente e cada parte pode lidar com uma tarefa diferente ao mesmo tempo, fazendo uso ideal dos recursos disponíveis, especialmente quando seu computador possui múltiplas CPUs.
Por definição, multitarefa é quando vários processos compartilham recursos de processamento comuns, como uma CPU. O multi-threading estende a ideia de multitarefa para aplicações onde você pode subdividir operações específicas dentro de uma única aplicação em threads individuais. Cada uma das threads pode ser executada em paralelo. O SO divide o tempo de processamento não apenas entre diferentes aplicações, mas também entre cada thread dentro de uma aplicação.
O multi-threading permite que você escreva de forma que múltiplas atividades possam prosseguir concorrentemente no mesmo programa.
Ciclo de Vida de uma Thread
Uma thread passa por várias etapas em seu ciclo de vida. Por exemplo, uma thread nasce, é iniciada, executa e depois morre. O diagrama a seguir mostra o ciclo de vida completo de uma thread.
As seguintes são as etapas do ciclo de vida:
New (Nova) - Uma nova thread começa seu ciclo de vida no estado novo. Ela permanece neste estado até que o programa inicie a thread. Também é referida como uma thread nascida.
Runnable (Executável) - Após uma thread recém-nascida ser iniciada, a thread torna-se executável. Uma thread neste estado é considerada como executando sua tarefa.
Waiting (Aguardando) - Às vezes, uma thread transiciona para o estado de espera enquanto a thread espera que outra thread execute uma tarefa. Uma thread transiciona de volta para o estado executável apenas quando outra thread sinaliza a thread em espera para continuar a execução.
Timed Waiting (Aguardando por Tempo Determinado) - Uma thread executável pode entrar no estado de espera temporizada por um intervalo de tempo especificado. Uma thread neste estado transiciona de volta para o estado executável quando esse intervalo de tempo expira ou quando o evento que ela está esperando ocorre.
Terminated/Dead (Terminada/Morta) - Uma thread executável entra no estado terminado quando completa sua tarefa ou termina de outra forma.
Prioridades de Thread
Toda thread Java tem uma prioridade que ajuda o sistema operacional a determinar a ordem em que as threads são agendadas.
As prioridades de thread Java estão no intervalo entre MIN_PRIORITY (uma constante de 1) e MAX_PRIORITY (uma constante de 10). Por padrão, a cada thread é dada a prioridade NORM_PRIORITY (uma constante de 5).
Threads com prioridade mais alta são mais importantes para um programa e devem receber tempo de processador antes de threads de prioridade mais baixa. No entanto, as prioridades de thread não podem garantir a ordem em que as threads são executadas e são muito dependentes da plataforma.
Criar uma Thread Implementando a Interface Runnable
Se sua classe se destina a ser executada como uma thread, você pode conseguir isso implementando a interface Runnable. Você precisará seguir três passos básicos:
Passo 1
Como primeiro passo, você precisa implementar um método run() fornecido pela interface Runnable. Este método fornece um ponto de entrada para a thread e você colocará sua lógica de negócios completa dentro deste método. A seguir está uma sintaxe simples do método run():
public void run()
Passo 2
Como segundo passo, você instanciará um objeto Thread usando o seguinte construtor:
Thread(Runnable threadObj, String threadName);
Onde, threadObj é uma instância de uma classe que implementa a interface Runnable e threadName é o nome dado à nova thread.
Passo 3
Uma vez que um objeto Thread é criado, você pode iniciá-lo chamando o método start(), que executa uma chamada ao método run(). A seguir está uma sintaxe simples do método start():
void start();
Exemplo
Aqui está um exemplo que cria uma nova thread e a inicia:
Live Democlass RunnableDemo implements Runnable {
private Thread t;
private String threadName;
RunnableDemo(String name) {
threadName = name;
System.out.println("Criando " + threadName);
}
public void run() {
System.out.println("Executando " + threadName);
try {
for(int i = 4; i > 0; i--) {
System.out.println("Thread: " + threadName + ", " + i);
// Deixa a thread dormir por um tempo.
Thread.sleep(50);
}
} catch (InterruptedException e) {
System.out.println("Thread " + threadName + " interrompida.");
}
System.out.println("Thread " + threadName + " saindo.");
}
public void start() {
System.out.println("Iniciando " + threadName);
if (t == null) {
t = new Thread(this, threadName);
t.start();
}
}
}
public class TestThread {
public static void main(String args[]) {
RunnableDemo R1 = new RunnableDemo("Thread-1");
R1.start();
RunnableDemo R2 = new RunnableDemo("Thread-2");
R2.start();
}
}
Isso produzirá o seguinte resultado:
Saída
Criando Thread-1 Iniciando Thread-1 Criando Thread-2 Iniciando Thread-2 Executando Thread-1 Thread: Thread-1, 4 Executando Thread-2 Thread: Thread-2, 4 Thread: Thread-1, 3 Thread: Thread-2, 3 Thread: Thread-1, 2 Thread: Thread-2, 2 Thread: Thread-1, 1 Thread: Thread-2, 1 Thread Thread-1 saindo. Thread Thread-2 saindo.
Criar uma Thread Estendendo a Classe Thread
A segunda maneira de criar uma thread é criar uma nova classe que estende a classe Thread usando dois passos simples. Esta abordagem fornece mais flexibilidade no manuseio de múltiplas threads criadas usando métodos disponíveis na classe Thread.
Passo 1
Você precisará sobrescrever o método run() disponível na classe Thread. Este método fornece um ponto de entrada para a thread e você colocará sua lógica de negócios completa dentro deste método. A seguir está uma sintaxe simples do método run():
public void run()
Passo 2
Uma vez que o objeto Thread é criado, você pode iniciá-lo chamando o método start(), que executa uma chamada ao método run(). A seguir está uma sintaxe simples do método start():
void start();
Exemplo
Aqui está o programa anterior reescrito para estender a Thread:
Live Democlass ThreadDemo extends Thread {
private Thread t;
private String threadName;
ThreadDemo(String name) {
threadName = name;
System.out.println("Criando " + threadName);
}
public void run() {
System.out.println("Executando " + threadName);
try {
for(int i = 4; i > 0; i--) {
System.out.println("Thread: " + threadName + ", " + i);
// Deixa a thread dormir por um tempo.
Thread.sleep(50);
}
} catch (InterruptedException e) {
System.out.println("Thread " + threadName + " interrompida.");
}
System.out.println("Thread " + threadName + " saindo.");
}
public void start() {
System.out.println("Iniciando " + threadName);
if (t == null) {
t = new Thread(this, threadName);
t.start();
}
}
}
public class TestThread {
public static void main(String args[]) {
ThreadDemo T1 = new ThreadDemo("Thread-1");
T1.start();
ThreadDemo T2 = new ThreadDemo("Thread-2");
T2.start();
}
}
Isso produzirá o seguinte resultado:
Saída
Criando Thread-1 Iniciando Thread-1 Criando Thread-2 Iniciando Thread-2 Executando Thread-1 Thread: Thread-1, 4 Executando Thread-2 Thread: Thread-2, 4 Thread: Thread-1, 3 Thread: Thread-2, 3 Thread: Thread-1, 2 Thread: Thread-2, 2 Thread: Thread-1, 1 Thread: Thread-2, 1 Thread Thread-1 saindo. Thread Thread-2 saindo.
Métodos de Thread
A seguir está a lista de métodos importantes disponíveis na classe Thread.
| Sr.No. | Método & Descrição |
|---|---|
| 1 | public void start() Inicia a thread em um caminho de execução separado, então invoca o método run() neste objeto Thread. |
| 2 | public void run() Se este objeto Thread foi instanciado usando um alvo Runnable separado, o método run() é invocado naquele objeto Runnable. |
| 3 | public final void setName(String name) Altera o nome do objeto Thread. Também existe um método getName() para recuperar o nome. |
| 4 | public final void setPriority(int priority) Define a prioridade deste objeto Thread. Os valores possíveis estão entre 1 e 10. |
| 5 | public final void setDaemon(boolean on) Um parâmetro true denota esta Thread como uma thread daemon. |
| 6 | public final void join(long millisec) A thread atual invoca este método em uma segunda thread, fazendo com que a thread atual bloqueie até que a segunda thread termine ou o número especificado de milissegundos passe. |
| 7 | public void interrupt() Interrompe esta thread, fazendo com que ela continue a execução se estava bloqueada por qualquer motivo. |
| 8 | public final boolean isAlive() Retorna true se a thread está viva, que é qualquer momento após a thread ter sido iniciada mas antes de ela ser executada até a conclusão. |
Os métodos anteriores são invocados em um objeto Thread particular. Os seguintes métodos na classe Thread são estáticos. Invocar um dos métodos estáticos realiza a operação na thread atualmente em execução.
| Sr.No. | Método & Descrição |
|---|---|
| 1 | public static void yield() Faz com que a thread atualmente em execução ceda a quaisquer outras threads da mesma prioridade que estão esperando para serem agendadas. |
| 2 | public static void sleep(long millisec) Faz com que a thread atualmente em execução bloqueie por pelo menos o número especificado de milissegundos. |
| 3 | public static boolean holdsLock(Object x) Retorna true se a thread atual mantém o bloqueio no objeto fornecido. |
| 4 | public static Thread currentThread() Retorna uma referência à thread atualmente em execução, que é a thread que invoca este método. |
| 5 | public static void dumpStack() Imprime o rastreamento de pilha para a thread atualmente em execução, o que é útil ao depurar uma aplicação multithreaded. |
Exemplo
// Nome do Arquivo: DisplayMessage.java
// Cria uma thread para implementar Runnable
public class DisplayMessage implements Runnable {
private String message;
public DisplayMessage(String message) {
this.message = message;
}
public void run() {
while(true) {
System.out.println(message);
}
}
}
Segue outra classe que estende a classe Thread:
// Nome do Arquivo: GuessANumber.java
// Cria uma thread para estender Thread
public class GuessANumber extends Thread {
private int number;
public GuessANumber(int number) {
this.number = number;
}
public void run() {
int counter = 0;
int guess = 0;
do {
guess = (int) (Math.random() * 100 + 1);
System.out.println(this.getName() + " adivinha " + guess);
counter++;
} while(guess != number);
System.out.println("** Correto!" + this.getName() + " em " + counter + " tentativas.**");
}
}
Segue o programa principal, que faz uso das classes definidas acima:
// Nome do Arquivo: ThreadClassDemo.java
public class ThreadClassDemo {
public static void main(String[] args) {
Runnable hello = new DisplayMessage("Hello");
Thread thread1 = new Thread(hello);
thread1.setDaemon(true);
thread1.setName("hello");
System.out.println("Iniciando thread hello...");
thread1.start();
Runnable bye = new DisplayMessage("Goodbye");
Thread thread2 = new Thread(bye);
thread2.setPriority(Thread.MIN_PRIORITY);
thread2.setDaemon(true);
System.out.println("Iniciando thread goodbye...");
thread2.start();
System.out.println("Iniciando thread3...");
Thread thread3 = new GuessANumber(27);
thread3.start();
try {
thread3.join();
} catch (InterruptedException e) {
System.out.println("Thread interrompida.");
}
System.out.println("Iniciando thread4...");
Thread thread4 = new GuessANumber(75);
thread4.start();
System.out.println("main() está terminando...");
}
}
Isso produzirá o seguinte resultado. Você pode tentar este exemplo repetidamente e obterá um resultado diferente a cada vez.
Saída
Iniciando thread hello... Iniciando thread goodbye... Hello Hello Hello Hello Hello Hello Goodbye Goodbye Goodbye Goodbye Goodbye .......
Principais Conceitos de Multithreading em Java
Ao fazer programação de Multithreading em Java, você precisaria ter os seguintes conceitos muito à mão:
O que é sincronização de thread?
Tratamento de comunicação entre threads
Tratamento de deadlock de thread
Principais operações de thread
Java
- Dominando Operadores Java: Tipos, Sintaxe e Exemplos Práticos
- Comentários em Java: Tipos, Uso e Boas Práticas
- Dominando o Loop for Aprimorado em Java: Sintaxe, Exemplos e Boas Práticas
- Dominando Strings em Java: Criação, Métodos e Boas Práticas
- Dominando Interfaces em Java: Conceitos, Implementação e Boas Práticas
- Dominando Classes Internas Anônimas em Java: Definição, Sintaxe e Exemplos Práticos
- Dominando Try-With-Resources em Java: Gerenciamento Automático de Recursos Explicado
- Anotações em Java Explicadas: Tipos, Posicionamento e Exemplos Práticos
- Asserções em Java: Guia Prático para Detectar Bugs e Garantir Confiabilidade do Código
- Entendendo o Java Vector: Uso, Recursos e Comparação com ArrayList