A especificação da API para utilitários concorrentes para a plataforma Java EE 7 está definida na JSR 236 que pode ser baixada no site oficial da Oracle.

Esse recurso de concorrência que é disponibilizado na plataforma Java EE fornece uma API simples e padronizada para utilização de concorrência pelos componentes da aplicação sem comprometer a integridade do container e ainda mantém os benefícios fundamentais da plataforma Java EE.

Os containers Java EE como os EJBs ou containers Web não permitem diretamente a utilização das APIs de concorrência do Java SE tais como java.util.concurrent.ThreadPoolExecutor, java.lang.Thread, ou java.util.Timer.

Isto ocorre, pois todo o código da aplicação está sendo executado em uma thread gerenciada pelo container, e cada container normalmente espera que todos os acessos a objetos fornecidos pelo container ocorram na mesma thread. Isso permite que o container gerencie os recursos e forneça uma administração centralizada. Além disso, a utilização de recursos de forma não gerenciada não é encorajada, pois pode prejudicar diversos recursos que a plataforma é projetada a fornecer, tais como disponibilidade, segurança, confiabilidade e escalabilidade.

Esta API disponibilizada na plataforma Java EE 7 estende a API Concurrency Utilities que foi desenvolvido sob a especificação JSR-166y e que também é encontrada na Java 2 Platform, Standard Edition 7 (Java SE 7) no pacote java.util.concurrent. A boa notícia é que desenvolvedores de aplicações que já estão familiarizados com essa API poderão utilizar as suas bibliotecas existentes e também usar padrões que já eram utilizados efetuando poucas modificações. Assim, isso permitirá que possamos adicionar princípios de concorrência para as aplicações Java EE existentes usando padrões de projeto existentes.

Veremos no restante do artigo mais funções disponibilizadas pela API de concorrência da plataforma Java EE 7.

Tarefas Assíncronas

A classe ManagedExecutorService presente no pacote javax.enterprise.concurrent é uma versão gerenciada da classe ExecutorService do pacote java.util.concurrent. Esta classe ManagedExecutorService fornece métodos para que possamos submeter tarefas assíncronas no ambiente Java EE.

Podemos obter uma instância de ManagedExecutorService utilizando a pesquisa JNDI. Um ManagedExecutorService padrão está disponível com o JNDI "java:comp/DefaultManagedExecutorService".

Segue na Listagem 1 um exemplo de como poderíamos obter um ManagedExecutorService.

Listagem 1. Obtendo um ManagedExecutorService utilizando uma pesquisa JNDI.

  InitialContext ctx = new InitialContext();
  ManagedExecutorService executor =
  (ManagedExecutorService)ctx.lookup("java:comp/DefaultManagedExecutorService");

Também podemos injetar um ManagedExecutorService na aplicação usando a anotação @Resource. Segue na Listagem 2 um exemplo de como poderíamos obter um ManagedExecutorService utilizando a anotação @Resource:

Listagem 2. Obtendo um ManagedExecutorService utilizando anotações.

  @Resource(lookup="java:comp/DefaultManagedExecutorService")
  ManagedExecutorService executor;

Podemos obter um ManagedExecutorService específico para uma aplicação adicionando o seguinte fragmento de código no arquivo web.xml, presente na Listagem 3.

Listagem 3. Obtendo um ManagedExecutorService específico para aplicação.

  <resource-env-ref>
                  <resource-env-ref-name>
                                 concurrent/meuExecutor
                  </resource-env-ref-name>
   
                  <resource-env-ref-type>
                                 javax.enterprise.concurrent.ManagedExecutorService
                  </resource-env-ref-type>
  </resource-env-ref>

Neste código, o elemento define o nome do JNDI relativo ao recurso "java:comp/env", sendo recomendável que tenhamos a referência do ambiente JNDI no subcontexto de "java:comp/env/concurrent". O elemento define o tipo de referência do ambiente de recursos JNDI. ManagedScheduledExecutor pode então ser obtido com a referência JNDI ou @Resource, mas com o nome "concurrent/meuExecutor".

