JavaRush /Blogue Java /Random-PT /Gerenciando a volatilidade
lexmirnov
Nível 29
Москва

Gerenciando a volatilidade

Publicado no grupo Random-PT

Diretrizes para usar variáveis ​​voláteis

Por Brian Goetz 19 de junho de 2007 Original: Gerenciando a Volatilidade Variáveis ​​voláteis em Java podem ser chamadas de "luz sincronizada"; Eles exigem menos código para usar do que os blocos sincronizados, geralmente são executados mais rápido, mas só podem fazer uma fração do que os blocos sincronizados fazem. Este artigo apresenta vários padrões para usar o volátil de forma eficaz – e alguns avisos sobre onde não usá-lo. Os bloqueios possuem duas características principais: exclusão mútua (mutex) e visibilidade. Exclusão mútua significa que um bloqueio só pode ser mantido por um thread por vez, e essa propriedade pode ser usada para implementar protocolos de controle de acesso para recursos compartilhados, de modo que apenas um thread os utilize por vez. A visibilidade é uma questão mais sutil, seu objetivo é garantir que as alterações feitas nos recursos públicos antes da liberação do bloqueio sejam visíveis para o próximo thread que assumir esse bloqueio. Se a sincronização não garantisse visibilidade, os threads poderiam receber valores obsoletos ou incorretos para variáveis ​​públicas, o que levaria a uma série de problemas sérios.
Variáveis ​​voláteis
Variáveis ​​voláteis têm as propriedades de visibilidade das sincronizadas, mas não possuem atomicidade. Isso significa que os threads usarão automaticamente os valores mais atuais das variáveis ​​voláteis. Eles podem ser usados ​​para segurança de thread , mas em um conjunto muito limitado de casos: aqueles que não introduzem relacionamentos entre múltiplas variáveis ​​ou entre valores atuais e futuros de uma variável. Assim, volátil por si só não é suficiente para implementar um contador, um mutex ou qualquer classe cujas partes imutáveis ​​estejam associadas a múltiplas variáveis ​​(por exemplo, "início <= fim"). Você pode escolher bloqueios voláteis por um de dois motivos principais: simplicidade ou escalabilidade. Algumas construções de linguagem são mais fáceis de escrever como código de programa e, posteriormente, de ler e entender, quando usam variáveis ​​voláteis em vez de bloqueios. Além disso, diferentemente dos bloqueios, eles não podem bloquear um thread e, portanto, são menos propensos a problemas de escalabilidade. Em situações em que há muito mais leituras do que gravações, as variáveis ​​voláteis podem fornecer benefícios de desempenho em relação aos bloqueios.
Condições para uso correto de voláteis
Você pode substituir bloqueios por voláteis em um número limitado de circunstâncias. Para ser thread-safe, ambos os critérios devem ser atendidos:
  1. O que está escrito em uma variável é independente do seu valor atual.
  2. A variável não participa de invariantes com outras variáveis.
