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 AbstractQueuedSynchronizer
resoluçã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
ReadWriteLock
Apenas 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:
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
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:
É 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:
A essência da realização de bloqueios do AQS reside no estado de propriedade interno mantido :
- 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;
- 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.
Vamos dar uma olhada no julgamento de writerShouldBlock (), e os comentários de código são claros à primeira vista
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:
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:
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:
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:
Ler liberação de bloqueio
Vejamos o processo de liberação do bloqueio de leitura:
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:
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
- 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;
- 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;
- 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;
- 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
- 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