Programação simultânea Java mortal (7): Análise do código-fonte ReentrantReadWriteLock

Neste artigo, vamos dar uma olhada na análise do código-fonte de ReentrantReadWriteLock, baseado em Java 8.

Sugestões de leitura: como os bloqueios no pacote de simultaneidade Java são implementados com base no AQS, os bloqueios de leitura e gravação neste artigo não são exceção. Se você ainda não entendeu, será mais difícil de ler. Recomendo que você leia o artigo sobre a AbstractQueuedSynchronizerresolução da fonte.
Explicação detalhada do AQS, desta vez vou entender completamente o princípio de bloqueio em pacotes simultâneos Java, então não tenho que memorizá-lo a cada entrevista

O que é um bloqueio de leitura e gravação?

Quando se trata de bloqueios, você pode pensar em implementações, como a palavra-chave synchronized ReentrantLock, que são bloqueios exclusivos. Ou seja, apenas um encadeamento pode acessar ao mesmo tempo, e o bloqueio de leitura e gravação pode permitir que vários encadeamentos do leitor acessem ao mesmo tempo, mas quando o encadeamento do gravador acessa, o encadeamento do leitor e o do gravador serão bloqueados. O bloqueio de leitura e gravação mantém um par de bloqueios, um bloqueio de leitura e um bloqueio de gravação.A separação de bloqueios de leitura e gravação melhora muito a simultaneidade em comparação com bloqueios exclusivos.

Podemos pensar que o significado da existência de bloqueios de leitura e gravação é que, em geral, a cena de leitura é muito maior do que a cena de gravação. Portanto, ele fornece maior simultaneidade e taxa de transferência do que bloqueios exclusivos em cenários onde as leituras são maiores do que as gravações. A implementação do bloqueio de leitura e gravação fornecido no pacote de simultaneidade Java é ReentrantReadWriteLock.

Os principais recursos do ReentrantReadWriteLock estão listados abaixo. Primeiro, obtenha uma compreensão geral e, em seguida, analise o código-fonte em detalhes.

característica Descrição
Escolha justa Suporta dois métodos de aquisição de bloqueio, justo e injusto (padrão), com grande rendimento no modo injusto
Reentrar Suporta reentrada, ou seja, o encadeamento do leitor pode continuar a adquirir o bloqueio de leitura após adquirir o bloqueio de leitura, e o encadeamento de gravação pode continuar a adquirir o bloqueio de gravação após adquirir o bloqueio de gravação e também pode adquirir o bloqueio de leitura ao mesmo tempo
Bloquear downgrade Siga a sequência de aquisição do bloqueio de gravação primeiro, adquirindo o bloqueio de leitura e liberando o bloqueio de gravação para atingir o processo de downgrade do bloqueio de gravação para leitura e gravação

Interface de bloqueio de leitura e gravação e exemplo de uso

ReadWriteLockApenas dois métodos para adquirir um bloqueio de leitura e um bloqueio de gravação são definidos, ou seja, o método readLock () e o método writeLock ()
, e sua implementação - ReentrantReadWriteLock, além do método de interface, também fornece alguns recursos fáceis de monitorar seu
status de trabalho interno Os métodos são listados a seguir:

Nome do método Descrição
getReadLockCount () O número de bloqueios de leitura retidos, o número de encadeamentos que não retêm bloqueios, porque um encadeamento pode adquirir bloqueios de leitura várias vezes, aqui está o número total de bloqueios de leitura adquiridos
getReadHoldCount () O número de vezes que o segmento atual mantém um bloqueio de leitura, armazenado em ThradLocal
isWriteLocked () Determine se o bloqueio de gravação foi adquirido
getWriteHoldCount () Retorna o número de bloqueios de gravação mantidos pelo segmento atual

Exemplo de uso

O exemplo a seguir ilustra o uso de bloqueios de leitura e gravação muito vividamente:

Insira a descrição da imagem aqui

No método get (String key) da operação de leitura, o bloqueio de leitura precisa ser adquirido, o que faz com que o acesso simultâneo ao método não seja bloqueado. Com o método put (String key, Object value) e clear () da operação de gravação, é necessário obter o bloqueio de gravação antecipadamente ao atualizar o HashMap.

O cache usa bloqueios de leitura e gravação para melhorar a simultaneidade das operações de leitura e também garante a visibilidade de todas as operações de leitura e gravação para cada operação de gravação e simplifica o método de programação.

Visão geral de ReentrantReadWriteLock

Insira a descrição da imagem aqui

Observe as informações na figura acima. Os bloqueios de leitura e gravação correspondem a duas instâncias de classe aninhadas internas e um sincronizador Sync personalizado herda AQS. ReadLock e WriteLock compartilham uma instância Sync.

Vamos dar uma olhada no código específico de ReadLock e WriteLock para entendê-lo mais claramente:

Insira a descrição da imagem aqui

É claro que os métodos em ReadLock e WriteLock são implementados por meio da classe Sync. Sync é uma subclasse de AQS e, em seguida, deriva os modos justos e injustos.

No método Sync que eles chamam, podemos ver: ReadLock usa modo compartilhado e WriteLock usa modo exclusivo .

Aí vem a pergunta, a mesma instância de Sync tem apenas um estado de sincronização de estado, como podemos usar o modo compartilhado e o modo exclusivo ao mesmo tempo ? ? ?

Se você não consegue entender a pergunta acima, então pode não estar familiarizado com o AQS. Aqui, eu listo resumidamente o modo compartilhado e o processo de modo exclusivo do AQS. Você pode compará-lo horizontalmente:

Insira a descrição da imagem aqui

A essência da realização de bloqueios do AQS reside no estado de propriedade interno mantido :

  1. Para aquisição exclusiva do status de sincronização, 0 significa que o bloqueio pode ser adquirido, 1 significa que foi roubado por outros e não pode ser adquirido e a reentrada atual é possível;
  2. Com acesso compartilhado ao estado de sincronização, cada thread pode realizar operações de adição e subtração no estado, de modo que a diferença do tipo exclusivo é garantir o estado de sincronização da operação thread-safe, que geralmente é garantido por loops e CAS.

Em outras palavras, o modo exclusivo e o modo compartilhado têm operações completamente diferentes no estado. Como o estado é usado no bloqueio de leitura e gravação ReentrantReadWriteLock? Não se preocupe, continue olhando para baixo, este design é bastante inteligente.

Análise do código-fonte do bloqueio de leitura e gravação

Esta análise de código-fonte, incluindo design de estado de leitura e gravação , aquisição e liberação de bloqueio de gravação , aquisição e liberação de bloqueios de leitura e downgrade de bloqueio .

1. Ler e escrever design de estado

Como mencionado acima, o sincronizador personalizado do bloqueio de leitura e gravação precisa manter o estado de vários threads de leitura e um thread de gravação no estado de sincronização (uma variável inteira). A resposta é "uso bit a bit". O bloqueio de leitura e gravação divide o estado de 32 bits nos 16 bits superiores e nos 16 bits inferiores, indicando leitura e gravação, respectivamente.

Então, como o bloqueio de leitura e gravação determina rapidamente o status atual de leitura e gravação? A resposta é por meio de operações de bits.
Assumindo que o valor do status de sincronização atual é S, o status de gravação é igual a S & 0x0000FFFF (todos os 16 bits superiores são apagados) e o status de leitura é igual a S >>> 16 (0 sem sinal é deslocado 16 bits para a direita). Quando o status de gravação aumenta em 1, é igual a S + 1 , e quando o status de leitura aumenta em 1, é igual a S + (1 << 16) , que é S + 0x00010000.

De acordo com a divisão do estado, uma inferência pode ser traçada: Quando S não é igual a 0, quando o estado de gravação (S & 0x0000FFFF) é igual a 0, o estado de leitura (S >>> 16) é maior que 0, ou seja, o bloqueio de leitura foi adquirido.

Esta conclusão é muito importante e será refletida no código a seguir.

Com a base acima, não seremos prolixos abaixo e iremos direto ao tópico e ver como ele é implementado no código-fonte. Não há muito código. Acredito que se você entendeu o acima, basta olhar o código linha por linha.

2. Aquisição e liberação do bloqueio de gravação

  • O bloqueio de gravação é um bloqueio exclusivo.
  • Se um bloqueio de leitura estiver ocupado, a aquisição do bloqueio de gravação deve entrar na fila de bloqueio e aguardar.

Aquisição de bloqueio de gravação

Vejamos primeiro o método de aquisição de bloqueio de gravação implementado pelo sincronizador personalizado Sync no bloqueio de leitura e gravação ReentrantReadWriteLock.

Insira a descrição da imagem aqui

Vamos dar uma olhada no julgamento de writerShouldBlock (), e os comentários de código são claros à primeira vista

Insira a descrição da imagem aqui

Você deve ter entendido o código acima. Aqui está uma explicação de por que o bloqueio de gravação não pode ser adquirido se o bloqueio de leitura foi adquirido?

É principalmente combinado com a intenção original do design e da cena de uso. O bloqueio de leitura e gravação deve garantir que a operação do bloqueio de gravação seja visível para o bloqueio de leitura. Se o bloqueio de leitura foi adquirido e o bloqueio de gravação ainda é adquirido por outros threads, o thread que adquiriu o bloqueio de leitura não pode percebê-lo. A operação de aquisição do thread de bloqueio de gravação.

Liberação de bloqueio de gravação

A seguir, veremos a liberação do bloqueio de gravação:

Insira a descrição da imagem aqui

3. Aquisição e liberação de bloqueios de leitura

  • O bloqueio de leitura é um bloqueio compartilhado;
  • O bloqueio de leitura pode ser adquirido por vários threads ao mesmo tempo. Quando o status de gravação é 0 (o bloqueio de gravação não foi adquirido), o bloqueio de leitura sempre será acessado com sucesso;

Obter o código-fonte do bloqueio de leitura é relativamente complicado. Do Java 5 ao Java 6, ficou muito mais complicado. O principal motivo é que algumas novas funções foram adicionadas, como o método getReadHoldCount (), que retorna o número de vezes que o encadeamento atual adquiriu o bloqueio de leitura. O status de leitura é a soma do número de vezes que todos os encadeamentos adquirem um bloqueio de leitura e o número de vezes que cada encadeamento adquire um bloqueio de leitura só pode ser salvo em ThreadLocal e mantido pelo próprio encadeamento, o que complica a implementação da aquisição de um bloqueio de leitura.

Então, eu deixei isso para trás. Afinal, a aquisição do bloqueio de gravação é relativamente simples, o que pode aumentar muito a confiança dos leitores. A seguir, vamos dar uma olhada na realização desse bloqueio de leitura.

Ler aquisição de bloqueio

O processo de bloqueio do ReadLock é mostrado abaixo:

Insira a descrição da imagem aqui

O código acima é principalmente para entender o método tryAcquireShared (arg):

No AQS, se o valor de retorno do método tryAcquireShared (arg) for menor que 0, significa que o bloqueio compartilhado (bloqueio de leitura) não foi adquirido, e se for maior que 0, significa que foi adquirido.

No código acima, para inserir o desvio if (ou seja, para obter um bloqueio de leitura), você precisa atender: readerShouldBlock () retorna falso e o CAS deve ser bem-sucedido (não nos preocupemos com o estouro de MAX_COUNT).

Portanto, com base no processo acima, pensamos em como inserir o método fullExperimenteAcquireShared (atual).

  • readerShouldBlock () retorna verdadeiro em 2 casos:

O que FairSync diz é hasQueuedPredecessors (), ou seja, existem outros elementos na fila de bloqueio aguardando o bloqueio. Em outras palavras, no modo justo, alguém está na fila e seu recém-chegado não pode adquirir o bloqueio diretamente;

O que NonFairSync diz é aparentementeFirstQueuedIsExclusive (), ou seja, para determinar se o primeiro nó sucessor da cabeça na fila de bloqueio deve adquirir o bloqueio de gravação. Se for o caso, deixe o bloqueio de gravação vir primeiro para evitar a privação do bloqueio de gravação. O autor define uma prioridade mais alta para o bloqueio de gravação, portanto, se o thread que adquire o bloqueio de gravação estiver prestes a adquirir o bloqueio, o thread que adquire o bloqueio de leitura não deve capturá-lo. Se head.next não é para adquirir o bloqueio de escrita, você pode pegá-lo à vontade, porque é um modo injusto, todo mundo é mais rápido que o CAS;

  • compareAndSetState (c, c + SHARED_UNIT) Aqui, o CAS falha e há competição. Ele pode competir com outra aquisição de bloqueio de leitura e, claro, pode competir com outra operação de aquisição de bloqueio de gravação.

Em seguida, chegará a fullExperimenteAcquireShared e tente novamente:

Insira a descrição da imagem aqui

A análise do código-fonte acima deve ser muito detalhada. Se você não consegue entender os comentários em alguns lugares acima, aqui vou resumir para você, remover firstReader, cachedHoldCounter, que são usados ​​para armazenar em cache a primeira aquisição de bloqueio de leitura O encadeamento e o último encadeamento que adquiriu o bloqueio de leitura são essencialmente usados ​​para melhorar o desempenho. O princípio é provavelmente este: normalmente a aquisição de um bloqueio de leitura é rapidamente acompanhada por uma liberação . Obviamente, na aquisição -> liberação do bloqueio de leitura Durante esse tempo, se nenhum outro encadeamento adquirir o bloqueio de leitura, esse cache pode ajudar a melhorar o desempenho, porque então não há necessidade de consultar o mapa em ThreadLocal.

Resuma o processo principal:

Insira a descrição da imagem aqui

Ler liberação de bloqueio

Vejamos o processo de liberação do bloqueio de leitura:

Insira a descrição da imagem aqui

O processo de liberação de bloqueio de leitura é relativamente simples. O principal é subtrair 1 do número de bloqueios de leitura mantidos pelo segmento atual. Se for reduzido para 0, o HoldCounter correspondente deve ser removido do ThreadLocal.

Então, no loop for, os 16 bits altos do estado são subtraídos por 1. Se for descoberto que o bloqueio de leitura e o bloqueio de gravação são liberados, o thread subsequente que adquire o bloqueio de gravação é ativado.

Bloquear downgrade

Degradação de bloqueio refere-se à degradação de bloqueios de gravação para ler bloqueios . Se o encadeamento atual tiver um bloqueio de gravação, então o libera e, finalmente, adquire um bloqueio de leitura, esse processo de conclusão segmentada não pode ser chamado de degradação de bloqueio. A degradação do bloqueio se refere ao processo de manter o bloqueio de gravação (pertencente atualmente), adquirir o bloqueio de leitura e, em seguida, liberar o bloqueio de gravação (pertencente anteriormente).

Vejamos um exemplo de degradação de bloqueio:

Insira a descrição da imagem aqui

No exemplo acima, quando os dados são alterados, a variável de atualização (booleana e volátil modificada) é definida como falsa. Neste momento, todos os threads que acessam o método processData () podem perceber a mudança, mas apenas um thread pode adquirir o bloqueio de gravação. Outros threads serão bloqueados no método lock () de bloqueio de leitura e bloqueio de gravação. Depois que o encadeamento atual adquire o bloqueio de gravação e conclui a preparação de dados, ele adquire o bloqueio de leitura e, em seguida, libera o bloqueio de gravação para concluir o downgrade do bloqueio.

É necessário adquirir um bloqueio de leitura na degradação do bloqueio?
A resposta é necessária, a fim de garantir a visibilidade dos dados, porque se o encadeamento atual não adquire o bloqueio de leitura e libera diretamente o bloqueio de gravação, então se outro encadeamento adquire o bloqueio de gravação e modifica os dados, então o encadeamento atual não pode perceber a aquisição de gravação As alterações feitas pelo thread de bloqueio.

Resumindo

  1. O bloqueio de leitura e gravação define um bloqueio de leitura e um bloqueio de gravação, que podem conter o bloqueio de gravação e o bloqueio de leitura ao mesmo tempo, mas não vice-versa;
  2. Quando o bloqueio de leitura é mantido, adquirir o bloqueio de gravação inevitavelmente falhará e entrará na fila de bloqueio.Você pode visualizar o código-fonte tryAcquire (adquires int) da aquisição do bloqueio de gravação para aprofundar seu entendimento;
  3. Ao adquirir um bloqueio de leitura, se o bloqueio de gravação foi adquirido, mas o thread que adquiriu o bloqueio de gravação é o segmento atual, então o bloqueio de leitura ainda pode ser adquirido.Aqui também é apenas para entender as etapas de degradação do bloqueio;
  4. A análise do código-fonte de bloqueios de leitura e gravação, a aquisição de bloqueios de leitura é difícil de entender, principalmente porque jdk1.6 introduz funções como o número de vezes que o bloqueio de thread atual é adquirido, e o status de leitura de cada thread só pode ser salvo em ThradLocal, que é mantido pelo próprio thread Ao mesmo tempo, considerando que a probabilidade de adquirir conflitos de bloqueio na maioria dos casos é pequena, o firstReader, cachedHoldCounter, etc. são introduzidos para armazenar em cache o primeiro e o último encadeamento de bloqueio de leitura e tempos de reentrada. Ao visualizar o código-fonte, meus comentários devem ser muito detalhados, e deve ser compreensível ver com esta mentalidade.

(Fim do texto completo) lutando!

Conta pública pessoal

Insira a descrição da imagem aqui

  • Amigos que acham que estão escrevendo bem podem se dar ao trabalho de gostar e seguir ;
  • Se o artigo estiver incorreto, indique-o, muito obrigado pela leitura;
  • Recomendo a todos que prestem atenção à minha conta oficial e irei regularmente divulgar artigos originais para você e colocá-lo na comunidade de aprendizagem de alta qualidade;
  • endereço do github: github.com/coderluojust/qige_blogs

Acho que você gosta

Origin blog.csdn.net/taurus_7c/article/details/105891774
Recomendado
Clasificación