A unidade de trabalho que precisa ser executada concorrentemente é chamada de tarefa (ou task). Uma tarefa é uma implementação concreta da interface java.lang.Runnable conforme mostra o exemplo da Listagem 4.

Listagem 4. Criando uma tarefa através da implementação da interface Runnable.

  public class MinhaTarefa1 implements Runnable {
                  @Override
                  public void run() {
                                 //Código a ser executado aqui
                  }
  }

Uma tarefa também pode ser definida como uma implementação concreta da interface java.util.concurrent.Callable conforme mostra o exemplo da Listagem 5.

Listagem 5. Criando uma tarefa através da implementação da interface Callable.

  public class MinhaTarefa2 implements Callable<Product> {
                  private int id;
   
                  public MinhaTarefa2(int id) {
                                 this.id = id;
                  }
   
                  @Override
                  public Produto call() {
                                 Produto produto = new Produto(id);
                                 //Mais código aqui
                                 return produto;
                  }
  }

Neste código, Produto é uma classe específica da aplicação. O atributo id fornece um identificador para a tarefa e é usado para inicializar o Produto. O método call é chamado para computar o resultado da tarefa ou então lançar uma exceção caso ocorra algum problema.

Existem basicamente duas diferenças entre uma tarefa que implementa Callable e outra que implementa Runnable. Uma tarefa Callable pode retornam um resultado, enquanto que uma tarefa Runnable não pode. A segunda diferença é que uma tarefa Callable pode lançar uma exceção checada, enquanto que Runnable não pode.

Os beans CDI podem ser submetidos como tarefas, mas é recomendado que eles não estejam nos escopos @RequestScoped, @SessionScoped, ou @ConversationScoped. No entanto, os beans CDI com escopo @Application ou @Dependent podem ser usados como tarefas.

Podemos submeter instancias de uma tarefa para um ManagedExecutorService usando os métodos submit, execute, invokeAll, ou invokeAny.

O método submit submete uma tarefa Runnable ou Callable e retorna um Future representando a tarefa. Segue um exemplo na Listagem 6.

Listagem 6. Submetendo uma tarefa e recebendo um Future como retorno.

  Future future = executor.submit(new MinhaTarefa1());

Outro método que pode ser utilizado é o método execute. O método execute executa uma tarefa Runnable em algum momento no futuro. Segue na Listagem 7 um exemplo.

Listagem 7. Utilizando o método execute para submeter a tarefa.

executor.execute(new MinhaTarefa1());

Já o método invokeAll executa todas as tarefas submetidas em uma Collection e retorna uma List> com os status e resultados quando completadas. Segue na Listagem 8 um exemplo.

Listagem 8. Executando todas as tarefas submetidas na Collection utilizando invokeAll.

  Collection<Callable<Produto>> tarefas = new ArrayList<>();
  tarefas.add(new MinhaTarefa2(1));
  tarefas.add(new MinhaTarefa2(2));
  //Mais código aqui
  List<Future<Produto>> resultados = executor.invokeAll(tarefas);
  Produto resultado = resultados.get(0).get();

Neste código, uma Collection de Callable é criada e submetida através do método invokeAll. O resultado é retornado como List> e o primeiro resultado é acessado. O método invokeAll tem outra variação em que podemos especificar o timeout.

O método invokeAny executa qualquer tarefa submetida na Collection e retorna o resultado de um que tenha completado com sucesso, sem lançar uma exceção. Segue na Listagem 9 um exemplo.

Listagem 9. Executando qualquer uma das tarefas submetidas na Collection utilizando invokeAny.

  Collection<Callable<Produto>> tarefas = new ArrayList<>();
  tarefas.add(new MinhaTarefa2(1));
  tarefas.add(new MinhaTarefa2 (2));
  //Mais código aqui
  Produto resultado = executor.invokeAny(tarefas);

Neste código, uma Collection de Callable é criada e submetida através de invokeAny. O resultado de uma tarefa que tenha sido completada com sucesso é retornado. O método invokeAny tem outra variação que é a possibilidade de especificarmos um timeout.

Transações inseridas nas tarefas também podem ser explicitamente iniciadas, confirmadas (commit) ou revertidas (roll back) através da classe javax.transaction.UserTransaction conforme mostra o exemplo da Listagem 10.

Listagem 10. Utilizando transações nas tarefas.

  public class MinhaTarefa implements Runnable {
                  @Override
                  public void run() {
                                 InitialContext context = new InitialContext();
                                 TransacaoUsuario tx = (TransacaoUsuario)context.lookup("java:comp/ TransacaoUsuario ");
                                 tx.start();
                                 //. . .
                                 tx.commit();
                  }
  }

Cada tarefa também pode implementar a interface ManagedTask para fornecer informações de identificação da tarefa, receber notificação dos eventos do ciclo de vida da tarefa, ou fornecer propriedades de execução adicionais.

Uma tarefa pode implementar também a interface ManagedTaskListener para receber notificações de eventos do ciclo de vida. Esta interface fornece os métodos taskSubmitted, taskStarting, taskDone e taskAborted. O método taskSubmitted é chamado após a tarefa ser submetida para o Executor. O método taskStarting é chamado antes que a tarefa está prestes a começar. O método taskDone é chamado quando uma tarefa submetida completou a execução com sucesso ou não. O método taskAborted é chamado quando uma tarefa foi cancelada.

Segue na Listagem 11 um exemplo de uma tarefa que implementa esses métodos.

Listagem 11. Implementando métodos para receber eventos do ciclo de vida.

  public class MinhaTarefa implements Runnable, ManagedTask, ManagedTaskListener {
                  @Override
                  public void run() {
                                 //Mais código aqui…
                  }
   
                  @Override
                  public void taskSubmitted(Future<?> f, ManagedExecutorService m, Object o) {
                                 //Mais código aqui…
                  }
   
                  @Override
                  public void taskStarting(Future<?> f, ManagedExecutorService m, Object o) {
                                 //Mais código aqui…
                  }
   
                  @Override
                  public void taskAborted(Future<?> f, ManagedExecutorService m, Object o, Throwable t) {
                                 //Mais código aqui…
                  }
   
                  @Override
                  public void taskDone(Future<?> f, ManagedExecutorService m, Object o, Throwable t) {
                                 //Mais código aqui…
                  }
   
  }

Agendamento de Tarefas

A classe ManagedScheduledExecutorService do pacote javax.enterprise.concurrent fornece uma versão gerenciada do ScheduledExecutorService e pode ser usado para agendar tarefas em horários específicos e periódicos.

Podemos obter uma instância de ManagedScheduledExecutorService através de uma pesquisa JNDI usando as referências do ambiente JNDI. Um ManagedScheduledExecutorService padrão está disponível sob o nome JNDIjava:comp/DefaultManagedScheduledExecutorService”.

Podemos então obter o ManagedScheduledExecutorService conforme mostra o exemplo da Listagem 12.

Listagem 12. Obtendo ManagedScheduledExecutorService utilizando a pesquisa JNDI.

  InitialContext ctx = new InitialContext();
  ManagedScheduledExecutorService executor = (ManagedScheduledExecutorService)ctx.lookup("java:comp/DefaultManagedScheduledExecutorService");

Podemos também injetar ManagedScheduledExecutorService na aplicação conforme mostra o exemplo da Listagem 13.

Listagem 13. Injetando ManagedScheduledExecutorService na aplicação.

  @Resource:
  @Resource(lookup="java:comp/DefaultManagedScheduledExecutorService")
  ManagedScheduledExecutorService executor;

Podemos obter um ManagedScheduledExecutorService específico para aplicação adicionando o seguinte fragmento de código no web.xml conforme mostra o exemplo da Listagem 14.

Listagem 14. Definindo um ManagedScheduledExecutorService específico de aplicação.

  <resource-env-ref>
                  <resource-env-ref-name>
                                 concurrent/meuScheduledExecutor
                  </resource-env-ref-name>
   
                  <resource-env-ref-type>
                                 javax.enterprise.concurrent.ManagedScheduledExecutorService
                  </resource-env-ref-type>
  </resource-env-ref>