Simplificando, essas condições significam que os valores válidos que podem ser gravados em uma variável volátil são independentes de qualquer outro estado do programa, incluindo o estado atual da variável. A primeira condição exclui o uso de variáveis ​​voláteis como contadores thread-safe. Embora incremento (x++) pareça uma única operação, na verdade é toda uma sequência de operações de leitura-modificação-gravação que devem ser executadas atomicamente, o que o volátil não fornece. Uma operação válida exigiria que o valor de x permanecesse o mesmo durante toda a operação, o que não pode ser alcançado usando volátil. (No entanto, se você puder garantir que o valor seja gravado em apenas um thread, a primeira condição poderá ser omitida.) Na maioria das situações, a primeira ou a segunda condições serão violadas, tornando as variáveis ​​voláteis uma abordagem menos comumente usada para alcançar a segurança do thread do que as sincronizadas. A Listagem 1 mostra uma classe não thread-safe com um intervalo de números. Ele contém um invariante - o limite inferior é sempre menor ou igual ao superior. @NotThreadSafe public class NumberRange { private int lower, upper; public int getLower() { return lower; } public int getUpper() { return upper; } public void setLower(int value) { if (value > upper) throw new IllegalArgumentException(...); lower = value; } public void setUpper(int value) { if (value < lower) throw new IllegalArgumentException(...); upper = value; } } Como as variáveis ​​de estado de intervalo são limitadas dessa forma, não será suficiente tornar os campos inferior e superior voláteis para garantir que a classe seja segura para threads; a sincronização ainda será necessária. Caso contrário, mais cedo ou mais tarde você terá azar e dois threads executando setLower() e setUpper() com valores inadequados podem levar o intervalo a um estado inconsistente. Por exemplo, se o valor inicial for (0, 5), o thread A chama setLower(4) e ao mesmo tempo o thread B chama setUpper(3), essas operações intercaladas resultarão em um erro, embora ambas passem na verificação isso supostamente protege o invariante. Como resultado, o intervalo será (4, 3) - valores incorretos. Precisamos tornar setLower() e setUpper() atômicos para outras operações de intervalo - e tornar os campos voláteis não fará isso.
Considerações de desempenho
A primeira razão para usar volátil é a simplicidade. Em algumas situações, usar tal variável é simplesmente mais fácil do que usar o bloqueio associado a ela. A segunda razão é o desempenho, às vezes o volátil funciona mais rápido que os bloqueios. É extremamente difícil fazer declarações precisas e abrangentes como "X é sempre mais rápido que Y", especialmente quando se trata de operações internas da Java Virtual Machine. (Por exemplo, a JVM pode liberar totalmente o bloqueio em algumas situações, tornando difícil discutir os custos da volatilidade versus sincronização de forma abstrata). Entretanto, na maioria das arquiteturas de processadores modernas, o custo da leitura de variáveis ​​voláteis não é muito diferente do custo da leitura de variáveis ​​regulares. O custo de escrever voláteis é significativamente maior do que escrever variáveis ​​regulares devido ao isolamento de memória necessário para visibilidade, mas geralmente é mais barato do que definir bloqueios.
Padrões para uso adequado de voláteis
Muitos especialistas em simultaneidade tendem a evitar completamente o uso de variáveis ​​voláteis porque são mais difíceis de usar corretamente do que bloqueios. No entanto, existem alguns padrões bem definidos que, se seguidos cuidadosamente, podem ser usados ​​com segurança numa ampla variedade de situações. Sempre respeite as limitações do volátil - use apenas voláteis que sejam independentes de qualquer outra coisa no programa, e isso deve evitar que você entre em território perigoso com esses padrões.
Padrão nº 1: sinalizadores de status
Talvez o uso canônico de variáveis ​​mutáveis ​​seja simples sinalizadores de status booleanos que indicam que ocorreu um evento importante e único do ciclo de vida, como a conclusão da inicialização ou uma solicitação de desligamento. Muitas aplicações incluem uma construção de controle no formato: "até que estejamos prontos para encerrar, continue executando", conforme mostrado na Listagem 2: É volatile boolean shutdownRequested; ... public void shutdown() { shutdownRequested = true; } public void doWork() { while (!shutdownRequested) { // do stuff } } provável que o método shutdown() seja chamado de algum lugar fora do loop - em outro thread - portanto, a sincronização é necessária para garantir a visibilidade correta da variável shutdownRequested. (Ele pode ser chamado a partir de um ouvinte JMX, um ouvinte de ação em um thread de evento GUI, via RMI, através de um serviço web, etc.). No entanto, um loop com blocos sincronizados será muito mais complicado do que um loop com um sinalizador de estado volátil como na Listagem 2. Como volátil facilita a escrita do código e o sinalizador de estado não depende de nenhum outro estado do programa, este é um exemplo de um bom uso de voláteis. A característica dessas bandeiras de status é que geralmente há apenas uma transição de estado; o sinalizador shutdownRequested passa de falso para verdadeiro e, em seguida, o programa é encerrado. Esse padrão pode ser estendido para sinalizadores de estado que podem mudar para frente e para trás, mas somente se o ciclo de transição (de falso para verdadeiro e para falso) ocorrer sem intervenção externa. Caso contrário, será necessário algum tipo de mecanismo de transição atômica, como variáveis ​​atômicas.
Padrão nº 2: publicação segura única
Erros de visibilidade possíveis quando não há sincronização podem se tornar um problema ainda mais difícil ao escrever referências de objetos em vez de valores primitivos. Sem sincronização, você pode ver o valor atual de uma referência de objeto que foi escrita por outro thread e ainda ver os valores de estado obsoletos desse objeto. (Essa ameaça está na raiz do problema com o infame bloqueio de verificação dupla, onde uma referência de objeto é lida sem sincronização, e você corre o risco de ver a referência real, mas obter um objeto parcialmente construído através dela.) Uma maneira de publicar com segurança um object é fazer uma referência a um objeto volátil. A Listagem 3 mostra um exemplo onde, durante a inicialização, um thread em segundo plano carrega alguns dados do banco de dados. Outro código que pode tentar usar esses dados verifica se eles foram publicados antes de tentar usá-los. public class BackgroundFloobleLoader { public volatile Flooble theFlooble; public void initInBackground() { // делаем много всякого theFlooble = new Flooble(); // единственная запись в theFlooble } } public class SomeOtherClass { public void doWork() { while (true) { // чё-то там делаем... // используем theFolooble, но только если она готова if (floobleLoader.theFlooble != null) doSomething(floobleLoader.theFlooble); } } } Se a referência ao theFlooble não fosse volátil, o código em doWork() correria o risco de ver um Flooble parcialmente construído ao tentar fazer referência ao theFlooble. O principal requisito para esse padrão é que o objeto publicado seja thread-safe ou efetivamente imutável (efetivamente imutável significa que seu estado nunca muda depois de publicado). Um link Volátil pode garantir que um objeto esteja visível em seu formato publicado, mas se o estado do objeto mudar após a publicação, será necessária uma sincronização adicional.
Padrão #3: Observações Independentes
Outro exemplo simples de uso seguro de volátil é quando as observações são periodicamente “publicadas” para uso dentro de um programa. Por exemplo, existe um sensor ambiental que detecta a temperatura atual. O thread de segundo plano pode ler esse sensor a cada poucos segundos e atualizar uma variável volátil contendo a temperatura atual. Outros threads podem então ler esta variável, sabendo que o valor nela contido está sempre atualizado. Outro uso desse padrão é coletar estatísticas sobre o programa. A Listagem 4 mostra como o mecanismo de autenticação pode lembrar o nome do último usuário logado. A referência lastUser será reutilizada para postar o valor para uso pelo resto do programa. public class UserManager { public volatile String lastUser; public boolean authenticate(String user, String password) { boolean valid = passwordIsValid(user, password); if (valid) { User u = new User(); activeUsers.add(u); lastUser = user; } return valid; } } Este padrão expande o anterior; o valor é publicado para uso em outras partes do programa, mas a publicação não é um evento único, mas uma série de eventos independentes. Este padrão exige que o valor publicado seja efetivamente imutável – que o seu estado não mude após a publicação. O código que utiliza o valor deve estar ciente de que ele pode mudar a qualquer momento.
Padrão #4: padrão “feijão volátil”
O padrão “bean volátil” é aplicável em frameworks que usam JavaBeans como “estruturas glorificadas”. O padrão “bean volátil” usa um JavaBean como contêiner para um grupo de propriedades independentes com getters e/ou setters. A justificativa para o padrão "bean volátil" é que muitas estruturas fornecem contêineres para detentores de dados mutáveis ​​(como HttpSession), mas os objetos colocados nesses contêineres devem ser thread-safe. No padrão de bean volátil, todos os elementos de dados JavaBean são voláteis e getters e setters devem ser triviais - eles não devem conter nenhuma lógica além de obter ou definir a propriedade correspondente. Além disso, para membros de dados que são referências de objetos, esses objetos devem ser efetivamente imutáveis. (Isso não permite campos de referência de array, pois quando uma referência de array é declarada volátil, apenas essa referência, e não os próprios elementos, possui a propriedade volátil.) Como acontece com qualquer variável volátil, não pode haver invariantes ou restrições associadas às propriedades de JavaBeans. . Um exemplo de JavaBean escrito usando o padrão “volátil bean” é mostrado na Listagem 5: @ThreadSafe public class Person { private volatile String firstName; private volatile String lastName; private volatile int age; public String getFirstName() { return firstName; } public String getLastName() { return lastName; } public int getAge() { return age; } public void setFirstName(String firstName) { this.firstName = firstName; } public void setLastName(String lastName) { this.lastName = lastName; } public void setAge(int age) { this.age = age; } }
Padrões voláteis mais complexos
Os padrões da seção anterior cobrem a maioria dos casos comuns em que o uso de volátil é razoável e óbvio. Esta seção analisa um padrão mais complexo no qual volátil pode fornecer um benefício de desempenho ou escalabilidade. Padrões voláteis mais avançados podem ser extremamente frágeis. É fundamental que suas suposições sejam cuidadosamente documentadas e que esses padrões sejam fortemente encapsulados, porque mesmo as menores alterações podem quebrar seu código! Além disso, dado que o principal motivo para casos de uso voláteis mais complexos é o desempenho, certifique-se de que você realmente tenha uma necessidade clara do ganho de desempenho pretendido antes de usá-los. Esses padrões são compromissos que sacrificam a legibilidade ou a facilidade de manutenção em prol de possíveis ganhos de desempenho - se você não precisa da melhoria de desempenho (ou não pode provar que precisa dela com um programa de medição rigoroso), então provavelmente é um mau negócio porque isso você está desistindo de algo valioso e recebendo algo menos em troca.
Padrão nº 5: bloqueio de leitura e gravação barato
Até agora você deve estar ciente de que volátil é muito fraco para implementar um contador. Como ++x é essencialmente uma redução de três operações (ler, acrescentar, armazenar), se algo der errado, você perderá o valor atualizado se vários threads tentarem incrementar o contador volátil ao mesmo tempo. No entanto, se houver significativamente mais leituras do que alterações, você poderá combinar o bloqueio intrínseco e variáveis ​​voláteis para reduzir a sobrecarga geral do caminho do código. A Listagem 6 mostra um contador thread-safe que usa sincronizado para garantir que a operação de incremento seja atômica e usa volátil para garantir que o resultado atual seja visível. Se as atualizações não forem frequentes, esta abordagem pode melhorar o desempenho, uma vez que os custos de leitura são limitados a leituras voláteis, que geralmente são mais baratas do que adquirir um bloqueio não conflitante. @ThreadSafe public class CheesyCounter { // Employs the cheap read-write lock trick // All mutative operations MUST be done with the 'this' lock held @GuardedBy("this") private volatile int value; public int getValue() { return value; } public synchronized int increment() { return value++; } } A razão pela qual esse método é chamado de "bloqueio de leitura e gravação barato" é porque você usa diferentes mecanismos de tempo para leituras e gravações. Como as operações de gravação neste caso violam a primeira condição de uso de volátil, você não pode usar volátil para implementar um contador com segurança - você deve usar um bloqueio. No entanto, você pode usar volátil para tornar o valor atual visível durante a leitura, portanto, você usa um bloqueio para todas as operações de modificação e volátil para operações somente leitura. Se um bloqueio permite apenas que um thread por vez acesse um valor, as leituras voláteis permitem mais de um; portanto, quando você usa o volátil para proteger a leitura, obtém um nível de troca mais alto do que se usar um bloqueio em todo o código: e lê e grava. No entanto, esteja ciente da fragilidade deste padrão: com dois mecanismos de sincronização concorrentes, ele pode tornar-se muito complexo se for além da aplicação mais básica deste padrão.
Resumo
Variáveis ​​voláteis são uma forma de sincronização mais simples, porém mais fraca, do que o bloqueio, que em alguns casos fornece melhor desempenho ou escalabilidade do que o bloqueio intrínseco. Se você atender às condições para o uso seguro de volátil - uma variável é verdadeiramente independente de outras variáveis ​​​​e de seus próprios valores anteriores - às vezes você pode simplificar o código substituindo sincronizado por volátil. No entanto, o código que usa volátil costuma ser mais frágil do que o código que usa bloqueio. Os padrões aqui sugeridos cobrem os casos mais comuns em que a volatilidade é uma alternativa razoável à sincronização. Seguindo esses padrões - e tomando cuidado para não forçá-los além de seus próprios limites - você pode usar voláteis com segurança nos casos em que eles fornecem benefícios.
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION