CodeGym /Cursos /JAVA 25 SELF /ExecutorService, Callable, Future: execução de tarefas

ExecutorService, Callable, Future: execução de tarefas

JAVA 25 SELF
Nível 54 , Lição 1
Disponível

1. ExecutorService: gerenciamos threads como profissionais

Por que não vale a pena apenas criar threads com new Thread

No início do multithreading tudo parece simples:

Thread t = new Thread(() -> {
    // fazemos algo
});
t.start();

Esse método funciona, mas rapidamente vira um fardo quando há muitas tarefas. Cada chamada de new Thread() cria uma nova thread, e dezenas ou centenas de threads começam a sobrecarregar o sistema. Além disso, gerenciá-las é incômodo: é preciso acompanhar quando terminam, o que fazer em caso de erros, como parar e reutilizá-las.

É aqui que entra em cena o ExecutorService — um orquestrador inteligente de threads. Você apenas entrega as tarefas a ele, e ele decide com qual thread e quando executá-las. Como resultado, tudo funciona mais rápido, de forma mais estável e sem dor de cabeça.

Como o ExecutorService funciona

ExecutorService funciona com um princípio simples e eficiente.

  • Dentro dele há um pool de threads — um conjunto de threads de trabalho pré-criadas (fixo ou dinâmico).
  • As tarefas entram em uma fila e são puxadas por threads livres.
  • O serviço gerencia o ciclo de vida: você pode aguardar a conclusão, encerrar o pool corretamente e liberar recursos.

Criando um ExecutorService

A forma mais comum — usar os métodos de fábrica da classe Executors:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

ExecutorService executor = Executors.newFixedThreadPool(4); // 4 threads
  • newFixedThreadPool(N) — um pool com N threads (serve para a maioria dos casos).
  • newCachedThreadPool() — pool dinâmico, cria threads conforme necessário (cuidado: pode esgotar a memória em uma avalanche de tarefas).
  • newSingleThreadExecutor() — uma única thread (execução sequencial).

Exemplo: executar Runnable via ExecutorService

executor.submit(() -> {
    System.out.println("Olá do pool de threads!");
});

Depois de terminar de usar o ExecutorService, é preciso encerrá-lo corretamente:

executor.shutdown(); // Proíbe adicionar novas tarefas, aguarda a conclusão das atuais

Importante: Se não chamar shutdown(), o programa pode não encerrar — as threads do pool ficarão esperando novas tarefas.

2. Runnable vs Callable: tipos diferentes de tarefas

Antes do Java 5, se você quisesse executar algo em uma thread, implementava a interface Runnable. É uma tarefa que não retorna nada e não lança exceções verificadas.

Runnable task = () -> {
    System.out.println("Só trabalho, não retorno nada!");
};
executor.submit(task);

Callable: tarefa com resultado (e exceções)

Às vezes queremos que a tarefa não apenas “faça algo”, mas também retorne um resultado — por exemplo, a soma de números, o resultado de cálculos, dados de um servidor. Para isso existe a interface Callable<T>.

import java.util.concurrent.Callable;

Callable<Integer> sumTask = () -> {
    int sum = 0;
    for (int i = 1; i <= 100; i++) sum += i;
    return sum;
};
  • O método call() retorna um resultado do tipo T.
  • O método call() pode lançar exceção verificada.

Analogia: Runnable — “vá lavar a louça” (o resultado não importa), Callable — “vá trazer um chá e diga qual é a temperatura” (o resultado importa).

Executando um Callable: para obter o resultado, use executor.submit(...). Ele retornará um objeto Future<T>.

3. Future: uma promessa de resultado

Future é uma “promessa” de devolver um resultado no futuro. Quando você envia uma tarefa ao ExecutorService, recebe um Future, do qual poderá obter o resultado mais tarde, saber se a tarefa terminou ou cancelá-la.

Métodos principais do Future

  • T get() — obter o resultado (aguarda até a tarefa terminar).
  • boolean isDone() — verifica se a tarefa terminou.
  • boolean cancel(boolean mayInterruptIfRunning) — tenta cancelar a tarefa.
  • boolean isCancelled() — verifica se a tarefa foi cancelada.

Exemplo: executar um Callable e obter o resultado

import java.util.concurrent.*;

public class ParallelSumApp {
    public static void main(String[] args) throws Exception {
        ExecutorService executor = Executors.newFixedThreadPool(2);

        Callable<Integer> sumTask = () -> {
            int sum = 0;
            for (int i = 1; i <= 100; i++) sum += i;
            return sum;
        };

        Future<Integer> future = executor.submit(sumTask);

        System.out.println("Tarefa iniciada, dá para fazer mais alguma coisa...");

        // Obtendo o resultado (o método bloqueia a thread se a tarefa ainda não tiver sido concluída)
        Integer result = future.get();
        System.out.println("Resultado dos cálculos: " + result);

        executor.shutdown();
    }
}
  • A tarefa é enviada ao pool de threads.
  • Enquanto a tarefa executa, a thread principal pode fazer outras coisas.
  • Quando o resultado for necessário, chamamos future.get() — a thread aguardará se a tarefa ainda estiver em execução.
  • Assim que a tarefa terminar, o resultado será retornado.

