Este documento explica o modelo de memória da linguagem Java, abordando conceitos como reordenação de operações, sincronização, uso das keywords "final" e "volatile". O modelo de memória Java garante o comportamento correto de programas multi-thread em diferentes arquiteturas, através de mecanismos como bloqueios sincronizados e variáveis marcadas como "final" ou "volatile".
1. Java Memory Model – FAQ
Table of Contents
O que é ?...............................................................................................................................................1
Reordenação de Operações:..................................................................................................................2
Conceito de sincronização:...................................................................................................................3
Keyword 'Final':...................................................................................................................................4
Keyword 'Volatile':...............................................................................................................................5
O que é ?
Em primeiro lugar, para efeitos de contexto, estamos a falar de sistemas com vários
processadores integrados em si mesmo. Tipicamente, estes sistemas contém vários níveis de
cache de memória, algo que melhora significativamente a performance de acesso a dados. No
entanto, com adição destas caches, algumas questões têm de ser tratadas com cautela. Por
exemplo:
• O que acontece quando dois processadores apontam para a mesma localização de
memória, ao mesmo tempo;
• Em que condições serão, os dois, capazes de visualizar o mesmo conteudo nessa
mesma localização de memória;
Basicamente, se analisarmos ao pormenor diversos processadores, seriamos capazes de
perceber que, consoante o cpu/modelo em causa, estaríamos na presença de um modelo de
memória que poderia ser muito bom ou muito mau. No entanto, enquanto programadores
de linguagens de alto nível, a abstração é tão grande que nunca se deve relacionar conceitos de
tão baixo nível com essas mesmas abstrações. Desta forma, a linguagem Java detém um
modelo que descreve o comportamento adequado em situações de código multi-threading,
bem como as Threads devem agir perante determinadas operações em memória. Descreve as
relações existentes entre variáveis de um programa com os detalhes de baixo-nivel
relativos à memória e processador.
2. Para ajudar nesta descrição e estabelecimento de regras relativamente ao modelo imposta
pela linguagem Java, a mesma detém funcionalidades que procuram ajudar o programador a
construir código concorrente sem erros:
• Volatile e Final keywords;
• Blocos synchronized;
• etc..
Ao especificar este modelo de memoria, a linguagem Java assegura que todos os programas
multi-threading executam corretamente em todas as arquiteturas.
Reordenação de Operações:
Por vezes, na execução de um programa, pode parecer que uma ou mais instruções estão a ser
executadas numa ordem diferente daquela que está especificada pelo código fonte. Faz-se
notar que, o compilador tem a liberdade de ordenar a execução de instruções caso entenda
que as mesmas serão otimizadas se for feita uma reordenação. A reordenação pode ser feita
por outras entidades que não o compilador.
Por exemplo, quando é feita uma escrita para uma variável a e depois para uma variável b, e o
valor de 'b' não depende do valor de 'a', então o compilador pode reordenar estas instruções
como bem entender.
Reordenações podem ter efeitos nefastos num programa multi-threading, porque tipicamente
, neste tipo de programas Threads estão interessadas em saber o que outras Threads andam a
fazer. Neste caso, para garantir o correto funcionamento, mesmo sobre reordenações de
operações, é aplicado o conceito de sincronização.
3. Conceito de sincronização:
O conceito de sincronização enquadra-se em diversos aspetos, sendo o mais conhecido, a
exclusão-mútua.
De uma forma geral, sincronização garante que, dados escritos anteriormente à execução de
um bloco synchronized estarão visíveis para a Thread que detém esse “bloco” de uma forma
previsível. Após sairmos desse bloco, libertamos o monitor, o mesmo é dizer, tornamos todas
as mudanças feitas durante esse bloco em mudanças fixas e visíveis para as outras Threads.
Antes de entrarmos num bloco, temos de adquirir o monitor, o mesmo é dizer que
invalidamos o acesso a caches locais do cpu de forma a que as variáveis sejam obtidas através
de acesso a memória principal.
O modelo de memória introduz novos conceitos como read-field, write-field, lock,
unlock, e novas operações sobre Threads como join e start. Introduz ainda o conceito de
relação entre operações denominado por happens-before.
Quando duas operações estabelecem esta relação entre si, entenda-se 'a' happens-before 'b',
significa que a operação 'a' irá sempre, mesmo que reordenada, ser executada antes da
operação 'b', bem como os seus resultados estarão visíveis perante o inicio de 'b'.
Algo que é completamente errado fazer é o seguinte padrão de código:
• synchronized (new Object()) {….}
O compilador irá remover por completo esta forma de “sincronização”, uma vez que é
inteligente o suficiente para perceber que nunca outra Thread irá tentar adquirir um lock
relativo a um monitor que não é referenciado por nenhuma referência.
É importante ter consciência de que, duas Threads têm de sincronizar relativamente ao
mesmo monitor, por forma a estabelecer esta relação de happens-before.
4. Keyword 'Final':
Dentro do modelo de memória Java, a keyword final e o comportamtento adjacente a esta,
explica o facto de classes imutáveis serem sempre Thread-Safe.
Porquê ?
Vejamos então o que significa ser 'final'. Quando um campo é 'final', esse mesmo campo será
visível para todas as Threads, mesmo sem qualquer sincronização implementada, após um
construção de objecto bem realizada. Por bem construido, entende-se que todas as funções de
construção de um objecto são apenas e só realizadas dentro do construtor.
Veja-se o seguinte exemplo:
Qualquer Thread que invoque o método reader, conseguirá visualizar f.x como tendo o valor
de 3. O mesmo não se pode dizer sobre y.
Assim, conclui-se que, uma classe que tenha uma construção bem realizada, bem como todos
os seus campos serem 'final', é uma classe immutable e sempre Thread-Safe, pois os seus
campos apresentarão sempre os valores armazenados sempre que forem invocados por outras
Threads.
Faz-se notar que, mesmo uma classe ser completamente Thread-Safe não remove a
necessidade de sincronização por parte de Threads que queiram ter acesso a esta. Por
exemplo, não existe forma de garantir que a referência para o objecto imutável será
correctamente visualizado por uma segunda Thread.
class FinalFieldExample {
final int x;
int y;
static FinalFieldExample f;
public FinalFieldExample() {
x = 3;
y = 4;
}
static void writer() {
f = new FinalFieldExample();
}
static void reader() {
if (f != null) {
int i = f.x;
int j = f.y;
}
}
}
5. Keyword 'Volatile':
Um mecanismo que foi implementado para possibilitar a comunicação de estados entre
Threads.
Cada leitura de um campo volatile irá sempre obter o último valor escrito até à data por
qualquer Thread. Proíbe o compilador e o runtime de armazenarem estes valores em registos.
Em vez disso, são sempre escritos na memória principal.
Faz-se notar um grande e importante aspeto que nem sempre é evidenciado pela literatura
acerca de variáveis volatile. O modelo de memória aplica uma enorme restrição no que toca à
escrita e leitura de variáveis volatile.
Entenda-se que:
• Escrita sobre um volatile => release de um monitor;
• Leitura de um volatile => adquire de um monitor;
(=> - “equivale a”)
Isto acontece porque, tudo o que é visível para uma Thread no dado momento em que a
mesmo escrever sobre um volatile, também tem de ser visível para uma próxima
Thread que vá ler o volatile.
No seguinte exemplo, temos então a garantia de visualizar um campo que não é final apenas
porque estamos perante a escrita de um valor numa variável volatile.
É importante perceber que, ambas as Threads têm de aceder à mesma variável volatile de
forma a estabelecer a propriedade Happens-Before.
class VolatileExample {
int x = 0;
volatile boolean v = false;
public void writer() {
x = 42;
v = true;
}
public void reader() {
if (v == true) {
//uses x - guaranteed to see 42.
}
}
}