terça-feira, 5 de agosto de 2008

Análise e Crítica: Double-Checked Locking: Clever, but Broken

Um dos fatores primordiais em sistemas computacionais está associado a performance de uma aplicação, a qual pode ser atingida através do uso de paralelismo de tarefas. Entretanto, se não realizado um gerenciamento eficiente dos dados compartilhados entre as diferentes linhas de execução, a performance desejada pode ser significativamente afetada além de facultar o acesso a dados inconsistentes por essas tarefas. Uma das abordagens utilizadas por programadores Java é o uso da sintaxe synchronized, a qual objetiva o acesso controlado a pontos críticos entre as diferentes threads executantes. Entretanto, segundo Goetz[5], ao sincronizar um método Java através desta sintaxe, a performance de uma aplicação pode ser afetada consideravelmente em certos domínios. Isto pode ser ainda mais crítico quando esta sincronização deve ser aplicada ao padrão de projeto Singleton. A técnica Double-checked locking (DCL) possibilita "aliviar" o gargalo no acesso ao método sincronizado, provendo Lazy Inicialization (ou inicialização tardia) sem o uso de blocos sincronizados. Porém, devido a mecanismos utilizados pela JMM (Java Memory Model) para permitir melhor performance em tempo de execução, tal como a reordenação das instruções a serem executadas, a criação de uma única referência a um objeto pode ser drasticamente afetada.

Desta forma, diversos pesquisadores têm estudado e proposto algumas "artimanhas" como técnicas para evitar que o gerenciamento da JMM não efete o algorítimo desejado. Em [1], a construção do objeto a ser criado é colocado em um bloco (extra) synchronized interno. Ou seja, no momento em que a sincronização (interna) é finalizada, deve-se existir uma "barreira de memória", evitando assim que a JMM reordene as instruções para inicialização do objeto. Entretanto, ainda sim é possível que a reordenação aconteça devido a não existência de regras para impedir que a execução de instruções que estão após o bloco sincronizado seja antes de finalizar o "monitor". Uma outra solução, proposta por Goetz [3], utiliza uma variável temporária para garantir a construção total do objeto e posteriormente atribuída à referência original. Porém, ainda sim a JMM está livre para realizar otimizações nesta variável temporária. Goetz ainda propõe outra abordagem, utilizando uma variável booleana cujo valor depende diretamente da existência na memória do objeto a ser criado e posicionada fora do bloco synchronized. Porém, o compilador ou a JVM (Java Virtual Machine) pode reordenar tal instrução e adicioná-la ao bloco sincronizado para reduzir problemas associados a cache. Ou seja, o valor desta variável pode ser atribuída antes da construção do objeto. Em [4], Goetz discute a solução deste problema através da classe ThreadLocal. Em vez de verificar se o objeto possui referência nula, um objeto ThreadLocal (estático) armazena um valor (booleano, por exemplo) para garantir afirmar que uma outra thread já inicializou o objeto desejado. Por não compartilhar este valor com outras threads (pois este valor é local), problemas com reordenação de instruções não efetará a solução. Apesar de alcançar o objeto desejado pelo DCL, a performance da ThreadLocal é considerada pior que o custo existente em blocos sincronizados.

Apesar de existirem soluções plausíveis para alcançar o objetivo do Double-Checked Locking, a maior parte destas não funcionam ou possuem baixa performance/otimização. Acho que em qualquer domínio de aplicações que necessitem de alta performance sempre haverá trade-offs, ficando a cargo dos programadores decidir cuidadosamente quando um código deve ou não ser otimizado e, neste processo, se de fato haverá ganho considerável.

Ainda segundo Goetz, métodos sincronizados podem ser até 100 vezes mais lento do que métodos não-sincronizados. Para colocar em prova tal afirmação,
realizei alguns experimentos utilizando uma máquina com as seguintes configurações:

  • Processador Pentium-M, 1.86
  • 1GB Memória RAM

Utilizei um método estatístico intitulado de Distribuição Z [2]. Foram realizados 10 experimentos para cada tipo de método (sincronizado e não sincronizado), e alcancei os seguintes resultados com médias para 95% de nível de confiança:

  • Métodos sincronizados: 32071.90 milissegundos. Intervalo de confiança: entre 31743.89 e 32399.91 milissegundos.
  • Métodos não-sincronizados: 9691.00 milissegundos. Intervalo de confiança: entre 9419.32 e 9962.68 milissegundos.
Para ambos os casos, isso significa dizer que para "n" vezes que for executado esse experimento para o mesmo cenário das execuções (ou seja, com uma máquina igual ou similar as configuraçoes supracitadas), a média estará dentro do intervalo de confiança especificado. Desta forma, podemos concluir que o método sincronizado atribui maior tempo (apenas 4 vezes mais) de execução à aplicação do que o método não sincronizado.

Thiago Sales

[1] David Bacon, Joshua Bloch, Jeff Bogda, and Cliff Click. The ”double-checked locking is broken”declaration. http://www.javaworld.com/javaworld/jw-02-2001/jw-0209-double.html, Fevereiro 2001. Último acesso em 2 de Maio de 2008.

[2] Fisher. On a distribution yielding the error functions of several well known
statistics. Proceedings of the International Congress of Mathematics, 2:805–
813, 1924

[3] Brian Goetz. Can double-checked locking be fixed?
http://www.javaworld.com/javaworld/jw-05-2001/jw-0525-double.html, Maio 2001. Último acesso em 2 de Maio de 2008.

[4] Brian Goetz. Can threadlocal solve the double-checked locking problem?
http://www.javaworld.com/javaworld/jw-11-2001/jw-1116-dcl.html,
Novembro 2001. Último acesso em 2 de Maio de 2008.

[5] Brian Goetz. Double-checked locking: Clever, but broken.
http://www.javaworld.com/javaworld/jw-02-2001/jw-0209-double.html, Fevereiro 2001. Último acesso em 2 de Maio de 2008.

Nenhum comentário: