Para programadores sem experiência em programação multiencadeada, o desafio de adaptar software para sistemas com vários núcleos é duplo: primeiro, a simultaneidade introduz uma nova categoria de erros nos programas Java, tais como disputa de dados e conflito, que são muito difíceis de reproduzir e diagnosticar. Em segundo lugar, muitos programadores não conhecem as sutilezas de certas expressões de programação multiencadeadas, o que pode levar a erros no código.
Para evitar que erros sejam introduzidos em programas simultâneos, os programadores Java devem aprender a reconhecer as junções críticas do código multiencadeado nas quais é provável que erros aconteçam, e então criar um software que seja à prova de erros. Neste artigo, nós nos voltamos para desenvolvedores Java em estágios básico e intermediário do entendimento do que torna a programação simultânea diferente. Em vez de focar em padrões de erro de simultaneidade de Java bem conhecidos, como bloqueio de verificação dupla, spin wait e wait-not-in-loop, nós apresentamos seis padrões que são menos conhecidos, mas que aparecem com frequência em aplicativos Java reais. Aliás, nossos dois primeiros exemplos são erros reais encontrados em dois populares servidores da Web.
Nosso primeiro erro de simultaneidade é encontrado no servidor HTTP de software livre altamente usado, Jetty. É um erro real, que foi confirmado pela comunidade do Jetty (consulte Recursos para ver o relatório do erro).
Listagem 1. Operações não atômicas em um campo volátil sem um bloqueio
// Jetty 7.1.0,
// org.eclipse.jetty.io.nio,
// SelectorManager.java, line 105
private volatile int _set;
......
public void register(SocketChannel channel, Object att)
{
int s=_set++;
......
}
......
public void addChange(Object point)
{
synchronized (_changes)
{
......
}
}
|
O erro na Listagem 1 é a soma das partes:
- Primeiro,
_seté declarado comovolatile, o que implica que o campo pode ser acesso por vários encadeamentos. - Mas
_set++não é atômico, o que significa que ele não é necessariamente executado como uma operação única, indivisível. Ao contrário, é um substituto para uma sequência de três operações discretas:read-modify-write. - Por fim,
_set++não está protegido por um bloqueio. Se o métodoregisterfor invocado simultaneamente por vários encadeamentos, isso pode resultar em uma condição de disputa, levando a um valor de_set incorreto.
Um erro desse tipo poderia aparecer tão facilmente no seu código como apareceu no do Jetty. por isso, vamos examinar mais de perto como isso aconteceu.
Elementos em um padrão de erro
Seguir o código até sua sequência lógica ajuda a esclarecer este padrão de erro. Operações em uma variável i, tais como
i++ --i i += 1 i -= 1 i *= 2 |
e assim por diante não são atômicas (ou seja, read-modify-write). Se você sabe que a palavra-chave volatile na linguagem Java garante apenas visibilidade da variável, mas não atomicidade, esse fato deve fazer você parar e pensar. Uma operação não atômica em um campo volátil que não é protegido por um bloqueio poderia resultar em uma condição de disputa, — mas apenas se a operação não atômica for acessada simultaneamente por vários encadeamentos.
Em um programa thread-safe, apenas um encadeamento que está gravando pode modificar a variável. Outros encadeamentos podem ler os valores atualizados declarando a variável volatile.
Portanto, o fato de um código ter erro ou não depende de quantos encadeamentos podem acessar a operação simultaneamente. Se a operação não atômica for chamada por apenas um encadeamento, devido a uma relação start-join ou bloqueio externo, a expressão de código será thread-safe.
Observe que a palavra-chave volatile em código Java pode apenas garantir que uma variável é visível. Essa palavra-chave não garante atomicidade. Em casos nos quais a operação de variável não é atômica e pode ser acessada por vários encadeamentos, não conte com recursos de sincronização volátil. Em vez disso, use blocos sincronizados, classes de bloqueio e classes atômicas do pacote java.util.concurrent. Eles são projetados para garantir a segurança de encadeamento dos programas.
2. Sincronização em campos mutáveis
Na linguagem Java, nós usamos blocos sincronizados para adquirir bloqueios mutex, que protegem acesso a recursos compartilhados em sistemas multiencadeados. No entanto, há uma brecha ao sincronizar em um campo mutável, que pode quebrar a mutex. A solução é sempre declarar o campo sincronizado como sendo private final. Vamos examinar o problema em mais detalhes para entender por quê.
Bloqueio simultâneo em campos atualizados
Blocos sincronizados são protegidos pelo objeto referenciado a partir do campo sincronizado, e não pelo próprio campo. Se um campo sincronizado for mutável (o que significa que ele pode ser designado em qualquer parte do programa além de sua inicialização), é difícil que ele tenha semântica útil, pois encadeamentos diferentes podem sincronizar em objetos diferentes.
Você pode observar o problema na Listagem 2, que é um fragmento de código do Tomcat, um servidor de aplicativos da Web de software livre:
Listagem 2. Erro no Tomcat
96: public void addInstanceListener(InstanceListener listener) {
97:
98: synchronized (listeners) {
99: InstanceListener results[] =
100: new InstanceListener[listeners.length + 1];
101: for (int i = 0; i < listeners.length; i++)
102: results[i] = listeners[i];
103: results[listeners.length] = listener;
104: listeners = results;
105: }
106:
107:}
|
Suponha que listeners refira-se à array A, e que o encadeamento T1 adquire o bloqueio da Array A e depois se ocupa com a criação da Array B. No meio tempo, T2 vem e bloqueia para travar a Array A. Quando T1 conclui a configuração de listeners na Array B e retira o bloqueio, T2 bloqueia a Array A e começa a fazer uma cópia da Array B. T3 então vem e bloqueia a Array B. Como eles obtiveram bloqueios diferentes, T2 e T3 estão agora simultaneamente fazendo cópias da Array B.
A Figura 1 ilustra melhor essa sequência
Figura 1. Ausência de mutex devido à sincronização em um campo mutável
Vários comportamentos indesejáveis podem resultar de uma configuração como essa. No mínimo, um dos listeners pode se perder ou um dos encadeamentos pode receber um ArrayIndexOutOfBoundsException (devido ao fato de que os listeners referenciam e seu comprimento pode mudar em qualquer parte do método).
Uma boa prática é sempre declarar o campo sincronizado como private final, o que garante que o objeto bloqueado permaneça inalterado e que mutex seja garantido.
3. Fuga de bloqueio de java.util.concurrent
Um bloqueio que implementa a interface java.util.concurrent.locks.Lock controla a maneira como diversos encadeamentos acessam um recurso compartilhado. Esses bloqueios não exigem estruturas de bloco, por isso são mais flexíveis que métodos ou instruções sincronizadas. Entretanto, essa flexibilidade pode levar a erros de código, pois um bloqueio sem um bloco nunca é liberado automaticamente. Se uma chamada Lock.lock() não tiver uma chamada unlock() correspondente na mesma instância, o resultado pode ser uma fuga de bloqueio.
É fácil introduzir o erro de fuga de bloqueio java.util.concurrent ignorando o comportamento do método em código crítico, tais como exceções que podem ser lançadas. Isso pode ser visto na Listagem 3, na qual o método accessResource lança uma InterruptedException , enquanto acessa o recurso compartilhado. Como resultado, unlock() não é chamado.
Listagem 3. Anatomia de uma fuga de bloqueio
private final Lock lock = new ReentrantLock();
public void lockLeak() {
lock.lock();
try {
// access the shared resource
accessResource();
lock.unlock();
} catch (Exception e) {}
public void accessResource() throws InterruptedException {...}
|
Para garantir que todos os bloqueios sejam liberados, basta associar cada método lock
com um método unlock, que deve ser colocado em um bloco try-finally. Essa associação é ilustrada na Listagem 4:
Listagem 4. Sempre coloque a chamada de desbloqueio no bloco finally
private final Lock lock = new ReentrantLock();
public void lockLeak() {
lock.lock();
try {
// access the shared resource
accessResource();
} catch (Exception e) {}
finally {
lock.unlock();
}
public void accessResource() throws InterruptedException {...}
|
4. Ajustando o desempenho de blocos sincronizados
Alguns erros de simultaneidade não quebram seu código, mas podem gerar desempenho precário do aplicativo. Considere o bloco synchronized na Listagem 5:
Listagem 5. Código invariável de bloco sincronizado
public class Operator {
private int generation = 0; //shared variable
private float totalAmount = 0; //shared variable
private final Object lock = new Object();
public void workOn(List<Operand> operands) {
synchronized (lock) {
int curGeneration = generation; //requires synch
float amountForThisWork = 0;
for (Operand o : operands) {
o.setGeneration(curGeneration);
amountForThisWork += o.amount;
}
totalAmount += amountForThisWork; //requires synch
generation++; //requires synch
}
}
}
|
O acesso às duas variáveis compartilhadas na Listagem 5 está sincronizado corretamente, mas, se você examinar com atenção, perceberá que o bloco synchronized exige mais cálculo do que deveria. Podemos consertar isso reorganizando as linhas, como mostra a Listagem 6:
Listagem 6. Bloco sincronizado sem o código invariável
public void workOn(List<Operand> operands) {
int curGeneration;
float amountForThisWork = 0;
synchronized (lock) {
int curGeneration = generation++;
}
for (Operand o : operands) {
o.setGeneration(curGeneration);
amountForThisWork += o.amount;
}
synchronized (lock)
totalAmount += amountForThisWork;
}
}
|
A segunda versão terá um desempenho muito melhor em máquinas com vários núcleos. O motivo é que o bloco sincronizado na Listagem 5 evita execução paralela. O tempo de cálculo nesse método deve ser provavelmente utilizado no loop. Na Listagem 6, o loop está fora do bloco sincronizado, por isso pode ser executado por vários encadeamentos em paralelo. Em geral, tente fazer com que seus blocos sincronizados sejam o menor possível, sem afetar a segurança dos encadeamentos.
Suponha que você está trabalhando em um aplicativo que contém duas tabelas: uma relaciona nomes de funcionários a um número de série, e a outra relaciona o número serial a um salário. Esses dados precisam suportar acessos e atualizações simultâneas, que você habilita pelo thread-safe ConcurrentHashMap, como mostra a Listagem 7:
Listagem 7. Acesso em duas etapas
public class Employees {
private final ConcurrentHashMap<String,Integer> nameToNumber;
private final ConcurrentHashMap<Integer,Salary> numberToSalary;
... various methods for adding, removing, getting, etc...
public int geBonusFor(String name) {
Integer serialNum = nameToNumber.get(name);
Salary salary = numberToSalary.get(serialNum);
return salary.getBonus();
}
}
|
Essa solução parece ser thread-safe, mas não é. O problema é que o método getBonusFor não é thread-safe. Entre a obtenção do número de série e seu uso para obter o salário, outro encadeamento poderia remover o funcionário de ambas as tabelas. Nesse caso, o acesso ao segundo mapa retornaria null e uma exceção seria lançada.
Tornar cada mapa thread-safe em si não é suficiente. Há uma dependência entre eles, e algumas operações que acessam ambos os mapas requerem acesso atômico. Você poderia obter segurança de encadeamentos neste caso usando contêineres que não thread-safe (tais como java.util.HashMap), e utilizando sincronização explícita para proteger cada acesso. O bloco sincronizado poderia então abranger ambos os acessos, se necessário.
6. Conflito de bloqueio simétrico
Imagine uma classe de contêiner thread-safe — uma estrutura de dados que garante segurança de encadeamento para seus clientes. (Isso é diferente da maioria dos contêineres em java.util, que requer que o cliente sincronize em relação aos usos do contêiner.) Na Listagem 8, um membro modificável armazena os dados e um objeto de bloqueio protege todo o acesso a ele.
Listagem 8. Um contêiner thread-safe
public <E> class ConcurrentHeap {
private E[] elements;
private final Object lock = new Object(); //protects elements
public void add (E newElement) {
synchronized(lock) {
... //manipular elementos
}
}
public E removeTop() {
synchronized(lock) {
E top = elements[0];
... //manipular elementos
return top;
}
}
}
|
Agora vamos adicionar um método que toma outra instância e adiciona todos os seus elementos à instância atual. Esse método precisa acessar o membro elements em ambas as instâncias, por isso ele toma ambos os bloqueios, como mostra a Listagem 9:
Listagem 9. Esta estrada leva ao conflito
public void addAll(ConcurrentHeap other) {
synchronized(other.lock) {
synchronized(this.lock) {
... //manipular other.elements e this.elements
}
}
}
|
Você vê o potencial para conflito? Imagine que um programa contenha duas instâncias, heap1 e
heap2. Se um encadeamento chamar heap1.addAll(heap2), e outro encadeamento ao mesmo tempo chamar heap2.addAll(heap1), os encadeamentos podem acabar em conflito. Em outros termos: imagine que o primeiro encadeamento bloqueia heap2, mas que, antes disso, o segundo encadeamento começa a executar o método, também bloqueando heap1. Como resultado, cada encadeamento termina esperando um bloqueio sustentado pelo outro encadeamento.
O conflito de bloqueio simétrico pode ser evitado determinando-se uma ordem entre as instâncias, de modo que quando bloqueios de duas instâncias precisem ser realizados juntos, a ordem é calculada dinamicamente e determina qual bloqueio é realizado primeiro. Brian Goetz discute essa solução alternativa em seu livro Java Concurrency in Practice (consulte Recursos ).
Muitos desenvolvedores Java estão nas etapas iniciais do aprendizado de como escrever programas simultâneos para ambientes com vários núcleos. Nesse processo, estamos abandonando as expressões de programação de um único encadeamento pelas expressões multiencadeadas, que são inerentemente mais complexas. Estudar padrões de erro de simultaneidade é uma boa maneira de descobrir os perigos da programação multiencadeada, e irá ajudar você a dominar as sutilezas de suas expressões.
Você pode aprender a reconhecer padrões de erro como a soma de suas partes, de modo que alguns sinais soarão o alerta quando você estiver escrevendo código ou durante revisões de código. Também é possível usar ferramentas de análise estática para esse fim. FindBugs é uma ferramenta de análise estática de software livre que procura por prováveis padrões de erros no código. Aliás, FindBugs poderia ser usado para detectar o segundo e o terceiro padrões de erros discutidos neste artigo.
Uma desvantagem conhecida das ferramentas de análise estática é que elas geram alarmes falsos, de modo que você gasta mais tempo do que gostaria verificando padrões de código que não contêm erros. Uma classe emergente de ferramentas de análise dinâmica é adequada especificamente para testar programas simultâneos. Duas dessas ferramentas, IBM® Multicore Software Development Kit (MSDK) e ConcurrentTesting (ConTest), estão disponíveis gratuitamente no alphaWorks.
Aprender
- Java Concurrency in Practice (Brian Goetz; Addison-Wesley, 2006): O excelente livro de Brian Goetz explica como prevenir conflitos ao sincronizar em diversas instâncias.
- Java theory and practice: a longa série de Goetz no developerWorks é outro lugar para procurar conselhos sobre simultaneidade, incluindo discussões profundas sobre conjuntos de encadeamentos e filas de trabalho (julho de 2002),
java.util.concurrent.lock(outubro de 2004), variáveis voláteis (junho de 2007) e fork-join emjava.util.concurrent(novembro de 2007). - "5 things you didn't know about ... java.util.concurrent, Part 1" (Ted Neward, developerWorks, maio de 2010): Descubra como classes como
CopyOnWriteArrayList,BlockingQueueeConcurrentMapadaptam classes de Coleção Java padrão para programação simultânea. - "Resolve common concurrency problems with GPars" (Alex Miller, developerWorks, setembro de 2010): Modelos de programação simultânea, tais como fork/join, atores, agentes e executores são todos encapsulados em GPars, uma biblioteca de simultaneidade baseada em Groovy.
- Jetty-1187: Non-atomic self-increment operation on volatile field _set in class SelectorManager: Esse relatório de erro detalha o antipadrão Jetty discutido neste artigo e sua solução.
- "FindBugs, Part 1: Improve the quality of your code" (Chris Grindstaff, developerWorks, maio de 2004): Descubra por que boas ferramentas de análise estática são uma adição valiosa à sua caixa de ferramentas e aprenda como usar uma das melhores.
- "IBM intros Multicore SDK" (Dr Dobb's Journal, julho de 2009): Uma visão geral do kit de ferramentas IBM alphaWorks para desenvolver programas paralelos em plataformas com vários núcleos.
-
Navegue pela livraria de tecnologia Java para obter livros sobre estes e outros tópicos técnicos.
-
Zona de tecnologia Java do developerWorks: Encontre centenas de artigos sobre cada aspecto da programação Java.
Obter produtos e tecnologias
- IBM Multicore SDK: Esse kit de ferramentas pode ser usado para localizar disputa de dados, conflito e contenção de bloqueio em programas Java de vários encadeamentos.
- Ferramenta IBM ConcurrentTesting: Usado para teste de unidade em aplicativos multiencadeados, ConcurrentTesting pode ajudar a eliminar erros relacionados à simultaneidade em programas Java paralelos e distribuídos.
Discutir
- Participe da comunidade My developerWorks. Entre em contato com outros usuários do developerWorks e explore os blogs, fóruns, grupos e wikis voltados para desenvolvedores.

Zhi Da Luo é engenheiro de software do Emerging Technology Institute, IBM China Development Lab. Mr. Luo passou a fazer parte da IBM em 2008. Ele tem experiência em análise de programa, instrumentação de bytecode e programação de simultaneidade de Java. Atualmente, ele está trabalhando em uma ferramenta de análise estática de tempo de execução para software paralelo Java. Luo tem mestrado em engenharia de software da Universidade de Pequim em Pequim, China.

Yarden Nir-Buchbinder completou seu bacharelado em ciência da computação na Technion, em Israel, e obteve seu MA em filosofia na Universidade de Haifa. Desde 2000, ele trabalha no IBM Haifa Research Lab, concentrando sua pesquisa em simultaneidade e cobertura de teste. Nir-Buchbinder é autor ou coautor de várias publicações e patentes.

Raja Das é arquiteto de software no IBM Software Group. Seu atual foco é desenvolver bibliotecas e estruturas para sistemas de vários núcleos. Anteriormente, ele foi arquiteto de produto do WebSphere Partner Gateway. Os interesses de Das incluem linguagens de programação, software paralelo e sistemas.