O livro "Concorrência Java na Prática"

imagemOlá, habrozhiteli! Os fluxos são uma parte fundamental da plataforma Java. Os processadores com vários núcleos são comuns e o uso efetivo da simultaneidade tornou-se necessário para criar qualquer aplicativo de alto desempenho. Uma máquina virtual Java aprimorada, suporte para classes de alto desempenho e um rico conjunto de componentes para tarefas de paralelização foram ao mesmo tempo uma inovação no desenvolvimento de aplicativos paralelos. No Java Concurrency in Practice, os próprios criadores da tecnologia inovadora explicam não apenas como eles funcionam, mas também falam sobre padrões de design. É fácil criar um programa competitivo que parece funcionar. No entanto, o desenvolvimento, teste e depuração de programas multiencadeados apresentam muitos problemas. O código para de funcionar exatamente quando é mais importante: sob carga pesada.No "Java Concurrency in Practice", você encontrará teoria e métodos específicos para criar aplicativos paralelos confiáveis, escaláveis ​​e suportados. Os autores não oferecem uma lista de APIs e mecanismos de simultaneidade; eles introduzem regras, padrões e modelos de design que são independentes da versão Java e permanecem relevantes e eficazes por muitos anos.

Excerto. Segurança da linha


Você pode se surpreender ao saber que a programação competitiva está associada a threads ou bloqueios (1), assim como a engenharia civil não está associada a rebites e vigas em I. Obviamente, a construção de pontes requer o uso correto de um grande número de rebites e vigas em I, e o mesmo se aplica à construção de programas competitivos, que exigem o uso correto de roscas e travas. Mas estes são apenas mecanismos - meios para alcançar a meta. Escrever código seguro para threads é, em essência, controlar o acesso a um estado e, em particular, a um estado mutável.

Em geral, o estado de um objeto é seus dados armazenados em variáveis ​​de estado, como instância e campos estáticos ou campos de outros objetos dependentes. O estado do hash do HashMap é parcialmente armazenado no próprio HashMap, mas também em muitos objetos Map.Entry. O estado de um objeto inclui qualquer dado que possa afetar seu comportamento.

(1) lock block, «», , . blocking. lock «», « ». lock , , , «». — . , , , . — . . .

Vários threads podem acessar uma variável compartilhada, modificada - altera seu valor. De fato, estamos tentando proteger dados, não códigos, de acesso competitivo não controlado.

A criação de um objeto seguro para threads requer sincronização para coordenar o acesso a um estado mutado, falha no cumprimento, o que pode levar à corrupção de dados e outras consequências indesejáveis.

Sempre que mais de um thread acessa uma variável de estado e um dos threads possivelmente grava nela, todos os threads devem coordenar seu acesso a ela usando a sincronização. A sincronização em Java é fornecida pela palavra-chave sincronizada, que fornece bloqueio exclusivo, bem como variáveis ​​voláteis e atômicas e bloqueios explícitos.

Resista à tentação de pensar que existem situações que não requerem sincronização. O programa pode funcionar e passar nos testes, mas permanece com defeito e falha a qualquer momento.

Se vários encadeamentos acessam a mesma variável com um estado mutado sem sincronização adequada, seu programa está com defeito. Há três maneiras de corrigi-lo:

  • Não compartilhe a variável state em todos os threads
  • torne a variável de estado não mutável;
  • use a sincronização de estado sempre que acessar a variável de estado.

As correções podem exigir alterações significativas no design, por isso é muito mais fácil projetar uma classe segura para threads imediatamente do que atualizá-la mais tarde.

É difícil descobrir se vários threads acessarão esta ou aquela variável. Felizmente, soluções técnicas orientadas a objetos que ajudam a criar classes bem organizadas e fáceis de manter - como encapsulamento e ocultação de dados - também ajudam a criar classes seguras para threads. Quanto menos encadeamentos tiverem acesso a uma variável específica, mais fácil será garantir a sincronização e definir as condições sob as quais essa variável pode ser acessada. A linguagem Java não o força a encapsular o estado - é perfeitamente aceitável armazenar o estado em campos públicos (mesmo campos estáticos públicos) ou publicar um link para um objeto que, de outra forma, é interno - mas quanto melhor o estado do seu programa for encapsulado,mais fácil é tornar o thread do programa seguro e ajudar os mantenedores a mantê-lo dessa maneira.

Ao projetar classes seguras para threads, boas soluções técnicas orientadas a objetos: encapsulamento, mutabilidade e uma especificação clara de invariantes serão seus assistentes.

