Para facilitar um pouco as coisas, a partir do Java 5, a JSR-166 sob o nome Concurrency Utilities, tornou-se disponível, trazendo facilidades para trabalhar com concorrência. Pensando nisso, dada a complexidade do assunto, este artigo visa apresentar o assunto de maneira clara através de exemplos práticos mostrando ao leitor as vantagens oferecidas pela Concurrency Utilities.
Dentre os elementos disponíveis na Concurrency Utility, serão apresentados neste artigo o Framework Executor, Framework Fork/Join e uma breve introdução à Concurrent Collections. Para melhor compreensão desse conteúdo, espera-se que o leitor possua conhecimentos básicos sobre threads.
Considerando a grande maioria dos trabalhos realizados hoje em projetos de sistemas corporativos com Java, poucas são as situações em que se torna necessário implementar soluções utilizando threads de forma manual. Dentre muitos frameworks, servidores de aplicação, web containers e diversas bibliotecas que utilizam threads, muito do trabalho é abstraído para o desenvolvedor.
Entretanto, ainda existem cenários (alguns bem complexos) em que pode ser necessária a implementação de soluções utilizando threads manualmente, sendo geralmente aplicações standalone.
Ainda existem muitas situações onde não se utiliza um servidor de aplicação ou um framework de terceiro para desenvolver uma solução. Dentre muitos desses cenários, o artigo aborda os assuntos sobre Executors, Fork/Join e Concurrent Collections através dos seguintes exemplos práticos:
· Produtor/Consumidor para apresentar a utilização de executors;
· Implementação do Produtor/Consumidor utilizando um pool de threads;
· Implementação de um crawler (BOX 1)para mostrar a utilização de Futures e Callbacks;
· Exemplo de utilização da técnica “dividir para conquistar” para realização de um cálculo de média aritmética utilizando ForkJoinPool.
Crawler, também conhecido por alguns como Spider, são sistemas que navegam pela web indexando conteúdo relevante. Algumas vezes um crawler pode buscar por conteúdo específico de um site pré-determinado. Outras vezes pode ser necessário que o crawler indexe uma lista de URLs, ou até mesmo uma busca geral (por exemplo o crawler do Google que indexa as páginas que encontramos graças ao seu mecanismo de busca).
Se o leitor já tentou desenvolver algo parecido com algum dos exemplos práticos citados, sabe que construir algo assim utilizando-se apenas da API básica de threads é algo bem complicado. Diante disso, o desenvolvedor fica vulnerável a problemas já bem conhecidos por muitos como condições de corrida (race conditions), deadlocks e starvation.
A definição, bem como soluções para os problemas citados estão fora do escopo desse artigo. A seção Links possui referências para ótimos livros sobre concorrência que descrevem conceitos importantes, bem como boas práticas que ajudam o desenvolvedor a não cair em armadilhas.
Devido à dificuldade de se trabalhar com a API básica de threads, alguns projetos foram criados antes da existência da Concurrency Utilities antes do Java 5. Dentre os projetos, podemos citar o projeto que deu origem à Concurrency Utilities, criado por Doug Lea.
O projeto era distribuído através de um arquivo compactado, contendo arquivos fontes os quais o desenvolvedor poderia adicionar diretamente em seu projeto.
Já a partir do Java 5, o pacote java.util.concurrency foi incorporado ao Java através da JSR-166 com o nome Concurrency Utilities. O projeto foi liderado pelo próprio criador do pacote original, ou seja, o próprio Doug Lea. O pacote java.util.concurrency possui diversas funcionalidades disponíveis considerando-se programação concorrente.
As principais funcionalidades fornecidas no pacote java.util.concurrency podem ser divididas em três grandes categorias: o Framework Executor, o Framework Fork/Join, Concurrent Collections e Synchronizers. Este artigo apresenta as principais funcionalidades oferecidas pelos Frameworks Executor e Fork/Join, além de apresentar alguns exemplos utilizando Concurrent Collections.
O Framework Executor
O Framework Executor oferece uma série de recursos que facilitam e muito a vida do desenvolvedor. No entanto, quando deparados com algum projeto que utiliza concorrência, alguns desenvolvedores acabam implementando manualmente todo o processo de gerenciamento de threads através da API básica de concorrência por não conhecer o Framework Executor e os seus mecanismos. Conforme a documentação da Oracle, “Executors são objetos que encapsulam as funções de criação e gerenciamento de Threads”. Criar threads manualmente, gerenciar as instâncias de modo eficiente sem derrubar um sistema por falta de recursos, lidar com tarefas que precisam aguardar a execução de outras tarefas utilizando Thread.wait() e Thread.notify(), não são itens fáceis de implementar. Quando se trabalha utilizando threads de maneira direta, o desenvolvedor é obrigado a controlar tanto a criação quanto a execução dessas threads, utilizando métodos construtores da classe Thread e possivelmente os métodos start(), join(), yield(), etc.
Os executors estão disponíveis através de implementações das interfaces ExecutorService e ScheduledExecutorService. Ambas as interfaces estendem direta ou indiretamente a interface Executor que, por sua vez, define a estrutura fundamental do Framework Executor. Para facilitar a criação de instâncias de executors, a classe Executors fornece factory methods (métodos estáticos utilizados como fábrica de objetos) para isso. Apesar da classe Executors (não confundir a classe Executors com a palavra executors que são instâncias do tipo Executor) oferecer métodos de fábrica para criação padronizada de executors, o desenvolvedor pode criar instâncias manualmente de forma customizada através das classes ThreadPoolExecutor, ForkJoinPool e ScheduledThreadPoolExecutor. Com todo esse ferramental disponível, o desenvolvedor não precisa pensar diretamente nas threads, mas sim em tasks.
Tasks nada mais são que tarefas, definidas através das interfaces Runnable e Callable. A partir deste ponto, adotaremos o termo “tarefas” quando estivermos referenciando as tasks. A Figura 1 mostra um diagrama de classes com as classes mais utilizadas do Framework Executor. Apesar dos mecanismos de agendamento utilizando a interface ScheduledExecutorService serem mencionados, o assunto de agendamento não está no foco do artigo.
Figura 1. Principais classes do Framework Executor
Utilizando executors, a criação e execução das threads são tratadas de maneira separada. Conforme citado anteriormente, o desenvolvedor pode se preocupar apenas com a implementação das tarefas. A execução dessas tarefas é delegada para uma implementação de Executor, através do método execute(), e o mesmo se encarrega de gerenciar a quantidade de threads necessárias para atender a todas as tarefas, podendo até mesmo utilizar o conceito de pool internamente.
O diagrama de classes apresentado na Figura 1 mostra a classe Executors e alguns de seus métodos de fábrica. Através da classe Executors é possível obter:
· Um executor que utiliza uma única thread;
· Um executor que utiliza um pool de threads;
· Executors específicos para agendamento de tarefas.
A classe Executors possui métodos de fábrica (factory methods) e métodos utilitários. Os ...