Alura > Cursos de Programação > Cursos de Java > Conteúdos de Java > Primeiras aulas do curso Java threads: aprenda a criar, gerenciar e aplicar com o Spring

Java threads: aprenda a criar, gerenciar e aplicar com o Spring

Utilizando threads em Java - Apresentação

Olá, meu nome é Iasmin Araújo, faço parte da equipe da Escola de Programação e dou as boas-vindas para mais um curso de Java na Alura.

Audiodescrição: Iasmin se identifica como uma mulher branca. Possui cabelos castanho-escuros longos e olhos verdes. No corpo, usa uma camiseta preta. Está sentada em uma cadeira gamer preta. Ao fundo, há um quarto com iluminação azul.

Neste curso, vamos falar de threads, mas não são as threads do Twitter (atual X), nem do Zuckerberg. Vamos falar das threads que criamos com Java.

Para quem é este curso?

Este conteúdo é para quem já tem uma base em Java — portanto, já conhece classes, objetos, interfaces e collections (coleções) — e já viu o conteúdo de Spring Boot, além de ter prática com isso.

O que vamos aprender?

Vamos explorar tudo isso em dois projetos diferentes. Primeiro, vamos trabalhar com um projeto de linha de comando, que é uma simulação de um banco. Nesse projeto, vamos ver primeiro como criar as threads manualmente e manipulá-las de certa forma.

Depois que tivermos essa prática com as threads, vamos para um projeto mais avançado, que utiliza Spring Boot e é mais parecido com o que vemos no mundo real. Esse projeto é a AdoPet Store, uma nova parte da AdoPet que vende produtos para os pets adotados.

Para aproveitar todos os recursos da plataforma, você pode assistir os vídeos, fazer todas as atividades e contar com o apoio do fórum e da comunidade do Discord.

Vamos começar nosso desenvolvimento!

Utilizando threads em Java - Tarefas síncronas ou assíncronas?

Quando estamos trabalhando com programação, sempre queremos simular as situações reais da melhor forma possível. Para armazenar as informações e lidar com elas, precisamos representá-las da melhor forma no código.

Uma situação muito comum quando estamos trabalhando com programação é querer simular coisas que acontecem paralelamente. Vamos pensar em um exemplo. Suponhamos que somos clientes de um banco X, e nele, temos uma conta e um cartão de débito.

Esse cartão fornece um cartão de débito adicional que podemos dar a outra pessoa. Contudo, ocorre uma falha de comunicação e as duas pessoas que possuem os cartões resolvem ir ao banco e sacar todo o dinheiro na conta ao mesmo tempo. Como podemos representar isso no código com Java da melhor forma possível?

Para ver como podemos fazer isso, vamos utilizar um projeto do qual temos a parte inicial do código já disponível. Recomendamos que você o clone e vá "codando" conosco.

Conhecendo o projeto

Vamos abrir no IntelliJ o projeto já importado. Nele, temos alguns arquivos com classes bem básicas:

Na lógica do log dentro do método executa, se o saldo atual for maior do que o valor que queremos sacar, podemos debitar o valor da conta. Caso contrário, não entramos no if e só finalizamos o saque.

Temos essas regras básicas. Esse é um projeto que está em evolução, no qual vamos adicionar algumas regras.

Como mencionado, queremos simular os dois saques ao mesmo tempo. Para isso, vamos criar uma classe para lidar com essas contas.

Criando a classe main

Podemos criar uma classe main e uma conta para ver como vamos sacar. No explorador de arquivos, na lateral esquerda da IDE, vamos clicar no pacote "br.com.alura" e usar "Alt+Insert" para criar uma nova classe. Ele exibirá uma janela flutuante chamada "New", na qual vamos selecionar a primeira opção, "Java Class".

Na janela "New Java Class", vamos adicionar o nome da classe, que vamos chamar de AppBanco, e pressionar "Enter". Se ele exibir uma janela que nos pergunta se queremos adicionar esse arquivo ao Git, vamos aceitar com um "Enter".