Se boas soluções técnicas de design orientado a objetos divergem das necessidades do desenvolvedor, vale a pena sacrificar as regras de bom design por uma questão de desempenho ou compatibilidade com o código legado. Às vezes, abstração e encapsulamento estão em desacordo com o desempenho - embora não com a frequência que muitos desenvolvedores acreditam - mas a melhor prática é tornar o código correto primeiro e depois rápido. Tente usar a otimização apenas se medidas de produtividade e necessidades indicarem que você deve fazê-lo (2) .

2)No código competitivo, você deve aderir a essa prática ainda mais do que o habitual. Como os erros competitivos são extremamente difíceis de reproduzir e não são fáceis de depurar, a vantagem de um pequeno ganho de desempenho em algumas ramificações de código raramente usadas pode ser bastante insignificante em comparação ao risco de o programa travar em condições operacionais.

Se você decidir quebrar o encapsulamento, nem tudo estará perdido. Seu programa ainda pode tornar o thread seguro, mas o processo será mais complicado e mais caro, e o resultado não será confiável. O capítulo 4 descreve as condições sob as quais o encapsulamento de variáveis ​​de estado pode ser mitigado com segurança.

Até agora, usamos os termos “thread safe class” e “thread safe program” quase de forma intercambiável. Um programa de thread safe é construído inteiramente a partir de classes de thread safe? Opcional: um programa que consiste inteiramente de classes seguras de encadeamento pode não ser seguro, e um programa seguro de encadeamento pode conter classes que não são seguras. Questões relacionadas ao layout das classes de thread-safe também são discutidas no Capítulo 4. De qualquer forma, o conceito de uma classe de thread-safe só faz sentido se a classe encapsular seu próprio estado. O termo "segurança de encadeamento" pode ser aplicado ao código, mas fala do estado e só pode ser aplicado àquela matriz de código que encapsula seu estado (pode ser um objeto ou o programa inteiro).

2.1 O que é segurança de rosca?


Definir a segurança da linha não é fácil. Uma rápida pesquisa no Google oferece várias opções como estas:

... podem ser chamadas a partir de vários threads de programa sem interações indesejadas entre threads.

... pode ser chamado por dois ou mais threads ao mesmo tempo, sem exigir nenhuma outra ação do chamador.

Dadas essas definições, não é surpreendente que achemos confusos a segurança dos threads! Como distinguir uma classe de thread-safe de uma classe não segura? O que queremos dizer com a palavra "seguro"?

No centro de qualquer definição razoável de segurança do fio está a noção de correção.

A correção implica que uma classe esteja em conformidade com sua especificação. A especificação define invariantes que limitam o estado de um objeto e pós-condições que descrevem os efeitos das operações. Como você sabe que as especificações para as classes estão corretas? De jeito nenhum, mas isso não nos impede de usá-los depois que nos convencemos de que o código funciona. Então, vamos supor que a correção de thread único seja algo visível. Agora podemos assumir que a classe de thread-safe se comporta corretamente durante o acesso de vários threads.

Uma classe é segura para threads se se comportar corretamente durante o acesso de vários threads, independentemente de como esses threads são agendados ou intercalados pelo ambiente de trabalho e sem sincronização adicional ou outra coordenação por parte do código de chamada.

Um programa multithread não pode ser seguro para threads se não estiver correto, mesmo em um ambiente de thread único (3) . Se o objeto for implementado corretamente, nenhuma sequência de operações - acessando métodos públicos e lendo ou gravando em campos públicos - deve violar seus invariantes ou pós-condições. Nenhum conjunto de operações executadas sequencialmente ou competitivamente em instâncias de uma classe segura para encadeamento pode fazer com que uma instância esteja em um estado inválido.

(3) Se o uso livre do termo correção incomodá-lo aqui, você pode pensar em uma classe de thread-safe como uma classe defeituosa em um ambiente competitivo, bem como em um ambiente de thread único.

As classes thread-safe encapsulam qualquer sincronização necessária e não precisam de ajuda do cliente.

2.1.1 Exemplo: servlet sem suporte de estado interno


No Capítulo 1, listamos as estruturas que criam threads e chamam componentes deles que você é responsável pela segurança da thread. Agora pretendemos desenvolver um serviço de fatoração de servlet e expandir gradualmente sua funcionalidade, mantendo a segurança do encadeamento.

A Listagem 2.1 mostra um servlet simples que descompacta um número de uma consulta, os fatora e agrupa os resultados em resposta.

Listagem 2.1. Servlet sem suporte de estado interno

@ThreadSafe
public class StatelessFactorizer implements Servlet {
      public void service(ServletRequest req, ServletResponse resp) {
            BigInteger i = extractFromRequest(req);
            BigInteger[] factors = factor(i);
            encodeIntoResponse(resp, factors);
      }
}

A classe StatelessFactorizer, como a maioria dos servlets, não possui estado interno: não contém campos e não se refere a campos de outras classes. O estado para um cálculo específico existe apenas em variáveis ​​locais que são armazenadas na pilha de fluxos e estão disponíveis apenas para o fluxo em execução. Um thread que acessa StatelessFactorizer não pode afetar o resultado de outro thread fazendo o mesmo, porque esses threads não compartilham o estado.

Objetos sem suporte de estado interno são sempre seguros para threads.

O fato de a maioria dos servlets poder ser implementada sem suporte interno do estado reduz significativamente a carga de segmentar os próprios servlets. E somente quando os servlets precisam se lembrar de algo, os requisitos para a segurança de seus threads aumentam.

2.2 Atomicidade


O que acontece quando um item de estado é adicionado a um objeto sem suporte interno ao estado? Suponha que desejemos adicionar um contador de visitas que mede o número de solicitações processadas. Você pode adicionar um campo do tipo long ao servlet e incrementá-lo com cada solicitação, conforme mostrado em UnsafeCountingFactorizer na Listagem 2.2.

Listagem 2.2. Um servlet que conta solicitações sem a sincronização necessária. Isso não deve ser feito.

imagem

@NotThreadSafe
public class UnsafeCountingFactorizer implements Servlet {
      private long count = 0;

      public long getCount() { return count; }

      public void service(ServletRequest req, ServletResponse resp) {
            BigInteger i = extractFromRequest(req);
            BigInteger[] factors = factor(i);
            ++count;
            encodeIntoResponse(resp, factors);
      }
}

Infelizmente, a classe UnsafeCountingFactorizer não é segura para threads, mesmo que funcione bem em um ambiente de thread único. Como o UnsafeSequence, é propenso a atualizações perdidas. Embora a contagem da operação de incremento ++ tenha sintaxe compacta, ela não é atômica, ou seja, indivisível, mas uma sequência de três operações: entrega do valor atual, adição de um valor a ele e gravação do novo valor. Nas operações “ler, alterar, gravar”, o estado resultante é derivado do anterior.

Na fig. 1.1 é mostrado o que pode acontecer se dois threads tentarem aumentar o contador ao mesmo tempo, sem sincronização. Se o contador for 9, devido à coordenação malsucedida do tempo, os dois threads verão o valor 9, adicionam um a ele e configuram o valor para 10. Portanto, o contador de ocorrências começará a ficar um para o outro.

Você pode pensar que ter um contador de ocorrências um pouco impreciso em um serviço da web é uma perda aceitável, e às vezes é. Mas se o contador for usado para criar seqüências ou identificadores exclusivos de objetos, o retorno do mesmo valor de várias ativações poderá levar a sérios problemas de integridade dos dados. A possibilidade do aparecimento de resultados incorretos devido à coordenação temporal malsucedida surge em uma condição de corrida.

2.2.1 Condições da corrida


A classe UnsafeCountingFactorizer possui várias condições de corrida (4) . O tipo mais comum de condição de corrida é a situação de "verificar e depois agir", em que uma observação potencialmente obsoleta é usada para decidir o que fazer a seguir.

4) (data race). , . , , , , , . Java. , , . UnsafeCountingFactorizer . 16.

Muitas vezes encontramos uma condição de raça na vida real. Suponha que você planeje encontrar um amigo ao meio-dia no Starbucks Café na Universitetskiy Prospekt. Mas você descobrirá que existem duas Starbucks na University Avenue. Às 12:10, você não vê seu amigo no café A e vai para o café B, mas ele também não está lá. Seu amigo está atrasado ou ele chegou ao café A imediatamente depois que você saiu, ou ele estava no café B, mas foi procurá-lo e agora está a caminho do café A. Vamos aceitar o último, ou seja, o pior cenário. Agora 12:15, e vocês dois estão se perguntando se seu amigo cumpriu sua promessa. Você vai voltar para outro café? Quantas vezes você vai e volta? Se você não concordou com um protocolo, pode passar o dia inteiro andando pela University Avenue com euforia com cafeína.
O problema com a abordagem “dar um passeio e ver se ele está lá” é que um passeio pela rua entre dois cafés leva vários minutos e, durante esse período, o estado do sistema pode mudar.

O exemplo da Starbucks ilustra a dependência do resultado na coordenação de eventos em tempo relativo (em quanto tempo você espera por um amigo enquanto está em um café etc.). A observação de que ele não está no café A se torna potencialmente inválida: assim que você sai pela porta da frente, ele pode entrar pela porta dos fundos. A maioria das condições de corrida causa problemas como uma exceção inesperada, dados sobrescritos e corrupção de arquivos.

2.2.2 Exemplo: condições de corrida na inicialização lenta


Um truque comum usando a abordagem "verificar e depois agir" é a inicialização lenta (LazyInitRace). Seu objetivo é adiar a inicialização do objeto até que seja necessário e garantir que ele seja inicializado apenas uma vez. Na Listagem 2.3, o método getInstance verifica se o ExpensiveObject é inicializado e retorna uma instância existente ou, caso contrário, cria uma nova instância e a retorna após manter uma referência a ela.

Listagem 2.3. A condição de corrida está na inicialização lenta. Isso não deve ser feito.

imagem

@NotThreadSafe
public class LazyInitRace {
      private ExpensiveObject instance = null;

      public ExpensiveObject getInstance() {
            if (instance == null)
                instance = new ExpensiveObject();
            return instance;
      }
}

A classe LazyInitRace contém condições de corrida. Suponha que os encadeamentos A e B executem o método getInstance ao mesmo tempo. A vê que o campo da instância é nulo e cria um novo ExpensiveObject. O segmento B também verifica se o campo da instância é o mesmo nulo. A presença de null no campo neste momento depende da coordenação do tempo, incluindo os caprichos do planejamento e a quantidade de tempo necessária para criar uma instância do ExpensiveObject e definir o valor no campo da instância. Se o campo da instância for nulo quando B a verificar, dois elementos de código que chamam o método getInstance podem obter dois resultados diferentes, mesmo que o método getInstance deva sempre retornar a mesma instância.

O contador de visitas no UnsafeCountingFactorizer também contém condições de corrida. A abordagem "ler, alterar, escrever" implica que, para aumentar o contador, o fluxo deve conhecer seu valor anterior e garantir que ninguém mais altere ou use esse valor durante o processo de atualização.

Como a maioria dos erros competitivos, as condições da corrida nem sempre levam ao fracasso: a coordenação temporária é bem-sucedida. Mas se a classe LazyInitRace for usada para instanciar o registro de todo o aplicativo, quando retornar instâncias diferentes de várias ativações, os registros serão perdidos ou as ações receberão representações conflitantes do conjunto de objetos registrados. Ou se a classe UnsafeSequence for usada para gerar identificadores de entidade em uma estrutura de conservação de dados, dois objetos diferentes poderão ter o mesmo identificador, violando as restrições de identidade.

2.2.3 Ações compostas


LazyInitRace e UnsafeCountingFactorizer contêm uma sequência de operações que devem ser atômicas. Mas, para evitar uma condição de corrida, deve haver um obstáculo para outros threads usarem a variável enquanto um segmento a modifica.

As operações A e B são atômicas se, do ponto de vista do encadeamento executando a operação A, a operação B foi totalmente executada por outro encadeamento ou nem parcialmente executada.

A atomicidade da operação de incremento no UnsafeSequence evitaria a condição de corrida mostrada na Fig. 1.1 As operações "verificar e depois agir" e "ler, alterar, escrever" devem sempre ser atômicas. Eles são chamados de ações compostas - sequências de operações que devem ser executadas atomicamente para manter a segurança do thread. Na próxima seção, consideraremos o bloqueio - um mecanismo incorporado ao Java que fornece atomicidade. Enquanto isso, corrigiremos o problema de outra maneira aplicando a classe segura de thread existente, conforme mostrado no Countingfactorizer na Listagem 2.4.

Listagem 2.4. Solicitações de contagem de servlets usando AtomicLong

@ThreadSafe
public class CountingFactorizer implements Servlet {
      private final AtomicLong count = new AtomicLong(0);

      public long getCount() { return count.get(); }

      public void service(ServletRequest req, ServletResponse resp) {
            BigInteger i = extractFromRequest(req);
            BigInteger[] factors = factor(i);
            count.incrementAndGet();
            encodeIntoResponse(resp, factors);
      }
}

O pacote java.util.concurrent.atomic contém variáveis ​​atômicas para gerenciar estados de classe. Substituindo o tipo de contador de long por AtomicLong, garantimos que todas as ações que se referem ao estado do contador sejam atomic1. Como o estado do servlet é o estado do contador e o contador é seguro para threads, nosso servlet se torna seguro para threads.

Quando um único elemento de estado é adicionado a uma classe que não oferece suporte ao estado interno, a classe resultante será protegida por thread se o estado for completamente controlado pelo objeto seguro de thread. Mas, como veremos na próxima seção, a transição de uma variável de estado para a próxima não será tão simples quanto a transição de zero para uma.

Onde for conveniente, use objetos seguros para threads existentes, como AtomicLong, para controlar o estado da sua classe. Os possíveis estados de objetos seguros para threads existentes e suas transições para outros estados são mais fáceis de manter e verificar a segurança de threads do que variáveis ​​de estado arbitrárias.

»Mais informações sobre o livro podem ser encontradas no site da editora
» Conteúdo
» Trecho do

cupom Khabrozhiteley de 25% de desconto no cupom - Java

Após o pagamento da versão impressa do livro, um livro eletrônico é enviado por e-mail.

All Articles