Neste código, o elemento define o nome JNDI do recurso relativo a “java:comp/env”. Também recomenda-se que as referências do ambiente JNDI estejam no subcontexto “java:comp/env/concurrent”. O elemento define o tipo da referência do ambiente JNDI. O ManagedScheduledExecutorService pode então ser obtido com a referência JNDI e @Resource, mas com o nome concurrent/meuScheduledExecutor.

Assim como no ManagedExectorService, uma tarefa é uma implementação concreta da interface java.lang.Runnable ou java.util.concurrent.Callable.

Submetemos instâncias de tarefas para ManagedScheduledExecutorService usando qualquer um dos métodos submit, execute, invokeAll, invokeAny, schedule, scheduleAtFixedRate, ou scheduleWithFixedDelay.

Os métodos submit, execute, invokeAll, e invokeAny se comportam como no ManagedExecutorService. Os outros métodos são explicados abaixo:

  • O método schedule possui a assinatura " ScheduledFuture schedule(Callable callable, Trigger trigger)". Este método cria e executa uma tarefa baseada em um Trigger.
  • O método schedule com a seguinte assinatura "ScheduledFuture schedule(Runnable command, Trigger trigger)" também cria e executa uma tarefa baseada em um Trigger, porém recebe uma tarefa que implementa um Runnable.
  • O método schedule com a assinatura " ScheduledFuture schedule(Callable callable, long delay, TimeUnit unit)" cria e executa um ScheduledFuture que é ativado após o prazo concedido.
    Exemplificando o método schedule criaremos primeiramente uma tarefa usando Callable, conforme mostra o exemplo da Listagem 15.
    Listagem 15. Criando uma tarefa implementando um Callable.
    public class MinhaTarefa implements Callable<Produto> {
                    @Override
                    public Produto call() {
                                   Produto produto = ...;
                                   //Mais código aqui
                                   return produto;
                    }
    }

    Produto é uma classe específica da aplicação. Agora esta tarefa pode ser agendada conforme a Listagem 16.
    Listagem 16. Agendando a tarefa criada acima.
    ScheduledFuture<Produto> future = executor.schedule(new MinhaTarefa(), 5, TimeUnit.SECONDS);

    Este código agendará a tarefa após cinco segundos. O ScheduledFuture retornado pode ser usado para checar o status da tarefa, cancelar a execução e retornar o resultado.
  • O método schedule com a assinatura "ScheduledFuture schedule(Runnable command, long delay, TimeUnit unit)" cria e executa uma ação de disparo que torna-se ativa após um determinado tempo. Uma tarefa usando Runnable pode ser definida conforme a Listagem 17.
    Listagem 17. Criando uma tarefa implementando um Runnable.
    public class MinhaTarefa implements Runnable {
                    @Override
                    public void run() {
                                   //Mais código aqui
                    }
    }

    Agora esta tarefa pode ser agendada conforme mostra a Listagem 18.
    Listagem 18. Agendando a tarefa criada acima.
    ScheduledFuture<?> f = executor.schedule(new MinhaTarefa(), 5, TimeUnit.SECONDS);

    Este código agendará a tarefa após 5 segundos. O ScheduledFuture retornado pode ser usado para checar o status da tarefa, cancelar a execução e retornar o resultado.
  • O método scheduleAtFixedRate com a assinatura "ScheduledFuture scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)" cria e executa uma ação periódica que é ativado pela primeira vez após o atraso inicial dado, e, posteriormente, com o período determinado. As execuções terão início após initialDelay, depois initialDelay + period, depois initialDelay + 2 * period, e assim por diante. Segue um exemplo na Listagem 19.
    Listagem 19. Exemplo utilizando o método scheduleAtFixedRate.
    ScheduledFuture<?> scheduleAtFixedRate = executor.scheduleAtFixedRate(new MeuRunnableTask(5), 2, 3, TimeUnit.SECONDS);

    Este código agendará a tarefa depois de um atraso inicial de dois segundos e depois a cada três segundos.
    Se qualquer execução da tarefa encontrar uma exceção, execuções subsequentes serão canceladas. No entanto, a tarefa terminará apenas através de cancelamento ou terminação do executor.
    O ScheduledFuture retornado pode ser usado para checar o status da tarefa, cancelar a execução e retornar o resultado.
  • O método scheduleWithFixedDelay com a assinatura "ScheduledFuture scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit)" cria e executa uma ação periódica que é ativada pela primeira vez após o atraso inicial dado, e, posteriormente, com o período determinado entre a terminação de uma execução e o início da próxima. Segue na Listagem 20 um exemplo.
    Listagem 20. Exemplo utilizando o método scheduleWithFixedDelay.
    ScheduledFuture<?> scheduleWithFixedDelay = executor.scheduleWithFixedDelay(new MeuRunnableTask(5), 2, 3, TimeUnit.SECONDS);

    Este código irá agendar a tarefa depois de um atraso inicial de dois segundos e depois a cada três segundos após isso.
    Uma nova tarefa é iniciada apenas após a tarefa anterior ter sido finalizada com sucesso.
    Se qualquer execução da tarefa encontrar uma exceção, execuções subsequentes são canceladas. No entanto, a tarefa será finalizada apenas através de cancelamento ou terminação do executor.
    Cada tarefa pode implementar as interfaces ManagedTask e ManagedTaskListener para fornecer informações de identificação sobre a tarefa, receber notificações de eventos do ciclo de Vida da tarefa, ou fornecer propriedades de execução adicionais. Este comportamento é semelhante ao ManagedExecutorService.

