As coleções simultâneas foram uma grande inclusão no Java™ 5, mas muitos desenvolvedores Java ignoraram essa inclusão devido à grande propaganda sobre anotações e genéricos. Além disso, e para falar a verdade, muitos desenvolvedores evitam esse pacote porque acreditam que ele, assim como os problemas que ele pretende resolver, é complicado.
Na verdade, o java.util.concurrent contém muitas classes que resolvem de forma efetiva problemas de simultaneidade comuns, sem a necessidade de um trabalho árduo. Leia sobre como as classes java.util.concurrent como CopyOnWriteArrayList e BlockingQueue ajudam a resolver os desafios prejudiciais de programação multiencadeada.
Apesar de não ser uma classe de coleções por assim dizer, a enumeração java.util.concurrent.TimeUnit facilita muito a leitura do código. O uso do TimeUnit livra os desenvolvedores que usam método ou API do controle do milissegundo.
O TimeUnit incorpora todas as unidades de tempo, desde de MILISSEGUNDOS e MICROSSEGUNDOS até DIAS e HORAS, o que significa que ele manipula praticamente todos os tipos de intervalos de tempo necessários para o desenvolvedor. E, graças aos métodos de conversão declarados na enumeração, é ainda mais simples converter HORAS em MILISSEGUNDOS quando o tempo acelera.
A execução de uma nova cópia de um array é uma operação muito cara, em termos de tempo e sobrecarga de memória, para ser usada normalmente, muitas vezes os desenvolvedores preferem usar um ArrayList sincronizado.
No entanto, essa também é uma opção cara, pois todas as vezes que é repetida nos conteúdos de uma coleção é necessário sincronizar todas as operações, incluindo leitura e gravação, para garantir a consistência.
Isso retrocede a estrutura de custo em cenários nos quais diversos leitores leem o ArrayList , mas poucos o modificam.
CopyOnWriteArrayList é o que resolve esse problema. Seu Javadoc define CopyOnWriteArrayList como uma "variante thread-safe do ArrayList na qual todas as operações variáveis (add, set entre outras) são implementadas através da criação de uma nova cópia do array".
A coleção copia internamente o seu conteúdo para um novo array em caso de modificação, dessa forma os leitores que acessam os conteúdos do array não ficam sujeitos aos custos de sincronização (pois não estão operando em dados variáveis).
Basicamente, o CopyOnWriteArrayList é ideal para o cenário no qual o ArrayList falha: leitura frequente e gravação rara, como os ouvintesde um evento JavaBean.
A interface BlockingQueue declara ser uma Fila, o que significa que seus itens são armazenados na ordem first in, first out (FIFO). Os itens inseridos em uma ordem específica são recuperados na mesma ordem — mas com a inclusão da garantia de que qualquer tentativa de recuperação de um item de uma fila vazia irá bloquear o encadeamento de chamada até que o item esteja pronto para ser recuperado. Da mesma forma, qualquer tentativa de inserção de um item em uma fila cheia irá bloquear o encadeamento de chamada até que haja espaço disponível no armazenamento da fila.
O BlockingQueue resolve perfeitamente o problema de como "transferir" itens reunidos por um encadeamento para outro encadeamento para processamento, sem a preocupação com problemas de sincronização. A trilha de blocos protegidos no tutorial do Java é um bom exemplo. Ela desenvolve um buffer delimitado com um único intervalo usando sincronização manual e wait()/notifyAll() para sinalizar entre os encadeamentos quando um novo item está disponível para consumo e quando o intervalo está pronto para ser preenchido com um novo item. (Consulte a seção de implementação de blocos protegidos para obter mais detalhes.)
Apesar do código no tutorial de blocos protegidos funcionar, ele é longo, complicado e não é totalmente intuitivo. No começo da plataforma Java, os desenvolvedores realmente tinham que lidar com tais códigos, mas em 2010— as coisas certamente melhoraram, não?
A Listagem 1 mostra uma versão reescrita do código dos blocos protegidos na qual foi empregado o ArrayBlockingQueue ao invés da versão manual Drop.
Listagem 1. BlockingQueue
import java.util.*;
import java.util.concurrent.*;
class Producer
implements Runnable
{
private BlockingQueue<String> drop;
List<String> messages = Arrays.asList(
"Mares eat oats",
"Does eat oats",
"Little lambs eat ivy",
"Wouldn't you eat ivy too?");
public Producer(BlockingQueue<String> d) { this.drop = d; }
public void run()
{
try
{
for (String s : messages)
drop.put(s);
drop.put("DONE");
}
catch (InterruptedException intEx)
{
System.out.println("Interrupted! " +
"Last one out, turn out the lights!");
}
}
}
class Consumer
implements Runnable
{
private BlockingQueue<String> drop;
public Consumer(BlockingQueue<String> d) { this.drop = d; }
public void run()
{
try
{
String msg = null;
while (!((msg = drop.take()).equals("DONE")))
System.out.println(msg);
}
catch (InterruptedException intEx)
{
System.out.println("Interrupted! " +
"Last one out, turn out the lights!");
}
}
}
public class ABQApp
{
public static void main(String[] args)
{
BlockingQueue<String> drop = new ArrayBlockingQueue(1, true);
(new Thread(new Producer(drop))).start();
(new Thread(new Consumer(drop))).start();
}
} |
O ArrayBlockingQueue também cumpre a "integridade" — o que significa que ele pode conceder aos encadeamentos de leitura e gravação o acesso primeiro a entrar, primeiro a sair. A alternativa seria uma política mais eficiente que poderia deixar alguns encadeamentos sem processamento. (Ou seja, seria mais eficiente permitir a execução de leitores enquanto outros leitores permanecem bloqueados, mas isso poderia gerar um fluxo constante de encadeamentos de leitores o que impediria os gravadores de executarem sua tarefa.)
O BlockingQueue também oferece suporte para métodos que usam parâmetros de tempo, indicando quanto tempo o segmento deve permanecer bloqueado antes de retornar a falha de sinal para inserir ou recuperar o item em questão. Isso evita uma espera ilimitada, o que poderia ser fatal para um sistema de produção, visto que uma espera ilimitada pode se tornar facilmente uma queda do sistema que requer a reinicialização.
O map possui um erro de simultaneidade sutil que já prejudicou muitos desenvolvedores Java desatentos. ConcurrentMap é a solução simples.
Quando um Map é acessado de diversos encadeamentos, é comum usar containsKey() ou get() para descobrir se uma determinada chave está presente antes de armazenar o par chave/valor. Mas mesmo com um Mapsincronizado, um encadeamento poderia penetrar durante esse processo e confiscar o controle do Map. O problema é que o bloqueio é obtido no começo de get()e liberado antes do bloqueio poder ser adquirido novamente, no chamado para put(). O resultado é uma disputa entre dois encadeamentos e o resultado será diferente dependendo do encadeamento que for executado primeiro.
Se dois encadeamentos chamarem um método ao mesmo tempo, ambos irão testar e efetuar a inclusão, desperdiçando o valor do primeiro encadeamento no processo. Felizmente, a interface ConcurrentMap oferece suporte para diversos métodos adicionais projetados para executar duas tarefas com um único bloqueio: putIfAbsent(), por exemplo, executa o teste primeiro e, em seguida, executa a inclusão somente se a chave não estiver armazenada em Map.
SynchronousQueue é um item interessante, de acordo com o Javadoc:
Uma fila de bloqueio na qual cada operação de inserção deve aguardar uma operação de remoção correspondente por outro encadeamento, e vice-versa. Uma fila síncrona não possui nenhuma capacidade interna, nem mesmo a capacidade para um item.
O SynchronousQueue é basicamente outra implementação do supramencionado BlockingQueue. Ele fornece uma maneira leve de trocar elementos únicos de um encadeamento para outro, usando a semântica de bloqueio usada por ArrayBlockingQueue. Na Listagem 2, eu reescrevi o código da Listagem 1 usando SynchronousQueue ao invés de ArrayBlockingQueue:
Listagem 2. SynchronousQueue
import java.util.*;
import java.util.concurrent.*;
class Producer
implements Runnable
{
private BlockingQueue<String> drop;
List<String> messages = Arrays.asList(
"Mares eat oats",
"Does eat oats",
"Little lambs eat ivy",
"Wouldn't you eat ivy too?");
public Producer(BlockingQueue<String> d) { this.drop = d; }
public void run()
{
try
{
for (String s : messages)
drop.put(s);
drop.put("DONE");
}
catch (InterruptedException intEx)
{
System.out.println("Interrupted! " +
"Last one out, turn out the lights!");
}
}
}
class Consumer
implements Runnable
{
private BlockingQueue<String> drop;
public Consumer(BlockingQueue<String> d) { this.drop = d; }
public void run()
{
try
{
String msg = null;
while (!((msg = drop.take()).equals("DONE")))
System.out.println(msg);
}
catch (InterruptedException intEx)
{
System.out.println("Interrupted! " +
"Last one out, turn out the lights!");
}
}
}
public class SynQApp
{
public static void main(String[] args)
{
BlockingQueue<String> drop = new SynchronousQueue<String>();
(new Thread(new Producer(drop))).start();
(new Thread(new Consumer(drop))).start();
}
} |
O código de implementação parece quase igual, mas o aplicativo inclui um benefício, o SynchronousQueue permitirá uma inserção na fila somente se houver um encadeamento aguardando para utilizá-lo.
Na prática, o SynchronousQueue é similar aos "canais rendezvous" disponíveis em linguagens como Ada ou CSP. Em outros ambientes, eles também são conhecidos como "junções" incluindo o .NET (consulte a seção Recursos).
Por que brigar com a introdução de simultaneidade em suas classes de coleção quando a biblioteca do Java Runtime oferece equivalentes úteis pré-desenvolvidos? O próximo artigo desta séria explora ainda mais o namespace java.util.concurrent .
| Descrição | Nome | Tamanho | Método de download |
|---|---|---|---|
| Sample code for this article | j-5things4-src.zip | 23KB | HTTP |
Informações sobre métodos de download
Aprender
- "Java theory and practice: Concurrent collections classes" (Brian Goetz, developerWorks, julho de 2003): Saiba como o pacote
util.concurrentde Doug Lea revitaliza os tipos de coleção padrãoListeMap. - Java Concurrency in Practice (Brian Goetz, et. al. Addison-Wesley, 2006): A excelente capacidade do Brian de detalhar conceitos complexos para os leitores torna esse livro obrigatório para todos os desenvolvedores Java.
- "Spice up collections with generics and concurrency" (John Zukowski, developerWorks, abril de 2008): Apresenta alterações na estrutura de coleções Java no Java 6.
- Package java.util.concurrent, Java platform SE 6: Saiba mais sobre as classes do utilitário discutidas neste artigo.
- Guarded Blocks: A linguagem mais comum para a coordenação de encadeamentos.
- "Introdução à Estrutura Collections " (MageLang Institute, Sun Developer Network, 1999): Esse excelente tutorial, apesar de antigo, oferece uma introdução completa à estrutura de coleções Java anterior às coleções simultâneas.
- "The Collections Framework": Leia a documentação de API e de estrutura de coleções Java da Sun Microsystems.
- The Joins Concurrency Library: A pesquisa da Microsoft® publicou essa biblioteca implementando o conceito de junções como um mecanismo de sincronização. O papel de pesquisa associado (em PDF) é uma excelente fonte de aprendizado sobre a teoria por trás das junções.
- A zona de tecnologia Java do developerWorks: Centenas de artigos sobre cada aspecto da programação Java.
Discutir
- Participe da comunidade My developerWorks.
