Por que o volátil é usado neste exemplo de travamento duplo verificado

Da cabeça Primeiro livro de padrões de design, o padrão singleton com duplo bloqueio verificado foi implementado como abaixo:

public class Singleton { private volatile static Singleton instance; private Singleton() {} public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } } 

Eu não entendo porque o volatile está sendo usado. O uso volatile não derrota o propósito de usar bloqueio duplo verificado, ou seja, o desempenho?

Um bom recurso para entender por que o volatile é necessário vem do livro JCIP . A Wikipedia também tem uma explicação decente desse material.

O problema real é que o Thread A pode atribuir um espaço de memory, por instance antes de terminar a construção da instance . Thread B verá essa atribuição e tentará usá-la. Isso resulta na falha do Thread B porque está usando uma versão parcialmente construída da instance .

Conforme citado por @retruável, o volátil não é caro. Mesmo que seja caro, a consistência deve ter prioridade sobre o desempenho.

Há uma maneira mais limpa e elegante para Singletons Preguiçosos.

 public final class Singleton { private Singleton() {} public static Singleton getInstance() { return LazyHolder.INSTANCE; } private static class LazyHolder { private static final Singleton INSTANCE = new Singleton(); } } 

Artigo fonte: Initialization-on-demand_holder_idiom da wikipedia

Na engenharia de software, o idioma da Inicialização sob demanda (padrão de design) é um singleton carregado com preguiça. Em todas as versões do Java, o idioma permite uma boot lenta segura e altamente concorrente com bom desempenho

Como a class não possui nenhuma variável static para inicializar, a boot é concluída de maneira trivial.

A definição de class estática LazyHolder dentro dela não é inicializada até que a JVM determine que LazyHolder deve ser executado.

A class estática LazyHolder é executada somente quando o método estático getInstance é invocado na class Singleton, e a primeira vez que isso acontece, a JVM carrega e inicializa a class LazyHolder .

Essa solução é thread-safe sem exigir construções de linguagem especiais (ou seja, volatile ou synchronized ).

Bem, não há bloqueio duplo para o desempenho. É um padrão quebrado .

Deixando as emoções de lado, o volatile está aqui porque sem ele na segunda vez que o thread passa a instance == null , o primeiro thread pode não construir um new Singleton() ainda: ninguém promete que a criação do object acontece – antes da atribuição a instance para qualquer thread, mas o que realmente está criando o object.

volatile por sua vez, estabelece o que acontece – antes da relação entre leituras e escritas, e fixa o padrão quebrado.

Se você estiver procurando desempenho, use a class estática interna do suporte.

Se você não tivesse, um segundo thread poderia entrar no bloco sincronizado após o primeiro setá-lo para null, e seu cache local ainda acharia que era nulo.

O primeiro não é para correção (se fosse você está correto que seria auto-destrutivo), mas sim para otimização.

Uma leitura volátil não é realmente cara por si só.

Você pode criar um teste para chamar getInstance() em um loop apertado, para observar o impacto de uma leitura volátil; entretanto esse teste não é realista; Em tal situação, o programador normalmente chamaria getInstance() uma vez e armazenaria em cache a instância durante o uso.

Outro impl é usar um campo final (veja wikipedia). Isso requer uma leitura adicional, que pode se tornar mais cara que a versão volatile . A versão final pode ser mais rápida em um loop apertado, no entanto, esse teste é discutível como discutido anteriormente.

Declarar a variável como volatile garante que todos os accesss a ela realmente leiam seu valor atual da memory.

Sem volatile , o compilador pode otimizar os accesss à memory e manter seu valor em um registrador, portanto, somente o primeiro uso da variável lê a localização real da memory que contém a variável. Isso é um problema se a variável for modificada por outro thread entre o primeiro e o segundo access; o primeiro thread tem apenas uma cópia do primeiro valor (pré-modificado), portanto, a segunda instrução if testa uma cópia obsoleta do valor da variável.