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 Threadde start() 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.

Figura 1. Os seis estados do ciclo de vida dos threads Java

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 Runnablepodemos 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 um System.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:

  1. Início da execução no thread principal.
  2. Imprima números de 1 a possivelmente 100.000.
  3. 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 um Thread.
  • É 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 Davidsone atribuímos a esse thread a prioridade padrão. O segundo tópico é Dodge Tomahawkatribuído MAX_PRIORITY. O terceiro é Yamaha YZFcom 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_PRIORITYnã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:

Saiba mais sobre Java