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.
01. 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.
02. 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.
03. 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ê.
04. 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.
05. 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 e write 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 e set, 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() );
}
}
Ao final do artigo, consulte os detalhes na seção 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.
Recursos
Aprender
- 5 coisas que você não sabia sobre … :
Descubra quando você não sabia sobre a plataforma Java nesta série
dedicada a tornar a tecnologia Java trivial em dicas úteis de
programação. - “Code Tracing” (Steven Haines, InformIT, agosto de 2010): Saiba mais sobre rastreio de código usando as variáveis ThreadLocal .
- “Java bytecode: Understanding bytecode makes you a better programmer”
(Peter Haggar, developerWorks, julho de 2001): Um tutorial de
introdução aos caminhos secundários do bytecode, incluindo um exemplo
anterior que ilustra a diferença entre métodos sincronizados e blocos
sincronizados. - “Java theory and practice: Going atomic”
(Brian Goetz, developerWorks, novembro de 2004): Explica como as
classes atômicas ativam o desenvolvimento de algoritmos não bloqueadores
altamente escaláveis na linguagem Java. - “Java theory and practice: Concurrency made simple (sort of)” (Brian Goetz, developerWorks, novembro de 2002): Conduz pelo java.util.concurrent .
- “5 coisas que você não sabia sobre … java.util.concurrent, Part 1”
(Ted Neward, developerWorks, maio de 2010): Obtenha uma introdução para
cinco classes de coleções simultâneas, que aperfeiçoam classes de
coleção padrão para suas necessidades de programação de simultaneidade. - A zona de tecnologia Java do developerWorks: Centenas de artigos sobre cada aspecto da programação Java.
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.
***
artigo publicado originalmente no developerWorks Brasil, por Steven Haines
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.