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
Runnabledefine 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:
Callablepermite retornar um valor (neste caso,42). O métodosubmit()envia a tarefa para o executor e retorna umFuture, 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
ExecutorServicegerencia um pool de threads e distribui as tarefas entre as threads disponíveis. Usamossubmit()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
ExecutorServicecomCallable, onde a tarefa retorna um valor. O métodosubmit()retorna umFuture, 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:
- Não Usar
Thread.start()Correto: Sempre usestart()para iniciar a execução de uma thread, e nãorun(), que apenas executa o código de forma síncrona. - Falta de Tratamento de Exceções em
Future.get():Future.get()pode lançarInterruptedExceptioneExecutionException, por isso deve ser tratado. - Não Fechar o
ExecutorService: Após a execução das tarefas, sempre chameshutdown()oushutdownNow()noExecutorServicepara liberar recursos. - Ficar Atento ao Tipo de Tarefa: Use
Runnablequando não precisar de retorno de valor eCallablequando precisar de um valor ou precisar lançar exceções verificadas.
⌛Quiz – Perguntas Rápidas
- 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
- D – O método correto para enviar uma tarefa que não retorna valor é
execute(), que é específico paraRunnable.submit()também pode ser usado comRunnable, mas é mais comum comCallable. - B – O método
get()doFutureé usado para obter o resultado de umCallable. Ele bloqueia até que o resultado esteja disponível. - C – A principal diferença é que
Callablepode lançar exceções verificadas e retornar um valor, enquantoRunnablenão retorna valor e não pode lançar exceções verificadas. - B – O método
awaitTermination()permite aguardar a conclusão das tarefas em andamento, enquanto oshutdownNow()tenta interromper as tarefas.