Guia para java.util.concurrent.Future

Guia para java.util.concurrent.Future

1. Visão geral

Neste artigo, aprenderemos sobreFuture. Uma interface que existe desde Java 1.5 e pode ser bastante útil ao trabalhar com chamadas assíncronas e processamento simultâneo.

2. CriandoFutures

Simplificando, a classeFuture representa um resultado futuro de uma computação assíncrona - um resultado que eventualmente aparecerá emFuture após a conclusão do processamento.

Vamos ver como escrever métodos que criam e retornam uma instânciaFuture.

Os métodos de longa execução são bons candidatos para processamento assíncrono e a interfaceFuture. Isso nos permite executar algum outro processo enquanto aguardamos a conclusão da tarefa encapsulada emFuture.

Alguns exemplos de operações que alavancariam a natureza assíncrona deFuture são:

  • processos intensivos computacionais (cálculos matemáticos e científicos)

  • manipulando grandes estruturas de dados (big data)

  • chamadas de método remoto (download de arquivos, descarte de HTML, serviços da Web).

2.1. ImplementandoFutures comFutureTask

Para nosso exemplo, vamos criar uma classe muito simples que calcula o quadrado de umInteger. Isso definitivamente não se encaixa na categoria de métodos de "longa duração", mas vamos colocar uma chamadaThread.sleep() para fazer com que dure 1 segundo para ser concluído:

public class SquareCalculator {

    private ExecutorService executor
      = Executors.newSingleThreadExecutor();

    public Future calculate(Integer input) {
        return executor.submit(() -> {
            Thread.sleep(1000);
            return input * input;
        });
    }
}

O bit de código que realmente executa o cálculo está contido no métodocall(), fornecido como uma expressão lambda. Como você pode ver, não há nada de especial nisso, exceto para a chamadasleep() mencionada anteriormente.

Fica mais interessante quando direcionamos nossa atenção para o uso deCallableeExecutorService.

Callable é uma interface que representa uma tarefa que retorna um resultado e tem um único métodocall(). Aqui, criamos uma instância dele usando uma expressão lambda.

Criar uma instância deCallable não nos leva a lugar nenhum, ainda temos que passar essa instância para um executor que se encarregará de iniciar essa tarefa em uma nova thread e nos devolver o objetoFuture valioso. É aí que entraExecutorService.

Existem algumas maneiras de obtermos uma instânciaExecutorService, a maioria delas são fornecidas pelos métodos de fábrica estáticos da classe de utilitáriosExecutors. Neste exemplo, usamos onewSingleThreadExecutor() básico, o que nos dá umExecutorService capaz de lidar com um único encadeamento por vez.

Uma vez que temos um objetoExecutorService, precisamos apenas chamarsubmit() passando nossoCallable como um argumento. submit() se encarregará de iniciar a tarefa e retornará um objetoFutureTask, que é uma implementação da interfaceFuture.

3. ConsumindoFutures

Até este ponto, aprendemos como criar uma instância deFuture.

Nesta seção, aprenderemos como trabalhar com esta instância, explorando todos os métodos que fazem parte da API deFuture.

3.1. UsandoisDone() eget() para obter resultados

Agora precisamos chamarcalculate()e usar oFuture retornado para obter oInteger resultante. Dois métodos da APIFuture nos ajudarão nessa tarefa.

Future.isDone() nos diz se o executor concluiu o processamento da tarefa. Se a tarefa for concluída, ele retornarátrue, caso contrário, ele retornaráfalse.

O método que retorna o resultado real do cálculo éFuture.get(). Observe que esse método bloqueia a execução até que a tarefa seja concluída, mas em nosso exemplo, isso não será um problema, pois primeiro verificaremos se a tarefa foi concluída chamandoisDone().

Usando esses dois métodos, podemos executar outro código enquanto aguardamos a conclusão da tarefa principal:

Future future = new SquareCalculator().calculate(10);

while(!future.isDone()) {
    System.out.println("Calculating...");
    Thread.sleep(300);
}

Integer result = future.get();

Neste exemplo, escrevemos uma mensagem simples na saída para informar ao usuário que o programa está realizando o cálculo.

O métodoget() bloqueará a execução até que a tarefa seja concluída. Mas não precisamos nos preocupar com isso, pois nosso exemplo só chega ao ponto em queget() é chamado depois de garantir que a tarefa foi concluída. Portanto, neste cenário,future.get() sempre retornará imediatamente.

É importante mencionar queget() tem uma versão sobrecarregada que leva um tempo limite eTimeUnit como argumentos:

Integer result = future.get(500, TimeUnit.MILLISECONDS);

A diferença entreget(long, TimeUnit)eget(), é que o primeiro lançará umTimeoutException se a tarefa não retornar antes do período de tempo limite especificado.

3.2. Cancelando umFuture comcancel()

Suponha que tenhamos acionado uma tarefa, mas, por algum motivo, não nos importamos mais com o resultado. Podemos usarFuture.cancel(boolean) para dizer ao executor para parar a operação e interromper sua thread subjacente:

Future future = new SquareCalculator().calculate(4);

boolean canceled = future.cancel(true);

Nossa instância deFuture do código acima nunca completaria sua operação. Na verdade, se tentarmos chamarget() dessa instância, após a chamada paracancel(), o resultado será aCancellationException. Future.isCancelled() nos dirá se aFuture já foi cancelado. Isso pode ser bastante útil para evitar a obtenção deCancellationException.

É possível que uma chamada paracancel() falhe. Nesse caso, seu valor retornado seráfalse. Observe quecancel() leva um valorboolean como um argumento - isso controla se a thread executando esta tarefa deve ser interrompida ou não.

4. Mais Multithreading comThread Pools

NossoExecutorService atual é de thread único, pois foi obtido com oExecutors.newSingleThreadExecutor. Para destacar esta "rosca única", vamos acionar dois cálculos simultaneamente:

SquareCalculator squareCalculator = new SquareCalculator();

Future future1 = squareCalculator.calculate(10);
Future future2 = squareCalculator.calculate(100);

while (!(future1.isDone() && future2.isDone())) {
    System.out.println(
      String.format(
        "future1 is %s and future2 is %s",
        future1.isDone() ? "done" : "not done",
        future2.isDone() ? "done" : "not done"
      )
    );
    Thread.sleep(300);
}

Integer result1 = future1.get();
Integer result2 = future2.get();

System.out.println(result1 + " and " + result2);

squareCalculator.shutdown();

Agora vamos analisar a saída para este código:

calculating square for: 10
future1 is not done and future2 is not done
future1 is not done and future2 is not done
future1 is not done and future2 is not done
future1 is not done and future2 is not done
calculating square for: 100
future1 is done and future2 is not done
future1 is done and future2 is not done
future1 is done and future2 is not done
100 and 10000

É claro que o processo não é paralelo. Observe como a segunda tarefa é iniciada apenas quando a primeira tarefa é concluída, fazendo com que todo o processo demore cerca de 2 segundos para terminar.

Para tornar nosso programa realmente multi-thread, devemos usar um sabor diferente deExecutorService. Vamos ver como o comportamento do nosso exemplo muda se usarmos um pool de threads, fornecido pelo método de fábricaExecutors.newFixedThreadPool():

public class SquareCalculator {

    private ExecutorService executor = Executors.newFixedThreadPool(2);

    //...
}

Com uma simples mudança em nossa classeSquareCalculator agora temos um executor que é capaz de usar 2 threads simultâneos.

Se executarmos exatamente o mesmo código de cliente novamente, obteremos o seguinte resultado:

calculating square for: 10
calculating square for: 100
future1 is not done and future2 is not done
future1 is not done and future2 is not done
future1 is not done and future2 is not done
future1 is not done and future2 is not done
100 and 10000

Isso está parecendo muito melhor agora. Observe como as 2 tarefas começam e terminam a execução simultaneamente, e todo o processo leva cerca de 1 segundo para ser concluído.

Existem outros métodos de fábrica que podem ser usados ​​para criar pools de threads, comoExecutors.newCachedThreadPool() que reutilizaThreads usado anteriormente quando estão disponíveis, eExecutors.newScheduledThreadPool() que agenda comandos para serem executados após um determinado atraso.

Para obter mais informações sobreExecutorService, leia nossoarticle dedicado ao assunto.

5. Visão geral deForkJoinTask

ForkJoinTask é uma classe abstrata que implementaFuturee é capaz de executar um grande número de tarefas hospedadas por um pequeno número de threads reais emForkJoinPool.

Nesta seção, vamos cobrir rapidamente as principais características deForkJoinPool. Para obter um guia completo sobre o tópico, verifique nossoGuide to the Fork/Join Framework in Java.

Então, a principal característica de aForkJoinTask é que geralmente gerará novas subtarefas como parte do trabalho necessário para completar sua tarefa principal. Ele gera novas tarefas chamandofork()e reúne todos os resultados comjoin(),, portanto, o nome da classe.

Existem duas classes abstratas que implementamForkJoinTask:RecursiveTask, que retorna um valor na conclusão, eRecursiveAction, que não retorna nada. Como os nomes sugerem, essas classes devem ser usadas para tarefas recursivas, como, por exemplo, navegação no sistema de arquivos ou computação matemática complexa.

Vamos expandir nosso exemplo anterior para criar uma classe que, dado umInteger, calculará a soma dos quadrados de todos os seus elementos fatoriais. Assim, por exemplo, se passarmos o número 4 para nossa calculadora, devemos obter o resultado da soma de 4² + 3² + 2² + 1² que é 30.

Em primeiro lugar, precisamos criar uma implementação concreta deRecursiveTaske implementar seu métodocompute(). É aqui que escreveremos nossa lógica de negócios:

public class FactorialSquareCalculator extends RecursiveTask {

    private Integer n;

    public FactorialSquareCalculator(Integer n) {
        this.n = n;
    }

    @Override
    protected Integer compute() {
        if (n <= 1) {
            return n;
        }

        FactorialSquareCalculator calculator
          = new FactorialSquareCalculator(n - 1);

        calculator.fork();

        return n * n + calculator.join();
    }
}

Observe como alcançamos a recursividade criando uma nova instância deFactorialSquareCalculator emcompute(). Ao chamarfork(), um método sem bloqueio, pedimos aForkJoinPool para iniciar a execução desta subtarefa.

O métodojoin() retornará o resultado desse cálculo, ao qual adicionamos o quadrado do número que estamos visitando no momento.

Agora só precisamos criar umForkJoinPool para lidar com a execução e gerenciamento de thread:

ForkJoinPool forkJoinPool = new ForkJoinPool();

FactorialSquareCalculator calculator = new FactorialSquareCalculator(10);

forkJoinPool.execute(calculator);

6. Conclusão

Neste artigo, tivemos uma visão abrangente da interface doFuture, visitando todos os seus métodos. Também aprendemos como aproveitar o poder dos pools de threads para acionar várias operações paralelas. Os principais métodos da classeForkJoinTask,fork()ejoin() também foram brevemente cobertos.

Temos muitos outros excelentes artigos sobre operações paralelas e assíncronas em Java. Aqui estão três deles que estão intimamente relacionados à interfaceFuture (alguns deles já foram mencionados no artigo):

Verifique o código-fonte usado neste artigo em nossoGitHub repository.