Com isso a estrutura básica abaixo será gerada.

package br.com.alura;

public class AppBanco {
}

Acessando o arquivo recém-criado, entre as chaves da classe AppBanco, vamos digitar psvm e pressionar "Tab" para transformar a classe em uma classe main.

Entre as chaves do método main, vamos começar criando uma entidade cliente, usando o var cliente, que vai ser um new Cliente() com o nome João.

Precisamos criar uma conta para João. Desceremos uma linha e faremos um var conta, que será new Conta(), passando entre parênteses o cliente, que acabamos de criar, e um valor inicial para a conta. Quando ele cria a conta, pode escolher depositar algum valor. Então, à direita de cliente, vamos informar um new BigDecimal(), passando entre parênteses 150 reais, por exemplo.

package br.com.alura;

public class AppBanco {
    public static void main(String[] args) {
        var cliente = new Cliente("João");
        var conta = new Conta(cliente, new BigDecimal("150"));
    } 
}

Queremos fazer dois saques ao mesmo tempo, utilizando essa conta. Para isso, vamos pressionar "Enter" duas vezes no final da linha da conta e criar uma operação de saque, que vamos chamar de operacao. Ela vai ser um new OperacaoSaque(), passando entre parênteses a conta e de novo o valor que queremos sacar.

Como queremos simular que as duas pessoas estão sacando o mesmo valor, para ver quais são os problemas que podem acontecer, vamos passar 150 reais novamente, por meio de um new BigDecimal("150").

package br.com.alura;

public class AppBanco {
    public static void main(String[] args) {
        var cliente = new Cliente("João");
        var conta = new Conta(cliente, new BigDecimal("150"));
        
        var operacao = new OperacaoSaque(conta, new BigDecimal("150"));
    } 
}

Instanciamos uma operação, mas precisamos executar esse saque. Primeiro, vamos supor que João sacará. Desceremos duas linha e comentaremos // saque João, só para ver o que estará acontecendo no código. Desceremos uma linha e faremos um operacao.executa().

Mas, ao mesmo tempo, Maria recebeu o cartão adicional de João e também quer executar esse saque. Então, vamos querer fazer o saque de Maria. Vamos comentar //saque Maria na linha de baixo, descer outra linha e fazer o operacao.executa(), mais uma vez.

package br.com.alura;

public class AppBanco {
    public static void main(String[] args) {
        var cliente = new Cliente("João");
        var conta = new Conta(cliente, new BigDecimal("150"));
        
        var operacao = new OperacaoSaque(conta, new BigDecimal("150"));
        
        //saque João
        operacao.executa();
        //saque Maria
        operacao.executa();
    } 
}

Vamos ver o que vai acontecer. Se clicarmos no botão "Run 'AppBanco.java'", na parte direita no menu superior, vamos rodar o programa.

O terminal será aberto, no qual podemos ver que aconteceu o seguinte: iniciamos o saque, debitamos o valor da conta e o saldo atual passou a ser zero, porque tínhamos 150 reais e tiramos 150 reais. Depois, iniciamos outro saque, mas finalizamos logo depois, sem debitar nada.

Iniciando operação de saque.

Debitando valor da conta

Saldo atual: 0

Finalizando operação de saque.

Iniciando operação de saque.

Finalizando operação de saque.

Process finished with exit code 0

Por que isso ocorre? Se acessarmos a classe OperacaoSaque, veremos que o saldo que tínhamos era zero. Quando tentamos tirar 150 de zero, não podemos fazer nada, porque senão o saldo vai ficar negativo.

Contudo, se voltarmos à classe AppBanco, o que está sendo feito? Temos as variáveis sendo instanciadas, fazemos uma operacao.executa() e, em seguida, fazemos uma operacao.executa() para simular esses saques sequenciais de João e de Maria. É como se João sacasse e, logo depois, Maria sacasse.

Mas queremos que as duas coisas aconteçam ao mesmo tempo. Deve haver outra forma no Java de representar operações paralelas, isto é, que acontecem ao mesmo tempo.

Vamos ver isso logo em seguida, no próximo vídeo. Até lá!

Utilizando threads em Java - Simulando tarefas paralelas

Vimos que nossos saques estão ocorrendo de forma sequencial, mas queremos que eles ocorram de forma paralela, simulando isso no código. Você já deve ter percebido pelo nome do curso e pela introdução que, para trabalhar com processos paralelos em Java, utilizamos as threads.

O que são threads em Java?

Mas o que são e como utilizamos essas threads? São relacionadas ao Twitter ou ao aplicativo da Meta? Não!

Estamos falando das threads do Java, ou seja, uma classe que vamos utilizar, mapeadas diretamente para o sistema operacional. Veremos como em breve.

Como utilizar uma thread?

Para usar uma thread no código, voltaremos ao arquivo AppBanco. Entre as chaves do método main(), após a declaração da var operacao, criaremos uma nova linha com "Enter".

Podemos fazer a thread do jeito mais tradicional, que é Thread thread = new Thread(). Ao escrever isso, a IDE exibirá uma lista de vários tipos de construtores que podemos utilizar: o construtor padrão, outro que recebe um Runnable task, outro no qual podemos passar o nome da thread, e vários outros.

Contudo, se escolhermos o new Thread() com o construtor padrão, simplesmente criaremos uma thread na JVM, e não faremos mais nada com ela.

Estamos trabalhando com um banco, no qual criamos uma entidade cliente e uma conta. Depois, queremos simular que os saques sejam paralelos. Se queremos simular atividades paralelas, ou seja, tarefas diferentes, precisamos informar ao Java que temos uma thread e que queremos paralelizar nossas atividades. Em cada thread que criarmos, colocaremos tarefas diferentes.

Para colocar tarefas diferentes, precisamos passar um Runnable com o estilo de construtor Thread(Runnable task). A tarefa (task) que queremos passar entre os parênteses será justamente a operacao.

AppBanco.java

public class AppBanco {
    public static void main(String[] args) {
        var cliente = new Cliente("João");
        var conta = new Conta(cliente, new BigDecimal("150"));
        
        var operacao = new OperacaoSaque(conta, new BigDecimal("150"));
    
        Thread thread = new Thread(operacao);
        
        //saque João
        operacao.executa();
        //saque Maria
        operacao.executa();
    } 
}

Contudo, ao acessar a classe OperacaoSaque, veremos que é apenas uma classe básica do Java que declaramos como public class OperacaoSaque. Precisamos transformá-la em um Runnable.

Para fazer isso, precisaremos implementar a interface Runnable. Logo após a declaração da classe, usaremos um implements Runnable.

OperacaoSaque.java

public class OperacaoSaque implements Runnable{

    // Código omitido
}

A interface Runnable tem o método run(). Vamos implementá-lo clicando no ícone de lâmpada vermelha, à esquerda dessa linha, e selecionamos a primeira opção, "Implement methods". Na nova janela exibida, o run() já estará o run selecionado, então pressionaremos o botão "OK".

public class OperacaoSaque implements Runnable{

    // Código omitido
}

@Override
public void run() {

}

Entre as chaves do método run(), precisaremos passar o que queremos que a thread faça quando for executada. Estamos executando o programa e queremos que algumas tarefas sejam executadas paralelamente.

No caso, queremos que os saques sejam feitos paralelamente. Como fazemos esse saque? Se olharmos a classe OperacaoSaque, veremos que isso é feito pelo método executa(). No método run(), vamos chamar esse método executa().

@Override
public void run() {
    executa();
}

Também poderíamos recortar o que está no corpo desse método e passar para o run(), mas é mais fácil passarmos o método. Podemos fazer como preferirmos.

Escolhemos o que queremos fazer com a thread, então podemos voltar para o arquivo AppBanco. Dentro dessa classe, criamos uma thread para um saque. Mas queremos duas threads para representar os dois saques. Para entender melhor, renomearemos a variável thread para saqueJoao. Teremos um saqueJoão e um saqueMaria, portanto, duplicaremos essa linha do saque com o "Ctrl+D" e renomearemos a segunda para saqueMaria.