Threads Gerenciadas

A classe ManagedThreadFactory do pacote javax.enterprise.concurrent pode ser usada para criar threads gerenciadas para execução em um ambiente Java EE.

Podemos obter uma instância de ManagedThreadFactory com o JNDI usando referências do ambiente JNDI. Um ManagedThreadFactory padrão está disponível sob o JNDI de nome java:comp/DefaultManagedThreadFactory.

Podemos então obter um ManagedThreadFactory conforme mostra o exemplo da Listagem 21.

Listagem 21. Obtendo um ManagedThreadFactory padrão utilizando JNDI.

  InitialContext ctx = new InitialContext();
  ManagedThreadFactory factory = (ManagedThreadFactory)ctx.lookup("java:comp/DefaultManagedThreadFactory");

Podemos injetar ManagedThreadFactory na aplicação usando @Resource. Segue na Listagem 22 um exemplo.

Listagem 22. Utilizando ManagedThreadFactory na aplicação usando @Resource.

  @Resource(lookup="java:comp/DefaultManagedThreadFactory")
  ManagedThreadFactory factory; 

Podemos obter um ManagedThreadFactory específico de aplicação adicionando o seguinte fragmento de código no arquivo web.xml, presente na Listagem 23.

Listagem 23. Definindo um ManagedThreadFactory específico para aplicação.

  <resource-env-ref>
                  <resource-env-ref-name>
                                 concurrent/meuFactory
                  </resource-env-ref-name>
   
                  <resource-env-ref-type>
                                 javax.enterprise.concurrent.ManagedThreadFactory
                  </resource-env-ref-type>
  </resource-env-ref>

Neste código, o elemento define o nome JNDI para o recurso java:comp/env. Também se recomenda que as referências do ambiente JNDI estejam no subcontexto java:comp/env/concurrent. O elemento define o tipo de referência do ambiente JNDI. O ManagedThreadFactory pode então ser obtido com a referência JNDI e @Resource, mas com o nome de pesquisa concurrent/meuFactory.

Podemos criar novas threads a partir desta factory usando newThread(Runnable r). Segue na Listagem 24 um exemplo.

Listagem 24. Criando uma tarefa.

  public class MinhaTarefa implements Runnable {
                  @Override
                  public void run() {
                                 //Mais código aqui...
                  }
  }

Uma nova thread pode ser criada e iniciada conforme a Listagem 25.

Listagem 25. Criando uma nova thread e iniciando.

  Thread thread = factory.newThread(new MinhaTarefa ());
  thread.start();

As threads retornadas implementam a interface ManageableThread.

Bibliografia

[1] G. Arun, Java EE 7 Essentials. O’Reilly, 2013.

[2] Java Concurrency Utilities. Disponível em https://docs.oracle.com/javase/7/docs/technotes/guides/concurrency/