Criando Threads de Trabalho Usando Runnable, Callable e ExecutorService

A criação de threads e a execução de tarefas em paralelo são conceitos fundamentais em Java para melhorar a performance de sistemas que realizam tarefas simultâneas. Vamos explorar como usar Runnable, Callable e ExecutorService para executar tarefas de forma concorrente.


1. Usando Runnable para Criar Threads

Runnable é uma interface funcional que representa uma tarefa que pode ser executada por uma thread. Para usar Runnable, você implementa o método run() com a lógica da tarefa e depois cria uma thread passando essa implementação para o construtor da Thread.

✅Exemplo de Uso de Runnable:

public class ExemploRunnable {
  public static void main(String[] args) {
    Runnable tarefa = () -> {
      System.out.println("Tarefa em execução: " + Thread.currentThread().getName());
    };

    Thread thread = new Thread(tarefa);
    thread.start(); // Inicia a execução da thread
  }
}
  • Explicação: A implementação de Runnable define o que será executado na thread. No exemplo, a thread imprime o nome da thread corrente.

❌Erro Comum: Thread não está sendo iniciada corretamente

Certifique-se de chamar thread.start() em vez de thread.run(). Usar run() diretamente executa o código de forma síncrona, sem criar uma nova thread.


2. Usando Callable para Criar Threads com Resultado

Callable é similar ao Runnable, mas oferece a possibilidade de retornar um valor e lançar exceções. Ele é executado em uma thread, mas seu método call() pode retornar um valor ou lançar exceções verificadas.

Exemplo de Uso de Callable:

import java.util.concurrent.*;

public class ExemploCallable {
  public static void main(String[] args) {
    Callable<Integer> tarefa = () -> {
      System.out.println("Tarefa em execução: " + Thread.currentThread().getName());
      return 42; // Retorna um valor
    };

    ExecutorService executor = Executors.newFixedThreadPool(1);
    Future<Integer> resultado = executor.submit(tarefa);

    try {
      System.out.println("Resultado da tarefa: " + resultado.get()); // Obtém o resultado
    } catch (InterruptedException | ExecutionException e) {
      e.printStackTrace();
    } finally {
      executor.shutdown(); // Fechar o executor
    }
  }
}
  • Explicação: Callable permite retornar um valor (neste caso, 42). O método submit() envia a tarefa para o executor e retorna um Future, que pode ser usado para obter o resultado da execução.

❌Erro Comum: Falta de chamada a get() no Future

Ao usar Callable, lembre-se de chamar get() no Future para obter o resultado da execução. O get() pode lançar exceções, por isso é importante tratá-las adequadamente.


3. Usando ExecutorService para Gerenciar e Executar Tarefas Concorrentes

ExecutorService fornece uma maneira mais flexível e eficiente de gerenciar threads, sem precisar criar e gerenciar manualmente as threads. Usando o ExecutorService, podemos enviar tarefas para execução, limitar o número de threads e recuperar os resultados de forma fácil.

Exemplo de Uso de ExecutorService com Runnable:

import java.util.concurrent.*;

public class ExemploExecutorServiceRunnable {
  public static void main(String[] args) {
    ExecutorService executor = Executors.newFixedThreadPool(2); // Thread pool com 2 threads

    for (int i = 0; i < 5; i++) {
      executor.submit(() -> {
        System.out.println("Tarefa em execução: " + Thread.currentThread().getName());
      });
    }

    executor.shutdown(); // Fecha o executor
  }
}
  • Explicação: O ExecutorService gerencia um pool de threads e distribui as tarefas entre as threads disponíveis. Usamos submit() para enviar tarefas para execução.

❌Erro Comum: Não fechar o Executor

Lembre-se de sempre chamar shutdown() ou shutdownNow() para fechar o ExecutorService depois de terminar de usá-lo. Caso contrário, o programa pode continuar executando indefinidamente, aguardando a conclusão das tarefas.


Exemplo de Uso de ExecutorService com Callable:

import java.util.concurrent.*;

public class ExemploExecutorServiceCallable {
  public static void main(String[] args) {
    ExecutorService executor = Executors.newFixedThreadPool(3);

    for (int i = 0; i < 3; i++) {
      Callable<Integer> tarefa = () -> {
        System.out.println("Tarefa em execução: " + Thread.currentThread().getName());
        return 42; // Retorna um valor
      };

      Future<Integer> resultado = executor.submit(tarefa);
      try {
        System.out.println("Resultado da tarefa: " + resultado.get());
      } catch (InterruptedException | ExecutionException e) {
        e.printStackTrace();
      }
    }

    executor.shutdown(); // Fecha o executor
  }
}
  • Explicação: O código acima utiliza um ExecutorService com Callable, onde a tarefa retorna um valor. O método submit() retorna um Future, que é usado para obter o resultado da execução.

❌Erro Comum: Aguardar o Future depois de chamar shutdown()

É importante lembrar que após chamar shutdown(), não é possível enviar novas tarefas para o ExecutorService. Se você precisar realizar tarefas após o encerramento, use o método awaitTermination() para aguardar a conclusão das tarefas em andamento.


🧩 Dicas e Erros Frequentes na Prova:

  1. Não Usar Thread.start() Correto: Sempre use start() para iniciar a execução de uma thread, e não run(), que apenas executa o código de forma síncrona.
  2. Falta de Tratamento de Exceções em Future.get(): Future.get() pode lançar InterruptedException e ExecutionException, por isso deve ser tratado.
  3. Não Fechar o ExecutorService: Após a execução das tarefas, sempre chame shutdown() ou shutdownNow() no ExecutorService para liberar recursos.
  4. Ficar Atento ao Tipo de Tarefa: Use Runnable quando não precisar de retorno de valor e Callable quando precisar de um valor ou precisar lançar exceções verificadas.

Quiz – Perguntas Rápidas

  1. Qual método de ExecutorService é usado para enviar uma tarefa que não retorna um valor?

A) submit()
B) invokeAll()
C) submit() com Runnable
D) execute()

2. Qual método você usaria para obter o resultado de um Callable?

A) submit()
B) get()
C) run()
D) execute()

3. Qual é a principal diferença entre Runnable e Callable?

A) Runnable retorna um valor, enquanto Callable não retorna valor.
B) Callable pode lançar exceções verificadas, enquanto Runnable não pode.
C) Runnable não pode lançar exceções, enquanto Callable pode.
D) Callable é para tarefas simples, enquanto Runnable é para tarefas complexas.

4. Como você pode evitar que o programa fique aguardando tarefas indefinidamente após o uso de um ExecutorService?

A) Usar shutdownNow()
B) Usar awaitTermination()
C) Usar stop()
D) Usar cancel()


Respostas Comentadas

  1. D – O método correto para enviar uma tarefa que não retorna valor é execute(), que é específico para Runnable. submit() também pode ser usado com Runnable, mas é mais comum com Callable.
  2. B – O método get() do Future é usado para obter o resultado de um Callable. Ele bloqueia até que o resultado esteja disponível.
  3. C – A principal diferença é que Callable pode lançar exceções verificadas e retornar um valor, enquanto Runnable não retorna valor e não pode lançar exceções verificadas.
  4. B – O método awaitTermination() permite aguardar a conclusão das tarefas em andamento, enquanto o shutdownNow() tenta interromper as tarefas.