Processamento assíncrono em Java com Future e FutureTask

Neste artigo vamos apresentar as classes Future e FutureTask, que permitem o processamento de tarefas de forma assíncrona e paralela no Java.

No começo do Java, para a programação de eventos assíncronos, utilizávamos a classe Thread e a interface Runnable, que permitiam o desenvolvimento de aplicações paralelas. O problema é que não é possível retornar um valor ao final da execução. Assim, foram adicionadas as classes FutureTaks, Future e Callable, que tem mais ou menos a mesma função das anteriores, mas facilitam bastante o desenvolvimento de aplicações paralelas. Veja:

Para demonstrar na prática como usá-las, esse artigo apresentará alguns programas simples utilizando as principais funcionalidades de cada uma.

Praticando

O primeiro exemplo mostra como implementar uma tarefa em paralelo que apenas gera números aleatórios. Isso foi feito na classe GerarNumeroAleatorio com a interface Callable. O método que deve ser implementado é o call() da interface, que ao final da execução retorna o valor gerado.

No main é criado um pool de threads e uma tarefa utilizando a classe GerarNumeroAleatorio na variável task. Assim, esse processo é enviado para o pool com o método submit() e nessa submissão é retornado um objeto do tipo Future. Com ele espera-se a execução terminar no while, que fica verificando se a thread terminou de executar com o método isDone(): isso acontecerá quando for retornado true. Então o valor aleatório gerado é retornado. Por fim, o valor retornado é impresso no console e o pool de threads é finalizado com o método shutdown(). A Listagem 1 mostra o código dessa aplicação.

package future; import java.util.Random; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; /** * * Classe que cria um pool de threads e cria uma thread do tipo GerarNumeroAleatorio que apenas gera um numero e o retorna para a classe que * criou a thread. * * @author Eduardo Santana * */ public class Exemplo1Print { private static final ExecutorService threadpool = Executors.newFixedThreadPool(3); public static void main(String args[]) throws InterruptedException, ExecutionException { GerarNumeroAleatorio task = new GerarNumeroAleatorio(); System.out.println("Processando a tarefa ..."); Future<Integer> future = threadpool.submit(task); while (!future.isDone()) { System.out.println("A tarefa ainda não foi processada!"); Thread.sleep(1); // sleep for 1 millisecond before checking again } System.out.println("Tarefa completa!"); long factorial = (long) future.get(); System.out.println("O número gerado foi: " + factorial); threadpool.shutdown(); } // classe que implementa a interface Callable e retorna um numero aleatorio private static class GerarNumeroAleatorio implements Callable<Integer> { @Override public Integer call() { Random rand = new Random(); Integer number = rand.nextInt(100); return number; } } }
Listagem 1. Exemplo utilizando a interface Callable e a classe Future

O resultado da execução do programa é mostrado na Listagem 2, onde é possível verificar que a tarefa não é executa assim que é submetida, então o main não avança enquanto a mesma não é executada. Quando isso acontece, o número gerado é impresso na tela.

Processando a tarefa ... A tarefa ainda não foi processada! A tarefa ainda não foi processada! A tarefa ainda não foi processada! A tarefa ainda não foi processada! A tarefa ainda não foi processada! A tarefa ainda não foi processada! A tarefa ainda não foi processada! A tarefa ainda não foi processada! A tarefa ainda não foi processada! A tarefa ainda não foi processada! A tarefa ainda não foi processada! A tarefa ainda não foi processada! A tarefa ainda não foi processada! A tarefa ainda não foi processada! A tarefa ainda não foi processada! A tarefa ainda não foi processada! A tarefa ainda não foi processada! A tarefa ainda não foi processada! A tarefa ainda não foi processada! A tarefa ainda não foi processada! A tarefa ainda não foi processada! A tarefa ainda não foi processada! A tarefa ainda não foi processada! Tarefa completa! O número gerado foi: 27
Listagem 2. Execução da Listagem 1

A Listagem 3 mostra um exemplo um pouco maior, onde é calculado o fatorial de um número em uma tarefa que será executada em paralelo. Veja que o método retorna um Long com o valor calculado. O método main dessa classe é parecido com o da Listagem 1.

A tarefa é criada na variável task e é enviada para a execução paralela com o método submit(). Ao terminar é retornado o valor calculado que é impresso na tela.

A classe Fatorial é uma implementação da interface Callable e, por isso, implementa o método call(), que tem por objetivo calcular o fatorial do número passado como parâmetro. Para isso, é feito um comando while que vai multiplicando o valor já calculado por um número e subtraindo 1 deste até que ele chegue em 1.

package future; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.logging.Level; import java.util.logging.Logger; public class Exemplo1 { private static final ExecutorService threadpool = Executors.newFixedThreadPool(3); public static void main(String args[]) throws InterruptedException, ExecutionException { Fatorial task = new Fatorial(20); System.out.println("Enviando a tarefa..."); Future<Long> future = threadpool.submit(task); System.out.println("Task is submitted"); while (!future.isDone()) { System.out.println("Tarefa não terminada ainda..."); Thread.sleep(1); // espera para tentar novamente } System.out.println("Tarefa finalizada!"); long factorial = (long) future.get(); System.out.println("Fatorial de 10 é: " + factorial); threadpool.shutdown(); } private static class Fatorial implements Callable<Long> { private final int number; public Fatorial(int number) { this.number = number; } @Override public Long call() { long output = 0; try { output = factorial(number); } catch (InterruptedException ex) { Logger.getLogger(Exemplo1.class.getName()) .log(Level.SEVERE, null, ex); } return output; } private long factorial(int number) throws InterruptedException { if (number < 0) { throw new IllegalArgumentException ("Number must be greater than zero"); } long result = 1; while (number > 0) { result = result * number; number--; } return result; } } }
Listagem 3. Programa para calcular o fatorial de um número de forma paralela

Repare que os dois exemplos anteriores tinham apenas uma thread rodando em paralelo.

Na Listagem 4 vemos a execução do código apresentado.

Enviando a tarefa... Task is submitted Tarefa não terminada ainda... Tarefa não terminada ainda... Tarefa não terminada ainda... Tarefa não terminada ainda... Tarefa não terminada ainda... Tarefa não terminada ainda... Tarefa não terminada ainda... Tarefa não terminada ainda... Tarefa não terminada ainda... Tarefa finalizada! Fatorial de 20 é: 2432902008176640000
Listagem 4. Execução da Listagem 3

Vamos aumentar um pouco a complexidade, onde na Listagem 5 são iniciadas três tarefas utilizando também a classe GerarNumeroAleatorio.

Perceba que as três tarefas foram criadas nas variáveis tarefa1, tarefa2 e tarefa3 e foram enviadas para serem executadas com o método submit().

Ao final da execução é feita a soma do retorno das três threads e esse valor é exibido no console.

A execução desse código é apresentada na Listagem 6.

package future; import java.util.Random; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; public class Exemplo1Soma { private static final ExecutorService threadpool = Executors.newFixedThreadPool(3); public static void main(String args[]) throws InterruptedException, ExecutionException { GerarNumeroAleatorio tarefa1 = new GerarNumeroAleatorio(); GerarNumeroAleatorio tarefa2 = new GerarNumeroAleatorio(); GerarNumeroAleatorio tarefa3 = new GerarNumeroAleatorio(); System.out.println("Processando a tarefa ..."); Future<Integer> futureT1 = threadpool.submit(tarefa1); Future<Integer> futureT2 = threadpool.submit(tarefa2); Future<Integer> futureT3 = threadpool.submit(tarefa3); while (!futureT1.isDone() && futureT2.isDone() && futureT3.isDone()) { System.out.println("As tarefas ainda não foram processadas!"); Thread.sleep(1); // sleep for 1 millisecond before checking again } System.out.println("Tarefa completa!"); long valor = futureT1.get(); valor = valor + futureT2.get() + futureT3.get(); System.out.println("A soma dos valores gerados são: " + valor); threadpool.shutdown(); } private static class GerarNumeroAleatorio implements Callable<Integer> { @Override public Integer call() { Random rand = new Random(); Integer number = rand.nextInt(100); System.out.println("Valor Gerado: " + number); return number; } } }
Listagem 5. Somando valores gerados por várias Threads

É possível verificar na Listagem 6 que as três tarefas são executadas em paralelo, gerando cada uma um valor individual. Ao final das três execuções os três valores são somados e essa soma é apresentada na tela.

Processando a tarefa ... As tarefas ainda não foram processadas!; As tarefas ainda não foram processadas!; As tarefas ainda não foram processadas!; As tarefas ainda não foram processadas!; As tarefas ainda não foram processadas!; As tarefas ainda não foram processadas!; Tarefa completa! Valor Gerado: 21 Valor Gerado: 24 Valor Gerado: 49 A soma dos valores gerados são: 94
Listagem 6. Execução da Listagem 5

Agora vamos implementar um exemplo um pouco maior. Na Listagem 7 é mostrado um servidor simples que recebe conexões na porta 8000. Ele é iniciado com uma thread onde é criada uma nova tarefa com o método execute. Esta é implementada com a classe VerificaRequisicao, que de 10 em 10 segundos verifica o status de todas as conexões feitas com o servidor, isto é, se ela foi cancela ou se foi terminada com sucesso.

Além disso, é necessário tratar a requisição de cada usuário e para isso foi implementado um processo bem simples, onde o servidor recebe uma mensagem do cliente e a responde com outra mensagem. Para isto utilizamos a classe TrataRequisicao, que recebe como parâmetro um Socket aberto com o cliente que fez a requisição. Foi colocado um Thread.sleep de cinco segundos na Thread que fica analisando as requisições que ainda não terminaram.

package future; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; import java.net.SocketTimeoutException; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; /** * * Implementacao de um servidor bem simples para demonstrar as varias formas de implementar programacao paralela com Java. * * @author Eduardo Santana * */ public class Servidor implements Runnable { private final ServerSocket serverSocket; private final ExecutorService pool; List<Future> requisicoes = new ArrayList<Future>(); public static void main(String args[]) throws IOException { // cria uma Thread nova eexcutando o servidor System.out.println("Servidor no ar"); new Thread(new Servidor(8000, 3)).run(); } public Servidor(int port, int poolSize) throws IOException { serverSocket = new ServerSocket(port); pool = Executors.newFixedThreadPool(poolSize); } public void run() { // run the service try { //cria uma tarefa para a verificação das requisições pool.execute(new VerificaRequisicao(requisicoes)); while (true) { System.out.println("Nova requisição!"); Future req = pool.submit(new TrataRequisicao (serverSocket.accept())); // armazena todas as requisições requisicoes.add(req); } } catch (IOException ex) { pool.shutdown(); } } } // classe que executa de 10 em 10 segundos e verifica o status das requisições class VerificaRequisicao implements Runnable { private final List<Future> requisicoes; VerificaRequisicao(List<Future> requisicoes) { this.requisicoes = requisicoes; } public void run() { while (true) { int somaTerminadas = 0; int somaCanceladas = 0; int somaEmExecucao = 0; try { Thread.sleep(10000); for (Future f : requisicoes) { if (f.isDone()) { somaTerminadas++; } else if (f.isCancelled()) { somaCanceladas++; } else if (!f.isDone()) { somaEmExecucao++; } } System.out.println("Terminadas: " + somaTerminadas); System.out.println("Canceladas: " + somaCanceladas); System.out.println("Execução: " + somaEmExecucao); } catch (InterruptedException e) { e.printStackTrace(); } } } } // classe que recebe a requisição do cliente e a responde class TrataRequisicao implements Runnable { private final Socket server; TrataRequisicao(Socket server) { this.server = server; } public void run() { try { System.out.println("Conectado a: " + server.getRemoteSocketAddress()); DataInputStream in = new DataInputStream(server.getInputStream()); System.out.println(in.readUTF()); DataOutputStream out = new DataOutputStream(server.getOutputStream()); Thread.sleep(5000); out.writeUTF("Sua conexão terminou! Tchau!"); server.close(); } catch (SocketTimeoutException s) { System.out.println("Socket timed out!"); } catch (IOException e) { e.printStackTrace(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }
Listagem 7. Servidor com várias Threads

Apenas para verificar o funcionamento do servidor foi implementado um cliente bem simples, conforme mostra a Listagem 8. A conexão é feita no endereço localhost da porta 8000. Depois de conectado, o cliente envia uma mensagem para o servidor e fica esperando a resposta.

package future; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; /** * * Cliente do servidor desenvolvido para testar as classes de execução em paralelo * * @author Eduardo Santana * */ public class Cliente { public static void main(String[] args) { String serverName = "localhost"; int port = 8000; try { System.out.println("Iniciando a conexão!"); //faz a conexão Socket client = new Socket(serverName, port); System.out.println("Conectado a: " + client.getRemoteSocketAddress()); OutputStream outToServer = client.getOutputStream(); // envia a mensagem para o servidor DataOutputStream out = new DataOutputStream(outToServer); out.writeUTF("Olá Servidor! " + client.getLocalSocketAddress()); // recebe a resposta do servidor InputStream inFromServer = client.getInputStream(); DataInputStream in = new DataInputStream(inFromServer); //imprime a resposta System.out.println("Resposta: " + in.readUTF()); client.close(); } catch (IOException e) { e.printStackTrace(); } } }
Listagem 8. Cliente que conecta ao servidor

O resultado da execução do servidor é mostrado na Listagem 9, onde é possível verificar as várias requisições feitas pelo cliente e a execução da Thread que examina os status da conexão. Esse status é sempre atualizado com as condições atuais do servidor. Vemos que na primeira execução dessa tarefa não existia ainda nenhuma requisição, enquanto na segunda existiam quatro execuções ainda pendentes e uma terminada; já na terceira existiam cinco terminadas e três pendentes, e finalmente, na última execução todas as oito requisições já haviam sido respondidas.

Servidor no ar Nova requisição! Verifica o status do servidor! Terminadas: 0 Canceladas: 0 Execução: 0 Nova requisição! Conectado a: /127.0.0.1:54841 Olá Servidor! /127.0.0.1:54841 Nova requisição! Conectado a: /127.0.0.1:54842 Olá Servidor! /127.0.0.1:54842 Nova requisição! Conectado a: /127.0.0.1:54845 Olá Servidor! /127.0.0.1:54845 Nova requisição! Nova requisição! Verifica o status do servidor! Terminadas: 1 Canceladas: 0 Execução: 4 Conectado a: /127.0.0.1:54846 Olá Servidor! /127.0.0.1:54846 Nova requisição! Conectado a: /127.0.0.1:54848 Olá Servidor! /127.0.0.1:54848 Nova requisição! Conectado a: /127.0.0.1:54850 Olá Servidor! /127.0.0.1:54850 Nova requisição! Conectado a: /127.0.0.1:54861 Olá Servidor! /127.0.0.1:54861 Verifica o status do servidor! Terminadas: 5 Canceladas: 0 Execução: 3 Conectado a: /127.0.0.1:54889 Olá Servidor! /127.0.0.1:54889 Verifica o status do servidor! Terminadas: 8 Canceladas: 0 Execução: 0
Listagem 9. Execução da Listagem 8

Apenas como curiosidade, a resposta do servidor para o cliente é apenas uma mensagem informando que a conexão está concluída, como mostra o código a seguir:

Iniciando a conexão! Conectado a: localhost/127.0.0.1:8000 Resposta: Sua conexão terminou! Tchau!

Com a classe FutureTask e as interfaces Callable e Futere a programação de tarefas assíncronas em Java ficou muito mais fácil, evitando algumas “gambiarras” que antes eram necessárias para a implementação desse tipo de aplicação.

Espero que esse artigo seja útil! Até a próxima!

Ebook exclusivo
Dê um upgrade no início da sua jornada. Crie sua conta grátis e baixe o e-book

Artigos relacionados