public class AppBanco {
    public static void main(String[] args) {
        var cliente = new Cliente("João");
        var conta = new Conta(cliente, new BigDecimal("150"));
        
        var operacao = new OperacaoSaque(conta, new BigDecimal("150"));
    
        Thread saqueJoao = new Thread(operacao);
        Thread saqueMaria = new Thread(operacao);
        
        //saque João
        operacao.executa();
        //saque Maria
        operacao.executa();
    } 
}

Criamos duas threads diferentes, mas apenas na memória. Precisamos "startar" (iniciá-las) de alguma forma.

Vamos usar o método start abaixo da criação das threads, por meio de um saqueJoao.start e de um saqueMaria.start. Com isso, ele inicializará a thread.

Como as threads estão executando as tarefas, podemos comentar ou apagar as quatro linhas que criamos no final do método, com o saque de João e de Maria. Para comentar, selecionaremos as quatro linhas e pressionaremos "Ctrl+/".

public class AppBanco {
    public static void main(String[] args) {
        var cliente = new Cliente("João");
        var conta = new Conta(cliente, new BigDecimal("150"));
        
        var operacao = new OperacaoSaque(conta, new BigDecimal("150"));
    
        Thread saqueJoao = new Thread(operacao);
        Thread saqueMaria = new Thread(operacao);
        
        saqueJoao.start();
        saqueMaria.start();
        
//		//saque João
//		operacao.executa();
//		//saque Maria
//		operacao.executa();
//	} 
}

Vamos ver o que será executado. Pressiona "Shift+F10" para rodar o programa, em alternativa ao pressionamento do botão "Run".

Iniciando operação de saque.

Iniciando operação de saque.

Debitando valor da conta

Debitando valor da conta

Saldo atual: 0

Finalizando operação de saque.

Saldo atual: -150

Finalizando operação de saque.

Process finished with exit code 0

No terminal, podemos ver o que foi executado. Temos um "iniciando a operação de saque", "debitando o valor da conta", depois, ele mostra um saldo e finaliza. Depois, mostra outro saldo e finaliza.

Vemos um log e outro logo em seguida, depois o saldo e depois o débito. Isso indica que as duas operações foram executadas paralelamente. Podemos ver realmente as threads sendo executadas.

Uma thread é uma linha de execução. Toda vez que abrimos qualquer aplicativo do computador, uma thread é inicializada. Se temos o IntelliJ aberto, há uma thread sendo executada no sistema operacional. Se temos o Google Chrome aberto, também tem uma thread executando no sistema. Atualmente, temos a thread do IntelliJ, mas se executamos o Java, temos mais uma thread sendo aberta especificamente para ele.

Queremos programar situações específicas que são paralelas, portanto, queremos criar novas threads no programa. Para isso, usamos a classe Thread que possibilita a criação de mais de uma thread no Java.

Vamos observar o que acontece quando estamos utilizando o Java e as threads. Temos um retângulo à esquerda denominado "Programa Java", e dele saem três setas denominadas "Thread 1", "Thread 2" e "Thread 3" sendo apontadas para três novas threads do sistema operacional à direita, a qual chamamos de "Thread SO 1", "Thread SO 2" e "Thread SO 3".

Descrição da imagem: Diagrama representando o processo de multithreading em um programa Java. À esquerda, há um retângulo com bordas arredondadas e uma linha ondulada na parte inferior, indicando um programa Java. A partir desse retângulo, saem três setas, cada uma apontando para um dos três retângulos separados à direita. Cada retângulo à direita é rotulado com 'Thread SO 1', 'Thread SO 2' e 'Thread SO 3', indicando threads em um sistema operacional. Textos adicionais próximos às setas da esquerda etiquetam-nas como 'Thread 1', 'Thread 2' e 'Thread 3'. O fundo da imagem é azul escuro.

As threads que estão sendo criadas no programa Java são mapeadas diretamente para threads no sistema operacional.

Outra informação importante: toda vez que estamos executando um programa, já temos uma thread. Nesse caso, a "Thread 1" sempre será a main (principal) e sempre vai existir. Posteriormente, podemos criar threads paralelamente no programa e ter novas threads.

Voltando ao código do arquivo AppBanco, podemos ver isso claramente. Para conferirmos se novas threads realmente estão sendo criadas, vamos adicionar abaixo do start() um método estático das threads chamado Thread.currentThread().getName().

Podemos querer imprimir esse método, que trará para nós o nome da thread atual que está sendo executada. Para isso adicionaremos o que está sendo feito na currentThread entre os parênteses de um System.out.println().

AppBanco.java

public class AppBanco {
    public static void main(String[] args) {
        var cliente = new Cliente("João");
        var conta = new Conta(cliente, new BigDecimal("150"));
        
        var operacao = new OperacaoSaque(conta, new BigDecimal("150"));
    
        Thread saqueJoao = new Thread(operacao);
        Thread saqueMaria = new Thread(operacao);
        
        saqueJoao.start();
        saqueMaria.start();
        
        System.out.println(Thread.currentThread().getName());
        
//		//saque João
//		operacao.executa();
//		//saque Maria
//		operacao.executa();
//	} 
}

O que está sendo feito no método main()? Temos a entidade cliente, a conta e a operação sendo criados, depois, criamos novas threads.

Temos uma linha (thread) de execução main sendo executada e que sempre existe, conforme mencionamos mais cedo. Mas existem também novas threads que estamos criando: a saqueJoao e a saqueMaria. Mas será que essas threads realmente estão sendo criadas?

Conferindo a criação das threads

Toda vez que executarmos a tarefa da operação, também imprimiremos a thread sendo executada usando o mesmo método currentThread().getName(). Portanto, para verificar se as threads estão sendo criadas, copiaremos a linha System.out.println(Thread.currentThread().getName()) com "Ctrl+C", voltaremos ao arquivo OperacaoSaque e colaremos entre as chaves do método run() com "Ctrl+V", abaixo de executa().

OperacaoSaque.java

@Override
public void run() {
    executa();
    System.out.println(Thread.currentThread().getName());
}

Vamos executar novamente o programa usando "Shift+F10" para ver o que está sendo feito.

main

Iniciando operação de saque.

Iniciando operação de saque.

Debitando valor da conta

Debitando valor da conta

Saldo atual: -150

Finalizando operação de saque.

Saldo atual: 0

Finalizando operação de saque.

Thread-1

Thread-0

Process finished with exit code 0

No terminal, ele começa imprimindo main, o nome da thread do método main, a primeira e que sempre existe. Mas existem as threads que criamos também e que, nesse caso, estão sendo impressas como "Thread-1" e a "Thread-0". Isso significa que realmente conseguimos simular esses saques paralelos.

Esse paralelismo, ou seja, essas coisas que acontecem ao mesmo tempo, sempre vão existir no contexto do banco e em vários outros. Nesse sentido, as threads são essenciais para nós.

Se verificarmos o que está acontecendo no terminal, iniciamos a operação e debitamos, mas o saldo fica negativo. E essa não é uma regra válida no banco. Queremos que o saldo fique no máximo zerado, ele não pode ficar negativo.

Para tratar esse problema, precisamos lidar com os recursos das threads. Faremos isso logo em seguida.

Sobre o curso Java threads: aprenda a criar, gerenciar e aplicar com o Spring

O curso Java threads: aprenda a criar, gerenciar e aplicar com o Spring possui 155 minutos de vídeos, em um total de 54 atividades. Gostou? Conheça nossos outros cursos de Java em Programação, ou leia nossos artigos de Programação.

Matricule-se e comece a estudar com a gente hoje! Conheça outros tópicos abordados durante o curso:

Aprenda Java acessando integralmente esse e outros cursos, comece hoje!

Conheça os Planos para Empresas