Padrões de erro de simultaneidade de Java para sistemas com vários núcleos

Seis padrões de erro de simultaneidade de Java pouco conhecidos

Ao estudar padrões de bug de simultaneidade, você aumenta sua consciência geral da programação simultânea e aprende a reconhecer expressões de código que não funcionam ou que podem não funcionar. Neste artigo, os autores Zhi Da Luo, Yarden Nir-Buchbinder e Raja Das destrincham seis erros de simultaneidade pouco conhecidos que ameaçam a segurança de encadeamento e desempenho de aplicativos Java™ sendo executados em sistemas com vários núcleos.

Zhi Da Luo, Software Engineer, IBM China

Zhi Da LuoZhi 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, Research Scientist, IBM

Yarden Nir-BuchbinderYarden 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, Software Architect, IBM

Raja DasRaja 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.



13/Jan/2011

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.

1. Um antipadrão de Jetty

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 como volatile, 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étodo register for 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
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.

E quanto a ...

Você pode estar se perguntando se seria melhor usar AtomicInteger e AtomicFloat para as duas variáveis compartilhadas nas Listagens 5 e 6, e livrar-se totalmente da sincronização. A possibilidade de isso acontecer depende do que os outros métodos fazem com essas variáveis e se há dependência entre elas.


5. Acesso de várias etapas

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
      }
   }
}

Não apenas para contêineres

O cenário de conflito de bloqueio simétrico é um tanto quanto famoso, pois aconteceu em um release do Java 1.4, no qual alguns contêineres sincronizados retornados pelos métodos Collections.synchronized entraram em conflito. Mas não são apenas os contêineres que são vulneráveis a conflito de bloqueio simétrico. Basta uma classe com um método que recebe outra instância da mesma classe como seu argumento e que também precisa realizar uma operação atomicamente nos membros das duas instâncias. Os métodos compareTo e equals são dois bons exemplos.

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 ).


Conclusão

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.

Recursos

Aprender

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.

Comentários

developerWorks: Conecte-se

Los campos obligatorios están marcados con un asterisco (*).


Precisa de um ID IBM?
Esqueceu seu ID IBM?


Esqueceu sua senha?
Alterar sua senha

Ao clicar em Enviar, você concorda com os termos e condições do developerWorks.

 


A primeira vez que você entrar no developerWorks, um perfil é criado para você. Informações no seu perfil (seu nome, país / região, e nome da empresa) é apresentado ao público e vai acompanhar qualquer conteúdo que você postar, a menos que você opte por esconder o nome da empresa. Você pode atualizar sua conta IBM a qualquer momento.

Todas as informações enviadas são seguras.

Elija su nombre para mostrar



Ao se conectar ao developerWorks pela primeira vez, é criado um perfil para você e é necessário selecionar um nome de exibição. O nome de exibição acompanhará o conteúdo que você postar no developerWorks.

Escolha um nome de exibição de 3 - 31 caracteres. Seu nome de exibição deve ser exclusivo na comunidade do developerWorks e não deve ser o seu endereço de email por motivo de privacidade.

Los campos obligatorios están marcados con un asterisco (*).

(Escolha um nome de exibição de 3 - 31 caracteres.)

Ao clicar em Enviar, você concorda com os termos e condições do developerWorks.

 


Todas as informações enviadas são seguras.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=80
Zone=Tecnologia Java
ArticleID=608064
ArticleTitle=Padrões de erro de simultaneidade de Java para sistemas com vários núcleos
publish-date=01132011