4. Prática: várias tarefas e espera pela conclusão

Muitas vezes é preciso iniciar várias tarefas de uma vez e esperar que todas terminem. Por exemplo, você processa um array de dados, divide-o em partes e calcula a soma de cada parte em uma tarefa separada.

Exemplo: soma dos elementos do array por partes

import java.util.*;
import java.util.concurrent.*;

public class ParallelArraySum {
    public static void main(String[] args) throws Exception {
        int[] array = new int[1000];
        Arrays.setAll(array, i -> i + 1); // Preenchemos com números de 1 a 1000

        ExecutorService executor = Executors.newFixedThreadPool(4);

        int chunkSize = array.length / 4;
        List<Future<Integer>> futures = new ArrayList<>();

        for (int i = 0; i < 4; i++) {
            int from = i * chunkSize;
            int to = (i == 3) ? array.length : (i + 1) * chunkSize;

            Callable<Integer> sumTask = () -> {
                int sum = 0;
                for (int j = from; j < to; j++) sum += array[j];
                System.out.println("Soma de " + from + " até " + (to - 1) + " = " + sum);
                return sum;
            };

            futures.add(executor.submit(sumTask));
        }

        int totalSum = 0;
        for (Future<Integer> f : futures) {
            totalSum += f.get(); // Aguardamos cada tarefa por sua vez
        }

        System.out.println("Soma total: " + totalSum);

        executor.shutdown();
    }
}

Aqui, o array é dividido em 4 partes. Para cada parte é criada uma tarefa (Callable) que calcula a soma. Todas as tarefas são enviadas ao ExecutorService, retornando Future. No final, reunimos os resultados de todas as tarefas e somamos.

Em casos reais é conveniente usar invokeAll para esperar todas as tarefas de uma vez.

5. Tratamento de erros ao usar Future

Quando você chama future.get(), se a tarefa terminou com exceção, ela será encapsulada como ExecutionException. Isso é importante: se algo deu errado na tarefa, você só saberá ao chamar get().

Exemplo: tratamento de exceções

Callable<Integer> errorTask = () -> {
    throw new IllegalArgumentException("Algo deu errado!");
};

Future<Integer> badFuture = executor.submit(errorTask);

try {
    badFuture.get();
} catch (ExecutionException e) {
    System.out.println("A tarefa terminou com erro: " + e.getCause());
}
  • Dentro da tarefa é lançada uma exceção.
  • Ao chamar get() ela é “embrulhada” em ExecutionException.
  • A causa real pode ser obtida por getCause().

6. Detalhes úteis

Como cancelar uma tarefa

Future<?> f = executor.submit(() -> {
    while (true) {
        // Trabalho infinito
        if (Thread.currentThread().isInterrupted()) {
            System.out.println("Pediram para eu encerrar!");
            break;
        }
    }
});

Thread.sleep(100); // Vamos esperar um pouco
f.cancel(true); // Vamos tentar cancelar a tarefa
  • cancel(true) tenta interromper a tarefa, se ela ainda não tiver sido concluída.
  • Dentro da tarefa é aconselhável verificar Thread.currentThread().isInterrupted() e encerrar corretamente.

shutdown vs shutdownNow

shutdown() — parada suave: proíbe adicionar novas tarefas e permite que as atuais terminem em paz. Usado na maioria dos casos.

shutdownNow() — parada brusca: tenta interromper as threads ativas e retorna a lista de tarefas que não chegaram a iniciar. Use com cautela.

invokeAll e invokeAny

invokeAll(Collection<Callable<T>> tasks) inicia todas as tarefas passadas e aguarda até que todas terminem. Retorna uma lista de Future.

invokeAny(Collection<Callable<T>> tasks) aguarda apenas a primeira tarefa concluída com sucesso, retorna seu resultado e cancela as demais. Útil quando importa a primeira resposta bem-sucedida.

7. Erros comuns ao trabalhar com ExecutorService, Callable e Future

Erro nº 1: Não encerrar o ExecutorService. Se você esquecer de chamar shutdown(), o programa pode “ficar preso” após o término do main, porque as threads do pool aguardam novas tarefas.

Erro nº 2: Aguardar o resultado imediatamente após enviar a tarefa. Se logo após submit() você chamar get(), não obterá as vantagens da assincronia — a thread vai esperar de qualquer forma. Faça trabalho útil em paralelo e solicite o resultado quando ele realmente for necessário.

Erro nº 3: Ignorar exceções nas tarefas. Se você não tratar ExecutionException ao chamar get(), pode perder erros importantes que ocorreram na tarefa.

Erro nº 4: Usar variáveis mutáveis compartilhadas sem sincronização. Se várias tarefas trabalham com os mesmos dados — é preciso sincronização ou coleções thread-safe.

Erro nº 5: Criar threads demais. Não faça um pool com número de threads muito acima da quantidade de núcleos do processador — isso pode até desacelerar a execução.

Erro nº 6: Esquecer de cancelar tarefas. Se a tarefa não for mais necessária, cancele-a via cancel() para não desperdiçar recursos.

Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION