5 Coisas que Você Não Sabia sobre ... Programação Multiencadeada

Sobre as sutilezas do encadeamento de alto desempenho

Programação multiencadeada nunca é fácil, mas é útil entender como a JVM processa sutilmente construções de códigos diferentes. Steven Haines compartilha cinco dicas que irão ajudá-lo a tomar decisões mais informadas ao trabalhar com métodos sincronizados, variáveis voláteis e classes atômicas.

Steven Haines, Founder and CEO, GeekCap Inc.

Steven Haines é arquiteto técnico na ioko e fundador da GeekCap Inc. Escreveu três livros sobre programação Java e análise de desempenho, além de várias centenas de artigos e dezenas de White Papers. Steven Também foi orador em conferências do segmento de mercado como JBoss World e STPCon e anteriormente ensinou programação Java na University of California, Irvine e na Universidade Learning Tree. Reside perto de Orlando, Flórida.



13/Dez/2010

Sobre esta série

Então, você acha que possui conhecimento sobre programação Java? A verdade é que a maioria dos desenvolvedores tem apenas algum conhecimento sobre a plataforma Java, aprendendo o suficiente para concluir a tarefa. Nesta série, investigadores da tecnologia Java escavam por debaixo da funcionalidade principal da plataforma Java, revelando dicas e truques que podem ajudar a resolver os desafios de programação mais complicados.

Enquanto poucos desenvolvedores de Java™ têm condições de ignorar a programação multiencadeada e as bibliotecas da plataforma Java que a suportam, menos ainda têm tempo de estudar os encadeamentos aprofundadamente. Em vez disso, aprendemos sobre os encadeamentos ad hoc, incluindo novas dicas e técnicas nas nossas caixas de ferramentas à medida que precisamos. É possível construir e executar aplicativos decentes dessa maneira, mas é possível fazer melhor. Entender as idiossincrasias de encadeamento do compilador Java e da JVM irá ajudá-lo a escrever com mais eficiência código Java que executa melhor.

Nesta parte da série 5 coisas, apresento alguns dos aspectos mais sutis da programação multiencadeada com métodos sincronizados, variáveis voláteis e classes atômicas. Minha discussão enfoca principalmente a maneira como algumas dessas construções interagem com a JVM e o compilador Java e como as diferentes interações poderiam afetar o desempenho do aplicativo Java.

1. Método sincronizado ou bloco sincronizado?

Ocasionalmente você pode ter ponderado se deve sincronizar uma chamada de método inteira ou somente o subconjunto thread-safe desse método. Nessas situações, é útil saber que quando o compilador Java converte o código de origem para o código de bytes, ele trata os métodos sincronizados e os blocos sincronizados de maneira bastante diferente.

Quando a JVM executa um método sincronizado, o encadeamento em execução identifica que a estrutura method_info do método tem o conjunto de sinalizadores ACC_SYNCHRONIZED , por isso adquire automaticamente o bloqueio do objeto, chama o método e libera o bloqueio. Se ocorrer uma exceção, o encadeamento libera o bloqueio automaticamente.

Por outro lado, sincronizar um bloqueio de método contorna o suporte integrado da JVM para adquirir o bloqueio e o tratamento de exceção do objeto e requer que a funcionalidade esteja explicitamente por escrito no código de bytes. Se o código de bytes for lido para um método com um bloco sincronizado, serão observadas mais de uma dezena de operações adicionais para gerenciar essa funcionalidade. A Listagem 1 mostra chamadas para gerar um método sincronizado e um bloco sincronizado:

Listagem 1. Duas abordagens da sincronização
package com.geekcap;

public class SynchronizationExample {
    private int i;

    public synchronized int synchronizedMethodGet() {
        return i;
    }

    public int synchronizedBlockGet() {
        synchronized( this ) {
            return i;
        }
    }
}

O método synchronizedMethodGet() gera o seguinte código de bytes:

	0:	aload_0
	1:	getfield
	2:	nop
	3:	iconst_m1
	4:	ireturn

E aqui está o código de bytes do método synchronizedBlockGet() :

	0:	aload_0
	1:	dup
	2:	astore_1
	3:	monitorenter
	4:	aload_0
	5:	getfield
	6:	nop
	7:	iconst_m1
	8:	aload_1
	9:	monitorexit
	10:	ireturn
	11:	astore_2
	12:	aload_1
	13:	monitorexit
	14:	aload_2
	15:	athrow

Criar o bloco sincronizado produziu 16 linhas de bytecode, enquanto que sincronizar o método retornou apenas 5.


2. Variáveis ThreadLocal

Para manter uma única instância de uma variável para todas as instâncias de uma classe, serão usadas variáveis membros de classe estática. Para manter uma instância de uma variável em base por encadeamento, serão usadas variáveis locais do encadeamento. As variáveis ThreadLocal são diferentes das variáveis normais no sentido de que cada encadeamento tem sua própria instância da variável inicializada individualmente, a qual acessa por meio de get() ou set() .

