Bloqueio verificado duplo Java

Eu me deparei com um artigo discutindo recentemente o duplo padrão de bloqueio verificado em Java e suas armadilhas e agora eu estou querendo saber se uma variante desse padrão que eu tenho usado há anos está sujeita a quaisquer problemas.

Eu olhei para muitos posts e artigos sobre o assunto e entendi os possíveis problemas em obter uma referência a um object parcialmente construído e, até onde eu sei, não acho que minha implementação esteja sujeita a esses problemas. Há algum problema com o seguinte padrão?

E, se não, por que as pessoas não usam isso? Eu nunca vi isso recomendado em qualquer discussão que eu vi em torno desta questão.

public class Test { private static Test instance; private static boolean initialized = false; public static Test getInstance() { if (!initialized) { synchronized (Test.class) { if (!initialized) { instance = new Test(); initialized = true; } } } return instance; } } 

O bloqueio de verificação dupla está quebrado . Como inicializado é um primitivo, ele pode não exigir que ele seja volátil para funcionar, no entanto, nada impede que o initialized seja visto como verdadeiro para o código não sincronizado antes que a instância seja inicializada.

EDIT: Para esclarecer a resposta acima, a pergunta original perguntou sobre o uso de um booleano para controlar o bloqueio de verificação dupla. Sem as soluções no link acima, isso não funcionará. Você poderia verificar o bloqueio na verdade definindo um booleano, mas ainda há problemas com a reordenação de instruções quando se trata de criar a instância da class. A solução sugerida não funciona porque a instância pode não ser inicializada depois que você vê o booleano inicializado como true no bloco não sincronizado.

A solução adequada para verificar o bloqueio é usar volatile (no campo instance) e esquecer o booleano inicializado, e estar certo de estar usando o JDK 1.5 ou superior, ou inicializá-lo em um campo final, conforme elaborado no link artigo e resposta de Tom, ou simplesmente não usá-lo.

Certamente, todo o conceito parece uma grande otimização prematura, a menos que você saiba que vai ter uma tonelada de contenção de threads para conseguir esse Singleton, ou que você tenha analisado o aplicativo e tenha visto isso como um ponto quente.

Isso funcionaria se initialized era volatile . Assim como no synchronized os efeitos interessantes do volatile não têm muito a ver com a referência do que podemos dizer sobre outros dados. A configuração do campo de instance e o object Test são forçados a acontecer antes da gravação ser initialized . Ao usar o valor em cache através do curto-circuito, a leitura de initialize acontece – antes da leitura da instance e dos objects alcançados através da referência. Não há diferença significativa em ter um sinalizador initialized separado (além de causar complexidade ainda maior no código).

(As regras para campos final em construtores para publicação insegura são um pouco diferentes.)

No entanto, você raramente deve ver o bug nesse caso. As chances de se meter em encrencas ao usar pela primeira vez são mínimas, e é uma corrida não repetida.

O código é muito complicado. Você poderia apenas escrevê-lo como:

 private static final Test instance = new Test(); public static Test getInstance() { return instance; } 

Bloqueio duplo verificado é realmente quebrado, ea solução para o problema é realmente mais simples de implementar em termos de código do que este idioma – basta usar um inicializador estático.

 public class Test { private static final Test instance = createInstance(); private static Test createInstance() { // construction logic goes here... return new Test(); } public static Test getInstance() { return instance; } } 

Um inicializador estático é garantido para ser executado na primeira vez que a JVM carrega a class e antes que a referência de class possa ser retornada para qualquer encadeamento – tornando-a inerentemente thread-safe.

Esta é a razão pela qual o bloqueio duplo verificado é quebrado.

Sincronizar garante que apenas um thread pode inserir um bloco de código. Mas isso não garante que as modificações de variables ​​feitas na seção sincronizada serão visíveis para outros threads. Somente as threads que entram no bloco sincronizado têm garantia de ver as alterações. Esta é a razão pela qual o bloqueio duplo verificado é quebrado – ele não está sincronizado no lado do leitor. O thread de leitura pode ver que o singleton não é nulo, mas os dados singleton podem não ser totalmente inicializados (visíveis).

Ordenação é fornecida por volatile . pedidos de garantias volatile , por exemplo, escrever para garantias de campo estático de um único estado volátil que gravam no object singleton serão finalizadas antes da gravação no campo estático volátil. Não impede a criação de singleton de dois objects, isso é fornecido pela synchronization.

Campos estáticos finais de class não precisam ser voláteis. Em Java, a JVM cuida desse problema.

Veja meu post, uma resposta ao padrão Singleton e ao bloqueio duplo verificado em um aplicativo Java do mundo real , ilustrando um exemplo de um singleton com relação ao bloqueio com verificação dupla que parece inteligente, mas está quebrado.

Você provavelmente deve usar os tipos de dados atômicos em java.util.concurrent.atomic .

Se “initialized” for true, então “instance” DEVE ser totalmente inicializado, o mesmo que 1 mais 1 é igual a 2 :). Portanto, o código está correto. A instância é instanciada apenas uma vez, mas a function pode ser chamada de um milhão de vezes, portanto, melhora o desempenho sem verificar a synchronization por um milhão menos uma vez.

Ainda existem alguns casos em que uma dupla verificação pode ser usada.

  1. Primeiro, se você realmente não precisa de um singleton, a checagem dupla é usada apenas para NÃO criar e inicializar muitos objects.
  2. Há um campo final definido no final do bloco construtor / inicializado (que faz com que todos os campos previamente inicializados sejam vistos por outros segmentos).

Eu tenho investigado sobre o idioma de bloqueio verificado duas vezes e pelo que eu entendi, seu código poderia levar ao problema de ler uma instância parcialmente construída, a menos que sua class de teste seja imutável:

O Java Memory Model oferece uma garantia especial de segurança de boot para compartilhamento de objects imutáveis.

Eles podem ser acessados ​​com segurança mesmo quando a synchronization não é usada para publicar a referência do object.

(Citações do muito aconselhável livro Java Concurrency in Practice)

Então, nesse caso, a dupla linguagem de bloqueio verificada funcionaria.

Mas, se esse não for o caso, observe que você está retornando a instância de variável sem synchronization, portanto, a variável de instância pode não ser completamente construída (você veria os valores padrão dos atributos em vez dos valores fornecidos no construtor).

A variável booleana não adiciona nada para evitar o problema, porque ela pode ser configurada como true antes que a class Test seja inicializada (a palavra-chave sincronizada não evita a reordenação completa, algumas sencences podem alterar a ordem). Não há regra de antes de acontecer no Java Memory Model para garantir isso.

E fazer o booleano volátil também não adicionaria nada, porque variables ​​de 32 bits são criadas atomicamente em Java. A dupla linguagem de bloqueio também funcionaria com eles.

Desde o Java 5, você pode corrigir esse problema declarando a variável de instância como volátil.

Você pode ler mais sobre a dupla linguagem marcada neste artigo muito interessante .

Finalmente, algumas recomendações que li:

  • Considere se você deve usar o padrão singleton. É considerado um anti-padrão por muitas pessoas. A injeção de dependência é preferida quando possível. Verifique isso .

  • Considere cuidadosamente se a otimização de travamento duplo verificado é realmente necessária antes de implementá-lo, porque na maioria dos casos, isso não valeria o esforço. Além disso, considere a construção da class Test no campo estático, porque o carregamento lento é útil apenas quando a construção de uma class requer muitos resources e, na maioria das vezes, não é o caso.

Se você ainda precisar realizar essa otimização, verifique este link que fornece algumas alternativas para obter um efeito semelhante ao que você está tentando.

O problema do DCL está quebrado, embora pareça funcionar em muitas VMs. Existe um bom artigo sobre o problema aqui http://www.javaworld.com/article/2075306/java-concurrency/can-double-checked-locking-be-fixed-.html .

multithreading e coerência de memory são assuntos mais complicados do que podem parecer. […] Você pode ignorar toda essa complexidade se você usar a ferramenta que o Java fornece exatamente para esse propósito – synchronization. Se você sincronizar todos os accesss a uma variável que possa ter sido gravada ou que possa ser lida por outro thread, você não terá problemas de coerência de memory.

A única maneira de resolver este problema corretamente é evitar a boot lenta (faça isso ansiosamente) ou fazer uma única verificação dentro de um bloco sincronizado. O uso do booleano initialized é equivalente a uma verificação nula na própria referência. Um segundo thread pode ver initialized como verdadeiro, mas a instance ainda pode ser nula ou parcialmente inicializada.

O bloqueio duplo é o anti-padrão.

A class de suporte de boot preguiçosa é o padrão para o qual você deve estar olhando.

Apesar de tantas outras respostas, imaginei que deveria responder, porque ainda não há uma resposta simples que diga por que o DCL é quebrado em muitos contextos, porque é desnecessário e o que você deve fazer em vez disso. Então, vou usar uma citação da Goetz: Java Concurrency In Practice que, para mim, fornece a explicação mais sucinta em seu capítulo final sobre o Modelo de Memória Java.

É sobre a publicação segura de variables:

O problema real com o DCL é a suposição de que a pior coisa que pode acontecer ao ler uma referência de object compartilhado sem synchronization é ver erroneamente um valor obsoleto (nesse caso, nulo); nesse caso, o idioma da DCL compensa esse risco tentando novamente com a trava mantida. Mas o pior caso é consideravelmente pior – é possível ver um valor atual da referência, mas valores obsoletos para o estado do object, o que significa que o object pode estar em um estado inválido ou incorreto.

Alterações subseqüentes no JMM (Java 5.0 e posterior) permitiram que o DCL funcionasse se o recurso fosse volátil, e o impacto no desempenho disso é pequeno, já que as leituras voláteis são geralmente apenas um pouco mais caras que as leituras não voláteis.

No entanto, esse é um idioma cuja utilidade já passou – as forças que o motivaram (synchronization lenta e lenta, boot lenta da JVM) não estão mais em jogo, o que o torna menos eficiente como uma otimização. O idioma de titularização de boot preguiçosa oferece os mesmos benefícios e é mais fácil de entender.

Listagem 16.6. Inicialização de Inicialização Preguiçosa Classe Idiom.

 public class ResourceFactory private static class ResourceHolder { public static Resource resource = new Resource(); } public static Resource getResource() { return ResourceHolder.resource; } } 

Essa é a maneira de fazer isso.

Primeiro, para singletons, você pode usar um Enum, como explicado nesta pergunta Implementando Singleton com um Enum (em Java)

Em segundo lugar, desde o Java 1.5, você pode usar uma variável volátil com bloqueio duplo verificado, conforme explicado no final deste artigo: https://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html