Embora os livros clássicos de padrões de projeto como o livro do GoF não citem a programação assíncrona como um padrão de projeto, este modelo de programação é um dos mais populares e importantes da década passada. Esse modelo de programação assíncrona se baseia na ideia de um ambiente multithread e na execução de funcionalidades em threads separadas. Pode-se verificar que não são apenas os ambientes multithreads e as linguagens de programação que tiram proveito das técnicas de programação assíncrona, mas também as plataformas que operam com uma única thread, tais como as populares plataformas de JavaScript como o Node.js, que também fazem uso dos princípios da programação assíncrona. Além disso, os frameworks de interface gráfica com usuário (UI – user interface) também utilizam bastante as chamadas assíncronas principalmente para manter as interfaces ativas e responsivas.

O Java foi projetado para suportar múltiplas threads desde o início do seu desenvolvimento. Contudo, o Java falhou ao fornecer uma única forma de fazer chamadas assíncronas. A interface Future<T> introduzida no Java 5 foi a primeira tentativa de implementar a programação assíncrona, mas isso era pesado e complicado de usar. Nas próximas versões do Java foi incluída a anotação @Asynchronous que facilitou bastante a implementação das chamadas assíncronas. Os Servlets também forneceram um conjunto melhor de ferramentas para ajudar na programação assíncrona.

A programação assíncrona também pode ser útil no lado servidor (ou backend). No início, as plataformas J2SE e J2EE não ofereciam uma implementação simples para programação assíncrona. Porém, no Java 5 o framework Concurrency foi lançado com base na especificação JSR166. A JSR166 incluía diversos utilitários que tornava a programação assíncrona possível, sendo mais fácil e melhor controlada. Além disso, a interface Future<V> também fornecia uma forma de ajudar os desenvolvedores a implementar uma execução assíncrona. Entretanto, o Spring ofereceu chamadas de métodos assíncronos, que estavam disponíveis com anotações. Por sua vez, a plataforma Java EE não incluiu até então nenhuma solução conveniente até o lançamento da versão 6.

No restante do artigo veremos como implementar o padrão como um POJO (Plain Old Java Objects). Também será visto como o padrão Assíncrono pode ser implementado na plataforma Java EE.

Padrão Assíncrono

A programação assíncrona não está especificada nas principais bibliografias de padrões de projeto, porém, se estivesse ela seria mais bem descrita como: "O Padrão de Projeto Assíncrono fornece uma forma de invocar um método sem bloquear o chamador".

A forma natural da execução de um método é o bloqueio do chamador até que o método chamado finalize a sua execução. Apesar desse comportamento ser simples e esperado ele pode não seu um comportamento desejado em alguns casos. Praticamente todos os frameworks de interface gráfica com o usuário e as plataformas da web trabalham com o não bloqueio das requisições.

O padrão assíncrono se baseia na abordagem "fire and forget" em que uma operação é feita em paralelo ou de forma a não bloquear a thread executora, e o resultado é verificado quando ela está pronta. Geralmente a abordagem assíncrona faz uso da execução paralela.

O restante do artigo mostra de forma prática o funcionamento do padrão Assíncrono.

Implementando um Asynchronous em Código Puro (POJO)

O Java suporta a utilização de Threads de forma bastante simples conforme mostra o exemplo da Listagem 1.

Listagem 1. Exemplo de utilização de Threads com Java.


  public class ExemploAssincronoRunnable implements Runnable {
           public void run() {
   
                     System.out.println("Executando Thread Assíncrona!");
   
           }
  }

Para executar o código visto devemos inicializá-lo em uma Thread e invocar o método "run()" chamando o método "start()" da nova Thread criada, conforme mostra o exemplo a seguir:

(new Thread(new ExemploAssincronoRunnable())).start();

Embora muitos desenvolvedores utilizem essa abordagem mostrada anteriormente, também existe outra forma de iniciar um processo em uma Thread separada. Para isso, basta estender a classe Thread e sobrescrever o método "run()" conforme mostra o exemplo da Listagem 2.

Listagem 2. Exemplo de outra abordagem para criação de Threads através a extensão da classe Thread.


  public class ExemploAssincronoThread extends Thread {
   
           public void run() {
                     System.out.println("Executando Thread Assíncrona!");
   
           }
  }

Para executar a classe basta instanciá-la e então chamar o método "start()", conforme mostra o código a seguir:

(new ExemploAssincronoThread()).start();

Quando se trabalha com Threads duas operações são bastante utilizadas, são elas: a operação "sleep()" e a operação "join()". Ambas as operações lançam uma exceção InterruptedException.

O método "sleep()" faz com que a thread fique inativa por um período específico que é dado em milissegundos. O exemplo a seguir coloca uma thread no estado "sleep" ou pausado por um segundo:

Thread.sleep(1000);

Por outro lado, o método "join()" faz com que uma thread aguarde pela finalização da execução de outra thread.

Como exemplo pode-se considerar uma thread chamada t1 que precisa de um recurso de outra thread chamada t2. Para implementar t1 de forma que essa aguarde a finalização de t2 deve-se chamar join em t1 para t2 conforme mostra o exemplo a seguir:

t2.join();

Uma das abordagens mais conhecidas e mais utilizadas para programação assíncrona em Java é utilizar a interface Future<V>. Esta interface possibilita o uso do objeto Proxy, que oferece uma referência para o objeto Future. Devido ao framework Concurrency não oferecer suporte a anotações para execução assíncrona, a interface Future é acoplada a um ExecutorService, que é parte do framework Concurrency.

O exemplo da Listagem 3 usa um serviço executor para completar uma tarefa enquanto ela retorna uma referência para a interface Future com seus tipos genéricos apropriados.

Listagem 3. Exemplo de utilização da interface Future e de ExecutorService.


  ExecutorService executor = Executors.newSingleThreadExecutor();
  Future<String> testeFuture = executor.submit(new Callable<String>() {
           public String call() {
                     return "Teste de execução!!";
           }
  }
  );
   
  //..
   
  if (testeFuture.isDone())
           System.out.println(testeFuture.get());

A classe FutureTask é uma implementação da interface Future<T>, que implementa a interface Runnable e pode ser executada diretamente, conforme mostra o exemplo da Listagem 4.

Listagem 4. Exemplo de utilização de FutureTask.


  FutureTask<String> futureTask = new FutureTask<String>(new Callable<String>() {
           public String call() {
                     return "Teste de execução!!";
           }
  });
   
  executor.execute(futureTask);

Esta execução pode ser cancelada através de uma chamada ao método "cancel" que tem a seguinte assinatura:

cancel(boolean mayInterruptIfRunning)

Se o parâmetro “mayInterruptIfRunning” estiver configurado para true, chamadas para o método “SessionContext.wasCancelled()” retornam true. Caso contrário, a chamada para o método “SessionContext.wasCancelled()” retorna false. Outra funcionalidade que pode ser utilizada é verificar o status de cancelamento através do método "isCancelled()" que retorna true se o cancelamento foi realizado com sucesso.

O framework Concurrency especificado na JSR-133 é uma ótima ferramenta para threads e concorrência, assim como o Fork/Join para Java 7.

Na próxima seção será visto como implementar o padrão assíncrono na plataforma Java EE.

Implementando Asynchronous na plataforma Java EE

A primeira forma de implementar o padrão assíncrono na plataforma Java EE é através do Asynchronous Beans. O Java EE suporta a programação assíncrona de diversas formas, a mais simples é através das anotações. Para isso, basta anotar um método com a anotação @Asynchronous. Isso já é suficiente para que o container Java EE execute o método chamado em uma thread separada. Segue na Listagem 5 um exemplo de um bean assíncrono.

Listagem 5. Exemplo de um Bean Assíncrono.


  package br.com.devmedia.exemploassincrono;
   
  import javax.annotation.PostConstruct;
  import javax.ejb.Singleton;
  import javax.ejb.Startup;
  import java.util.logging.Logger;
  import javax.ejb.Asynchronous;
   
  @Startup
  @Singleton
  public class ExemploMeuBeanAssincrono {
           private Logger logger;
   
           @PostConstruct
           public void start() {
                     //inicialização do logger
                     logger = Logger.getLogger("ExemploLogGlobal");
                     logger.info("Primeira mensagem logada!!!");
           }
   
           public void logInfoSincrono(String msg){
                     logger.info(msg);
           }
   
           @Asynchronous
           public void logInfoAssincronamente(String msg){
                     logger.info(msg);
           }
  }

No exemplo mostrado o método “logInfoAssincronamente()” é executado assincronamente, diferente do método “logInfoSincrono()”.

Para que possa ser observado o comportamento assíncrono basta adicionar uma chamada a “Thread.sleep()” nos dois métodos conforme mostra o exemplo da Listagem 6.

Listagem 6. Exemplo dos métodos anteriores modificados para visualização do comportamento assíncrono.


  public void logInfoSincrono(String msg) {
   
           logger.info(“Entrando no log síncrono”);
   
           try {
                     Thread.sleep(1000);
           } catch (InterruptedException e) {}
                     
           logger.info(msg);
  }
   
  @Asynchronous
  public void logInfoAssincrono(String msg) {
   
           logger.info(“Entrando no log assíncrono”);
   
           try {
                     Thread.sleep(13000);
           } catch (InterruptedException e) {}
   
           logger.info(msg);
  }

Também pode ser utilizado um “System.out.println()” para os exemplos mostrados caso a informação queira ser visualizada no console.

Por fim, deve-se criar um novo bean para chamar ambas as funções em ordem, conforme mostra o exemplo da Listagem 7.

Listagem 7. Exemplo de como testar os métodos anteriores e seus devidos comportamentos.


  package br.com.devmedia.exemploassincrono;
   
  import javax.annotation.PostConstruct;
  import javax.ejb.Singleton;
  import javax.ejb.Startup;
   
  @Startup
  @Singleton
  public class TestaBeanAssincrono {
   
           @EJB
           ExemploMeuBeanAssincrono beanAssincrono;
           @PostConstruct
           public void testaLoggers() {
                     System.out.println("Chamando método assíncrono...");
                     beanAssincrono.logInfoAssincrono("Método assíncrono executado.");
                     System.out.println("Chamando método síncrono...");
                     beanAssincrono.logInfoSincrono("Método síncrono executado.");
                     System.out.println("Finalizado");
           }
  }

Assim, a saída será ser na ordem primeiramente a mensagem “Chamando método assíncrono...”, “Entrando no log assíncrono”, “Chamando método síncrono...”, “Entrando no log síncrono”, “Método síncrono executado”, “Finalizado” e por fim “Método assíncrono executado”.

Dessa forma, tem-se que após executar o método “testaLoggers()” são chamados os métodos “logInfoAssincrono()” e “logInfoSincrono()”. Ambos os métodos pausam a execução das threads por um tempo determinado. Como pode ser visto a partir da saída do console, o método assíncrono “logInfoAssincrono()” foi chamado e entrou em um longo tempo de espera que é de 13000 milissegundos, mas ele não bloqueia a execução do método “logInfoSincrono()”. O método “logInfoSincrono()” dorme por um tempo, mas retorna o controle para o método chamador e imprime a mensagem "Finalizado". Finalmente, o método “logInfoAssincrono()” acorda e termina imprimindo no console.

Este exemplo mostra claramente que a chamada assíncrona não bloqueia a thread chamadora, e também não bloqueia o método “logInfoSincrono()” conforme mostrado no exemplo. No entanto, quando o método “logInfoSincrono()” vai para o estado de sleep, o método chamador aguarda até que essa pausa termine, isso ocorre porque ele não é um método assíncrono que está rodando em outra thread de execução. A anotação @Asynchronous é uma maneira fácil de implementar o comportamento assíncrono e pode ser adicionado a quase qualquer método em qualquer momento durante e após o desenvolvimento.

Outra forma de implementar o padrão assíncrono na plataforma Java EE é utilizando Asynchronous Servlets. Com isso pode-se implementar um servlet assíncrono.

A especificação Servlet 3.0 detalhada na JSR315 contém grandes melhorias e atualizações para suportar um modelo de execução assíncrona. Além disso, possui também configurações mais fáceis de serem realizadas, entre outras facilidades que foram incorporadas.

Os Servlets assíncronos se baseiam basicamente em melhorias no Hypertext Transfer Protocol (HTTP) 1.1. Na versão HTTP 1.0 cada conexão é usada para enviar e receber uma única requisição e resposta, diferente do HTTP 1.1 em que se permitiu que as aplicações web pudessem manter as conexões ativas e enviarem assim múltiplas requisições.

Em uma implementação padrão o backend do Java precisa de uma thread separada constantemente anexada a conexão HTTP. Entretanto, a API Java Nonblocking I/O ou NIO recicla as threads entre as requisições ativas.

Atualmente todos os servidores web compatíveis com a especificação Servlet 3.0 suportam o Java NIO.

O Servlet 3.0 introduziu o método “startAsync()” que permite as operações assíncronas conforme mostra o exemplo da Listagem 8.

Listagem 8. Exemplo utilizando o método startAsync().


  package br.com.devmedia.exemploassincrono;
   
  import java.io.*;
  import javax.servlet.*;
  import javax.servlet.annotation.*;
  import javax.servlet.http.*;
   
  @WebServlet(urlPatterns={"/assincrono"}, asyncSupported=true)
  public class ExemploServletAssincrono extends HttpServlet {
   
           @Override
           protected void doGet(HttpServletRequest req, HttpServletResponse res)
                     throws IOException, ServletException {
   
                     final AsyncContext asyncContext = req.startAsync();
                     final String informacao;
   
                     asyncContext.addListener(new AsyncListener() {
                              @Override
                              public void onComplete(AsyncEvent event) throws IOException {
                                        AsyncContext asyncContext = event.getAsyncContext();
                                        asyncContext().getWriter().println(informacao);
                              }
   
                              @Override
                              public void onTimeout(AsyncEvent event) throws IOException {
                                        // Código aqui se ocorrer timeout
                              }
   
                              @Override
                              public void onError(AsyncEvent event) throws IOException {
                                        // Código aqui se ocorrer erro
                              }
   
                              @Override
                              public void onStartAsync(AsyncEvent event) throws IOException {
                                        // Código aqui que deve ser executado na inicialização
                              }
                     });
   
                     new Thread() {
                              @Override
                              public void run() {
                                        asyncContext.complete();
                              }
                     }.start();
   
                     res.getWriter().write("Resultados:");
   
                     informacao = "Informação pesquisada";
           }
  }

Este Servlet imprime "Resultados:" e depois imprime a informação que pode ser recuperada de uma base de dados, mas para este exemplo trata-se de uma simples string. Como pode ser visto deve ser iniciada uma thread separada. O método “onComplete” do Listener AsyncListener é executado apenas quando a execução estiver completa. Existem vários outros métodos do ciclo de vida do AsyncListener, são eles: “onStartAsync” que executa quando o contexto assíncrono é iniciado, “onTimeOut” que executa quando um timeout ocorre e “onError” que executa apenas se um erro é recebido.

Pode ser notado que o código mostrado não é muito claro e limpo. Assim, a especificação Servlet 3.1 fornece uma forma ainda mais fácil para implementar servlets assíncronas onde se utiliza pools de threads gerenciadas e o serviço executor. Na Listagem 9 temos um exemplo utilizando ManagedThreadFactory para criar uma nova thread.

Listagem 9. Exemplo utilizando ManagedThreadFactory para criação de uma nova thread.


  package br.com.devmedia.exemploassincrono;
   
  import java.io.*;
  import javax.servlet.*;
  import javax.servlet.annotation.*;
  import javax.servlet.http.*;
   
  @WebServlet(urlPatterns="/assincrono", asyncSupported=true)
  public class ExemploServletAssincrono extends HttpServlet {
   
           @Resource
           private ManagedThreadFactory factory;
   
           @Override
           protected void doGet(HttpServletRequest req, HttpServletResponse res)
                     throws ServletException, IOException {
   
                     final AsyncContext asyncContext = req.startAsync();
                     final PrintWriter writer = res.getWriter();
   
                     Thread thread = factory.new Thread(new Runnable() {
                              @Override
                              public void run() {
                                        writer.println("Execução completa!");
                                        asyncContext.complete();
                              }
                     });
   
                     thread.start();
           }
  }

ManagedThreadFactory serve como uma thread disponível do pool que precisa ser iniciada implicitamente.

Outra forma é submeter diretamente para o ManagedExecutorService ao invés de criar e iniciar a thread no servlet conforme mostra o exemplo da Listagem 10.

Listagem 10. Exemplo de como delegar para o ExecutorService.


  package br.com.devmedia.exemploassincrono;
   
  import java.io.*;
  import javax.servlet.*;
  import javax.servlet.annotation.*;
  import javax.servlet.http.*;
   
  @WebServlet(urlPatterns="/assincrono", asyncSupported=true)
  public class ExemploServletAssincrono extends HttpServlet {
   
           @Resource
           private ManagedExecutorService executor;
   
           @Override
           protected void doGet(HttpServletRequest req, HttpServletResponse res)
                     throws ServletException, IOException {
   
                     final AsyncContext asyncContext = req.startAsync();
                     final PrintWriter writer = res.getWriter();
   
                     executor.submit(new Runnable() {
   
                              @Override
                              public void run() {
                                        writer.println("Execução completa!");
                                        asyncContext.complete();
                              }
   
                     });
           }
  }

Assim, pode-se verificar que servlets assíncronas também fornecem uma implementação limpa e clara podendo ser utilizado sem problemas.

Bibliografia

[1] Erich Gamma, Ricard Helm, Ralph Johnson, John Vlissides. Design Patterns: Elements of Reusable Object-Oriented Software (Addison-Wesley, 1994).

[2] Eric Freeman, Elisabeth Robson, Bert Bates, Kathy Sierra. Head First Design Patterns. O'Reilly Media, 2004.

[3] Murat Yener, Alex Theedom. Proffesional Java EE Design Patterns. Wrox, 2015.