Introdução às Threads

Threads permitem a execução de múltiplas tarefas em paralelo dentro de um mesmo processo. Isso é especialmente útil em aplicações que necessitam realizar operações simultâneas, como processamento de dados em background enquanto a interface do usuário permanece responsiva.

public class Main {
    public static void main(String[] args) {
        System.out.println("Programa iniciado.");
        // Executa uma tarefa em uma nova thread
        new Thread(() -> {
            System.out.println("Executando em uma nova thread.");
        }).start();
        System.out.println("Retornou ao método main.");
    }
}

Criação de Threads em Java

Existem duas maneiras principais de criar uma thread em Java: implementando a interface Runnable ou estendendo a classe Thread.

Implementando a interface Runnable:

public class MyRunnable implements Runnable {
    public void run() {
        System.out.println("Executando em uma thread Runnable.");
    }
}

// Na classe principal
public class Main {
    public static void main(String[] args) {
        Thread myThread = new Thread(new MyRunnable());
        myThread.start();
    }
}

Estendendo a classe Thread:

public class MyThread extends Thread {
    public void run() {
        System.out.println("Executando em uma thread estendida.");
    }
}

// Na classe principal
public class Main {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
    }
}

Dormindo e Interrupção

Threads em Java podem ser pausadas utilizando o método sleep(). A interrupção de uma thread pode ser feita chamando o método interrupt().

public class SleepExample implements Runnable {
    public void run() {
        try {
            Thread.sleep(1000); // Pausa a thread por 1 segundo
            System.out.println("Thread acordada.");
        } catch (InterruptedException e) {
            System.out.println("Thread interrompida.");
        }
    }

    public static void main(String[] args) {
        Thread thread = new Thread(new SleepExample());
        thread.start();
        thread.interrupt(); // Interrompe a thread durante o sono
    }
}

Prática com Threads: Envio de Emails em Paralelo

Podemos usar threads para simular o envio de emails em paralelo. Cada thread simulará o envio de um email.

public class EmailSender implements Runnable {
    private String recipient;

    public EmailSender(String recipient) {
        this.recipient = recipient;
    }

    public void run() {
        System.out.println("Enviando email para " + recipient);
        // Simula o tempo de envio
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            System.out.println("Envio interrompido para " + recipient);
        }
        System.out.println("Email enviado para " + recipient);
    }

    public static void main(String[] args) {
        String[] recipients = {"alice@example.com", "bob@example.com", "charlie@example.com"};

        for (String recipient : recipients) {
            new Thread(new EmailSender(recipient)).start();
        }
    }
}

Classes Anônimas e Lambdas

Java permite a criação de threads de maneira concisa usando classes anônimas e expressões lambda, facilitando a escrita de código para tarefas concorrentes.

Exemplo com Classe Anônima:

new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("Executando com classe anônima.");
    }
}).start();

Exemplo com Expressão Lambda:

new Thread(() -> System.out.println("Executando com lambda.")).start();

Soluções e Boas Práticas

A melhor forma de usar threads em Java depende do contexto específico da aplicação, mas algumas práticas e abordagens modernas podem ajudar a otimizar o desempenho, a escalabilidade e a manutenção do código. Aqui estão algumas recomendações:

Utilizar Pools de Threads

Em vez de criar novas threads manualmente para cada tarefa, é preferível utilizar um pool de threads, como os fornecidos pelo Executor Framework (java.util.concurrent). Os pools de threads ajudam a limitar o número total de threads ativas, reutilizam threads para múltiplas tarefas e reduzem o overhead de criação e destruição de threads.

Exemplo:

ExecutorService executor = Executors.newFixedThreadPool(10); // Cria um pool com 10 threads

executor.execute(() -> {
    System.out.println("Tarefa executada no pool de threads.");
});

executor.shutdown(); // Inicia o processo de encerramento, não aceitando novas tarefas.