Digamos que você esteja desenvolvendo um rastreador de código multiencadeado cujo objetivo é identificar de forma exclusiva cada caminho de encadeamento através do código. O desafio é que é necessário coordenar múltiplos métodos em múltiplas classes através de múltiplos encadeamentos. Sem ThreadLocal, esse seria um problema complexo. Quando um encadeamento começa a executar, é necessário gerar um token exclusivo para identificá-lo no rastreador e, em seguida, passar esse token exclusivo para cada método no rastreamento.

Com ThreadLocal, as coisas são mais simples. O encadeamento inicializa a variável local do encadeamento no início da execução e a acessa em cada método em cada classe, com garantia de que a variável hospedará somente informações de rastreamento do encadeamento atualmente em execução. Ao concluir a execução, o encadeamento pode passar seu rastreamento específico do encadeamento para um objeto de gerenciamento responsável pela manutenção de todos os rastreamentos.

Usar ThreadLocal faz sentido quando for necessário armazenar instâncias de variável em uma base por encadeamento.


3. Variáveis voláteis

Estimo que aproximadamente metade de todos os desenvolvedores de Java sabe que a linguagem inclui a palavra-chave volatile. Desses, somente 10% sabem o que isso significa e menos ainda sabem como usá-la de maneira efetiva. Em resumo, identificar uma variável com a palavra-chave volatile significa que o valor da variável será modificado por diferentes encadeamentos. Para entender inteiramente o que a palavra-chave volatile executa, primeiro é útil entender como os encadeamentos tratam as variáveis não voláteis.

Para melhorar o desempenho, a especificação da linguagem Java permite ao JRE manter uma cópia local de uma variável em cada encadeamento que fizer referência a ela. Essas cópias de variáveis "locais do encadeamento" poderiam ser consideradas como semelhantes a uma cache, ajudando a evitar a verificação da memória principal cada vez que precisar acessar o valor da variável.

Mas considere o que acontece no seguinte cenário: dois encadeamentos começam e o primeiro lê a variável A como 5 e o segundo lê a variável A como 10. Se a variável A tiver mudado de 5 para 10, o primeiro encadeamento não ficará sabendo da mudança, por isso terá o valor errado para A. Porém, se a variável estivesse marcada como sendo volatile, a qualquer momento que um encadeamento lesse o valor de A, iria consultar a cópia mestre de A e ler seu valor atual.

Se as variáveis do aplicativo não irão mudar, uma cache local do encadeamento faz sentido. Por outro lado, é muito útil saber o que a palavra-chave volatile pode fazer por você.


4. Volátil versus sincronizada

Se uma variável for declarada como volatile, significa que se espera que seja modificada por múltiplos encadeamentos. Naturalmente, seria de esperar que o JRE impusesse alguma forma de sincronização para as variáveis voláteis. Por sorte, o JRE fornece sincronização explicitamente ao acessar as variáveis voláteis, mas com uma grande ressalva: a leitura de uma variável volátil é sincronizada e a gravação em uma variável volátil é sincronizada, mas operações não atômicas não são.

Isso significa que o seguinte código não tem segurança de encadeamento:

myVolatileVar++;

A instrução anterior também poderia ser escrita da seguinte maneira:

int temp = 0;
synchronize( myVolatileVar ) {
  temp = myVolatileVar;
}

temp++;

synchronize( myVolatileVar ) {
  myVolatileVar = temp;
}

Em outras palavras, se uma variável volátil for atualizada de maneira que, segundo as aparências, o valor é lido, modificado e lhe é atribuído um novo valor, o resultado será uma operação sem segurança de encadeamento executada entre duas operações síncronas. Assim, é possível decidir usar sincronização ou confiar no suporte do JRE para sincronizar automaticamente as variáveis voláteis. A melhor abordagem depende do caso de uso: se o valor designado da variável volátil depender do seu valor atual (como durante uma operação de incremento), é necessário usar sincronização para a operação ter segurança de encadeamento.


5. Atualizadores de campo atômico

Ao implementar ou decrementar um tipo primitivo em um ambiente multiencadeado, é bem melhor usar umas das novas classes atômicas encontradas no pacote java.util.concurrent.atomic do que escrever seu próprio bloqueio de código sincronizado. As classes atômicas garantem que determinadas operações serão executadas com segurança de encadeamento, como incrementar e decrementar um valor, atualizar um valor e incluir em um valor. À lista de classes atômicas inclui AtomicInteger, AtomicBoolean, AtomicLong, AtomicIntegerArray etc.

O desafio de usar classes atômicas é que todas as operações de classe, incluindo get, set e a família de operações get-set são renderizadas atômicas. Isso significa que as operações read ewrite que não modificam o valor de uma variável atômica são sincronizadas, não apenas as operações read-update-write importantes. A solução alternativa para ter controle com granularidade mais baixa sobre a implementação do código sincronizado é usar um atualizador de campo atômico.

Usando atualizações atômicas

Atualizadores de campo atômico como AtomicIntegerFieldUpdater, AtomicLongFieldUpdater e AtomicReferenceFieldUpdater são basicamente wrappers aplicados a um campo volátil. Internamente, as bibliotecas de classe Java os utilizam. Embora não sejam amplamente usados no código do aplicativo, não há motivo para não poder usá-los também.

A Listagem 2 apresenta um exemplo de uma classe que usa atualizações atômicas para alterar o livro que alguém está lendo:

Listagem 2. Classe Book
package com.geeckap.atomicexample;

public class Book
{
    private String name;

    public Book()
    {
    }

    public Book( String name )
    {
        this.name = name;
    }

    public String getName()
    {
        return name;
    }

    public void setName( String name )
    {
        this.name = name;
    }
}

A classe Book é apenas um POJO (plain old Java object) que tem um único campo: name.

Listagem 3. Classe MyObject
package com.geeckap.atomicexample;

import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;

/**
 *
 * @author shaines
 */
public class MyObject
{
    private volatile Book whatImReading;

    private static final AtomicReferenceFieldUpdater<MyObject,Book> updater =
            AtomicReferenceFieldUpdater.newUpdater(
                       MyObject.class, Book.class, "whatImReading" );

    public Book getWhatImReading()
    {
        return whatImReading;
    }

    public void setWhatImReading( Book whatImReading )
    {
        //this.whatImReading = whatImReading;
        updater.compareAndSet( this, this.whatImReading, whatImReading );
    }
}

A classe MyObject na listagem 3 expõe sua propriedade whatAmIReading como seria esperado, com métodos get eset , mas o método set faz alguma coisa um pouco diferente. Em vez de simplesmente designar sua referência Book interna ao Book especificado (o que seria realizado usando o código que está comentado na listagem 3), ele usa um AtomicReferenceFieldUpdater.

AtomicReferenceFieldUpdater

O Javadoc para AtomicReferenceFieldUpdater o define da seguinte maneira:

Um utilitário baseado em reflexão que ativa atualizações atômicas para campos de referência volátil designados de classes designadas. Essa classe é designada para uso em estrutura de dados atômicos em que vários campos de referência do mesmo nó são sujeitos independentemente a atualizações atômicas.

Em listagem 3, as variáveis AtomicReferenceFieldUpdater é criada por uma chamada ao seu método estático newUpdater , que aceita três parâmetros:

  • A classe do objeto que contém o campo (neste caso, MyObject)
  • A classe do objeto que atualizar atomicamente (neste caso, Book)
  • O nome do campo a ser atualizado atomicamente

O valor real aqui é que o método getWhatImReading é executado sem sincronização de qualquer espécie, enquanto que o setWhatImReading é executado como uma operação atômica.

A Listagem 4 ilustra como usar o método setWhatImReading() e declara que o valor altera corretamente:

Listagem 4. Etapa de teste que exercita a atualização atômica
package com.geeckap.atomicexample;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;

public class AtomicExampleTest
{
    private MyObject obj;

    @Before
    public void setUp()
    {
        obj = new MyObject();
        obj.setWhatImReading( new Book( "Java 2 From Scratch" ) );
    }

    @Test
    public void testUpdate()
    {
        obj.setWhatImReading( new Book(
                "Pro Java EE 5 Performance Management and Optimization" ) );
        Assert.assertEquals( "Incorrect book name",
                "Pro Java EE 5 Performance Management and Optimization",
                obj.getWhatImReading().getName() );
    }

}

Consulte os detalhes de Recursos para saber mais sobre classes atômicas.


Conclusão

A programação multiencadeada é sempre desafiadora, mas como a plataforma Java evoluiu, ganhou suporte que simplifica algumas tarefas de programação multiencadeada. Neste artigo discuto cinco coisas que você pode não conhecer sobre como escrever aplicativos multiencadeados na plataforma Java, incluindo a diferença entre sincronizar métodos versus sincronizar blocos de códigos, o valor de empregar variáveis ThreadLocal para armazenamento por encadeamento, a palavra-chave amplamente mal entendida volatile (incluindo os perigos de depender de volatile para suas necessidades de sincronização) e uma breve olhada nas complexidades das classes atômicas. Consulte a seção Recursos para saber mais.

Recursos

Aprender

Discutir

  • Participe da comunidade do My developerWorks. Entre em contato com outros usuários do developerWorks, enquanto explora os blogs, fóruns, grupos e wikis orientados ao desenvolvedor.

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=600651
ArticleTitle=5 Coisas que Você Não Sabia sobre ... Programação Multiencadeada
publish-date=12132010