Ao brincar com testes de unidade para uma classe singleton altamente concorrente, deparei-me com o seguinte comportamento estranho (testado no JDK 1.8.0_162):
private static class SingletonClass {
static final SingletonClass INSTANCE = new SingletonClass(0);
final int value;
static SingletonClass getInstance() {
return INSTANCE;
}
SingletonClass(int value) {
this.value = value;
}
}
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
System.out.println(SingletonClass.getInstance().value); // 0
// Change the instance to a new one with value 1
setSingletonInstance(new SingletonClass(1));
System.out.println(SingletonClass.getInstance().value); // 1
// Call getInstance() enough times to trigger JIT optimizations
for(int i=0;i<100_000;++i){
SingletonClass.getInstance();
}
System.out.println(SingletonClass.getInstance().value); // 1
setSingletonInstance(new SingletonClass(2));
System.out.println(SingletonClass.INSTANCE.value); // 2
System.out.println(SingletonClass.getInstance().value); // 1 (2 expected)
}
private static void setSingletonInstance(SingletonClass newInstance) throws NoSuchFieldException, IllegalAccessException {
// Get the INSTANCE field and make it accessible
Field field = SingletonClass.class.getDeclaredField("INSTANCE");
field.setAccessible(true);
// Remove the final modifier
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
// Set new value
field.set(null, newInstance);
}
As duas últimas linhas do método main () discordam do valor de INSTANCE - meu palpite é que o JIT se livrou completamente do método, pois o campo é estático final. A remoção da palavra-chave final faz com que o código produza os valores corretos.
Deixando de lado sua simpatia (ou falta dela) por singletons e esquecendo por um minuto que usar uma reflexão como esta está causando problemas - é minha suposição correta que as otimizações do JIT são as culpadas? Em caso afirmativo - esses são limitados apenas a campos finais estáticos?
java
reflection
java-8
jit
Kelm
fonte
fonte
static final
campo. Além disso, não importa se esse hack de reflexão é interrompido devido a JIT ou simultaneidade.Respostas:
Tomando sua pergunta literalmente: “ … minha suposição está correta em que as otimizações do JIT são as culpadas? ”, A resposta é sim, é muito provável que as otimizações do JIT sejam responsáveis por esse comportamento neste exemplo específico.
Mas como a alteração de
static final
campos está completamente fora da especificação, há outras coisas que podem quebrá-lo da mesma forma. Por exemplo, o JMM não possui uma definição para a visibilidade da memória de tais alterações, portanto, não é completamente especificado se ou quando outros threads notam essas alterações. Eles nem precisam notá-lo de forma consistente, ou seja, podem usar o novo valor, seguido pelo valor antigo novamente, mesmo na presença de primitivas de sincronização.No entanto, é difícil separar o JMM e o otimizador aqui.
Sua pergunta “ … são limitados apenas a campos finais estáticos? ”É muito mais difícil de responder, pois as otimizações, obviamente, não se limitam a
static final
campos, mas o comportamento de, por exemplo, não estáticofinal
, campos , não é o mesmo e também possui diferenças entre teoria e prática.Para
final
campos não estáticos , modificações via Reflexão são permitidas em determinadas circunstâncias. Isso é indicado pelo fato de quesetAccessible(true)
é suficiente para possibilitar essa modificação, sem invadir aField
instância para alterar omodifiers
campo interno .A especificação diz:
Na prática, determinar os locais certos onde são possíveis otimizações agressivas sem violar os cenários legais descritos acima, é um problema em aberto ; portanto, a menos que
-XX:+TrustFinalNonStaticFields
tenha sido especificado, a JVM do HotSpot não otimizará osfinal
campos não estáticos da mesma maneira que osstatic final
campos.Obviamente, quando você não declara o campo como
final
, o JIT não pode assumir que ele nunca será alterado. Porém, na ausência de primitivas de sincronização de encadeamento, ele pode considerar as modificações reais acontecendo no caminho do código que otimiza (incluindo o reflexivos). Portanto, ele ainda pode otimizar agressivamente o acesso, mas apenas como se as leituras e gravações ainda acontecessem na ordem do programa no encadeamento em execução. Portanto, você só notaria as otimizações ao analisá-lo de um thread diferente sem construções de sincronização adequadas.fonte
final
, mas, embora algumas tenham se mostrado com melhor desempenho, algumas economiasns
não valem a pena quebrar muitos outros códigos. Razão pela qual Shenandoah está recuando em algumas de suas bandeiras, por exemplo