Programação simultânea Java mortal (6): AQS explicada em detalhes, desta vez vou entender completamente o princípio de bloqueio em pacotes simultâneos Java, então não tenho que repeti-lo em todas as entrevistas

Você costuma ser questionado sobre bloqueios em Java durante a entrevista? Talvez você também tenha visto conceitos e conhecimentos relacionados em postagens de blogs na Internet, mas se você não tiver um entendimento profundo, pode separar esse conhecimento para formar seu próprio mapa cerebral do conhecimento e logo o esquecerá. O resultado é que toda entrevista deve ser revisada desde o início, o que é demorado e trabalhoso.

Hoje, comecei a aprender sobre bloqueios em simultaneidade Java.O objetivo principal é classificar as APIs e componentes relacionados a bloqueios em pacotes de simultaneidade Java. O objetivo é saber como usá-lo e implementá-lo. Somente sabendo o que é e por que é, você poderá usá-lo corretamente e lidar com a entrevista.

2

Para diminuir a sobrecarga dos leitores, este artigo trata principalmente do AQS, ou seja AbstractQueuedSynchronizer, ver como ele implementa a voz de bloqueio.

Interface de bloqueio

Falando em bloqueios, você definitivamente pensará na palavra - chave synchronized.Isso mesmo, ela era usada por programas Java para realizar a função de bloqueio antes de jdk1.5. Após jdk1.5, a interface Lock é adicionada ao pacote simultâneo para implementar a função de bloqueio. Sua função é semelhante a synchronized, mas precisa ser exibida para adquirir e liberar o bloqueio.

O uso do Lock também é muito simples, conforme mostrado na demonstração a seguir:

Lock lock = new ReentrantLock();
lock.lock();
try {
} finally {
    lock.unlock();
}

Aqui precisamos explicar os principais recursos que o sincronizado fornecido pela interface de Lock não possui:

  • Aquisição de teste do bloqueio : O thread atual tenta adquirir o bloqueio.Se o bloqueio atual não for adquirido por outros threads, o resultado é adquirido e mantido.

  • Aquisição de bloqueio interrompível : ao contrário de synchronized, o thread que adquire o bloqueio pode responder à interrupção. Quando o thread que adquire o bloqueio é interrompido por outros threads, a exceção de interrupção é lançada e o bloqueio é liberado ao mesmo tempo.

  • Adquira o bloqueio ao longo do tempo: Adquira o bloqueio antes do tempo especificado e retorne se o tempo limite não puder ser obtido.

Lock é uma interface que define as operações básicas de aquisição e liberação de lock:

Explique o significado de api de cima para baixo:

  1. Adquire o bloqueio.Depois que o thread atual adquire o bloqueio, ele retorna deste método, e o método bloqueia enquanto obtém o bloqueio;

  2. A diferença com lock () é que este método responderá a interrupções;

  3. O sem bloqueio tenta adquirir o bloqueio, o método retorna imediatamente e retorna verdadeiro se adquirido, caso contrário, retorna falso;

  4. Adquira o bloqueio ao longo do tempo, quando nos três cenários de timeout, interrupção e aquisição sem timeout, o bloqueio for adquirido, ele retornará;

  5. Libere o bloqueio e ative os nós subsequentes;

  6. Obtenha o componente de notificação de espera, que está vinculado ao bloqueio atual, e o método de espera do componente pode ser chamado apenas depois que o bloqueio é adquirido;

Sincronizador de fila (AQS)

AbstractQueuedSynchronizerPode-se dizer que o sincronizador de fila é a pedra angular do Java e contrata a construção de uma variedade de bloqueios e contêineres de segurança implementados, como para alcançar ReentrantLock, ReadWriteLock, CountDownLatch, etc. são menos figura AQS.

O próprio AQS usa uma variável de membro int para representar o estado de sincronização e completa o enfileiramento do thread de aquisição de recursos por meio da fila FIFO embutida. Doug Lea, o autor do pacote simultâneo java, queria que ele se tornasse a base para a maioria dos requisitos de sincronização ao projetá-lo.

O sincronizador é a chave para a realização da fechadura, claro, também pode ser qualquer componente de sincronização.O sincronizador é agregado na realização da fechadura, e o sincronizador é usado para realizar a semântica da fechadura. A relação entre os dois pode ser entendida como que o bloqueio é uma API orientada ao programador definida na interface do Lock, que define a interface interativa usada pelo programador e oculta os detalhes de implementação. O sincronizador é para o implementador do bloqueio. Ele simplifica a implementação do bloqueio e protege os detalhes de implementação subjacentes, como gerenciamento de estado de sincronização, enfileiramento de threads, espera e notificação. Este design é muito bom e isola as áreas às quais os usuários e implementadores precisam prestar atenção.

Exemplo de uso de AQS

O design do sincronizador AQS é baseado no método de modelo, ou seja, o usuário precisa herdar o sincronizador e reescrever o método especificado e, em seguida, combinar o sincronizador em um componente de sincronização personalizado e chamar o método de modelo fornecido pelo sincronizador. Esses métodos de modelo irão Chame o método de substituição do usuário.

Para permitir que os usuários reescrevam o método especificado, o sincronizador fornece três métodos básicos:

  1. getState (), obtém o estado de sincronização atual

  2. setState (int newState): Defina o estado de sincronização atual

  3. compareAndSetState (int expect, int update): Use CAS para definir o estado atual, este método pode garantir a atomicidade da configuração de estado

Os métodos síncronos regraváveis ​​são divididos em bloqueios de aquisição exclusivos e bloqueios de aquisição compartilhados. Para não aumentar a carga sobre os leitores, apenas os métodos regraváveis ​​de bloqueios de aquisição exclusivos são listados aqui. O código-fonte simplificado está listado abaixo

protected boolean tryAcquire(long arg) {    
    throw new UnsupportedOperationException();
}
protected boolean tryRelease(long arg) {
    throw new UnsupportedOperationException();
}
protected boolean isHeldExclusively() {
    throw new UnsupportedOperationException();
}

Pode-se ver que esses métodos que precisam ser reescritos não são implementados especificamente, portanto, precisamos implementá-los ao usá-los.

Os métodos que precisam ser implementados por componentes de sincronização personalizados estão listados acima. A seguir, vamos dar uma olhada em quais métodos de modelo o sincronizador fornece. Devido a razões de espaço, para não pressionar os leitores, apenas alguns métodos principais são listados. você pode ver a AbstractQueuedSynchronizerimplementação específica do código-fonte JDK específico.

Bloqueio de aquisição exclusivo

Obter bloqueio em resposta à interrupção

Liberar bloqueio

Em geral, os métodos de modelo fornecidos pelo sincronizador são basicamente divididos em três categorias: aquisição exclusiva e liberação do status de sincronização, aquisição compartilhada e liberação do status de sincronização e consulta de threads em espera na fila de sincronização . O componente de sincronização personalizado usará o método de modelo fornecido pelo sincronizador para implementar sua própria semântica de sincronização.

Dito isso, a seguir, implementaremos um bloqueio exclusivo por nós mesmos e usaremos o método de combinação do sincronizador personalizado AQS para ajudá-lo a dominar o princípio de funcionamento do sincronizador. Somente entendendo o AQS podemos aprender e compreender mais profundamente no pacote simultâneo. Outros componentes de sincronização.

Os exemplos são os seguintes:

Conforme mostrado no código de amostra, você pode ver que é muito fácil implementar um bloqueio exclusivo simples usando AQS. Uma classe interna estática é definida em Mutex, que herda o sincronizador para obter aquisição e liberação exclusivas do estado de sincronização.

No tryAcquire(int acquires)método, se a configuração for bem-sucedida após o CAS (o estado de sincronização é definido como 1), representa um estado de sincronização é adquirido e, tryRelease(int releases)apenas redefinir o estado de sincronização é 0. método. Os usuários não percebem e lidam diretamente com os sincronizadores internos que usam Mutex, mas chamam o método Mutex fornecido para obter Mutex para adquirir o lock()método de bloqueio , por exemplo, apenas os sincronizadores de método de modelo precisam chamar uma implementação de método acquire(int args)Ou seja, isso simplifica muito o limite para a implementação de um componente de sincronização personalizado confiável.

AQS realiza análise de código-fonte

Estrutura AQS

Vamos primeiro dar uma olhada nos atributos do AQS. Depois de ler isso, você saberá basicamente como o AQS implementa os bloqueios.

Depois de ler, você descobrirá que é muito simples, existem apenas três atributos principais.

O sincronizador depende da fila de sincronização interna para concluir o gerenciamento do estado de sincronização. O processo é assim: quando o thread não consegue obter o estado de sincronização, o sincronizador construirá o thread atual e o estado de espera em um nó (Nó), adicionará à fila e ao mesmo tempo Bloquear o encadeamento atual. Quando o estado de sincronização for liberado, o encadeamento no primeiro nó será ativado para fazer com que tente adquirir o estado de sincronização de bloqueio novamente.

Os nós na fila de sincronização são usados ​​para salvar a referência, o estado de espera e os nós predecessores e sucessores do thread que falhou em obter o estado de sincronização. Vejamos o código:

A estrutura de dados do Node não é complicada, são apenas thread + waitStatus + pre + next + nextWaitercinco atributos.Você deve primeiro ter esse conceito em mente.

O nó é a base da fila de sincronização. O sincronizador tem o nó principal e o nó final. O encadeamento que falha em obter a sincronização se tornará o nó e entrará no final da fila. A estrutura básica da fila de sincronização é a seguinte:

Com a introdução acima, você pode estar ansioso e querer ver como o AQS adquire e libera bloqueios. Não se preocupe, aprenda o princípio, lento é rápido!

Em seguida, siga o código de implementação específico, não sou muito prolixo.

Adquirir bloqueio

Conforme mencionado acima, os bloqueios de aquisição são divididos em tipos exclusivos e compartilhados. Para tornar a leitura mais suave, aqui analisamos apenas os bloqueios de aquisição exclusivos. Acredito que você tenha dominado o modo de bloqueio de aquisição exclusivo e, portanto, não há problema com a aquisição compartilhada. do.

Há muito pouco código acima e a lógica é relativamente clara. Primeiro, o método tryAcquire (arg) será chamado.Como mencionado acima, esse método precisa ser implementado pelo próprio componente de sincronização, como o bloqueio Mutex que implementamos acima. Este método garante a aquisição thread-safe do estado de sincronização, tryAcquire (arg) retorna true para indicar que a aquisição foi bem-sucedida e sai normalmente. Caso contrário, ele construirá o nó de sincronização (Nó exclusivo.EXCLUSIVE) e pelo addWaiter(Node mode)método será adicionado ao final da sincronização da fila, a chamada final acquireQueued(final Node node, int arg)para obter o status de sincronização por abordagem de "ciclo de morte". Se não estiver disponível, o encadeamento correspondente no nó será bloqueado, e o despertar após o bloqueio só pode ser obtido retirando da fila o nó precursor ou interrompendo o encadeamento bloqueado.

Vamos examinar a estrutura do nó e adicioná-lo à fila de sincronização.

O código acima usa o método compareAndSetTail (pred, node) para garantir que o nó possa ser adicionado com segurança de thread ao adicionar o nó construído ao final da fila de sincronização.

Aqui, observamos o momento em que a extremidade superior da fila adicionada rapidamente a sincronização não é satisfeita (ou seja, o código mostrado acima na fila está vazio ou vários threads simultâneos na equipe), fomos para o enq(node)método, que usa o giro do caminho para a equipe.

DETALHADO enrolado não o código foi escrito muito claro, que enq(final Node node)o método, nó CAS definido, antes de o thread ser retornado do processo após os nós finais em um loop infinito por. Caso contrário, o segmento atual continuará tentando. Pode-se ver que o cenário de uso deste método não é seguro para threads, porque ao mesmo tempo pode haver muitos threads que falham ao chamar o método tryAcquire para obter o status de sincronização a ser enfileirado. Aqui, spin plus CAS é usado de forma inteligente para "serializar" solicitações simultâneas.

Após o método acima, após o nó entrar na fila de sincronização, ele entra no próximo processo de rotação. Cada nó (ou seja, o encadeamento que falhou em adquirir o bloqueio) é observado introspectivamente. Quando a condição é atendida, o estado de sincronização é obtido. Você pode sair desse processo giratório, caso contrário, será forçado a permanecer nesse processo giratório e bloquear o thread do nó. O código específico é o seguinte:

Como acima, se o nó atual não foi realmente a primeira equipe ou apenas tryAcquire(arg)não agarrou a vitória dos outros, foi para um julgamento de ramo é o próximo: shouldParkAfterFailedAcquire(p, node) o segmento atual não agarrou o bloqueio, se necessário suspender o segmento atual ?

Você deve entender o código acima por si mesmo. Se sua ideia estiver quebrada, espere segui-la a partir de cima para evitar perda de tempo.

Aqui, analisamos private static boolean shouldParkAfterFailedAcquire(Node pred, Node node)o valor de retorno deste método:

  1. Se retornar true, significa que waitStatus == - 1 do nó precursor é normal e o thread atual precisa ser suspenso, aguardando para ser ativado mais tarde, apenas espere o nó precursor obter o bloqueio e, em seguida, chamá-lo quando o bloqueio for liberado;

  2. Se retornar falso, significa que não precisa ser suspenso no momento, por quê? Olhe para trás

shouldParkAfterFailedAcquire(Node pred, Node node)Depois que esse método retornar, ele será verdadeiro, então execute o parkAndCheckInterrupt()método:

Então fale se shouldParkAfterFailedAcquire(p, node)a situação retorna falso: dando uma olhada de perto em shouldParkAfterFailedAcquire (p, nó), podemos ver que, na verdade, entrou primeiro, geralmente não retorna verdadeiro, o motivo é muito simples, o nó precursor waitStatus=-1é dependente do sucessor Conjunto de nós. Em outras palavras, não configurei -1 para o predecessor ainda, como poderia ser verdade, mas você deve ver que esse método está aninhado em um loop, então o estado é -1 na segunda vez que ele entra.

Se você perceber que a ideia aqui ainda é relativamente clara, então explicaremos aqui por que o shouldParkAfterFailedAcquire(p, node)tópico não é suspenso diretamente quando false é retornado.

Isso ocorre porque após esse método, o nó anterior do nó atual pode desbloquear e sair da fila de sincronização devido ao tempo limite ou interrupção. Portanto, um novo nó pai é definido. Este nó pai pode já ser o principal. Há alguma surpresa aqui? sentir. . .

Aqui também entende o processo de sincronização AQS obtém um bloqueio, ou espero que você possa acquireQueued(final Node node, int arg)método Duokanjibian . Não há muito código, vale a pena perder tempo deduzindo os motivos de entrada de cada filial.

Liberar bloqueio

Depois que o thread atual obtém o estado de sincronização e executa a lógica correspondente, ele precisa liberar o estado de sincronização para que os nós subsequentes possam continuar a obter o estado de sincronização. O estado de sincronização pode ser liberado chamando o método release (int arg) do sincronizador. Depois que o estado de sincronização for liberado, este método irá despertar seus nós sucessores (e então fazer os nós sucessores tentarem obter o status de sincronização novamente).

O código de ativação é relativamente simples. Se você entende todos os bloqueios acima, não precisa ler o seguinte para saber o que está acontecendo!

Depois que o tópico for ativado, ele continuará avançando a partir do seguinte código:

Bem, depois de ler isso, você deve ter um certo entendimento do processo de bloqueio e desbloqueio exclusivo do sincronizador AQS, este artigo não continuará a usar o código-fonte. Eu acredito que você entendeu o acima, se você ainda tiver problemas ou quiser ver o processo de aquisição e liberação de bloqueio não exclusivo, vá honestamente e dê uma olhada no código.

Resumindo

Resumindo.

O pacote de simultaneidade Java fornece outra implementação da interface Lock, que define a operação básica de aquisição e liberação de bloqueio.

AbstractQueuedSynchronizerPode-se dizer que o sincronizador de fila é a pedra angular do Java e contrata a construção de uma variedade de bloqueios e contêineres de segurança implementados, como para alcançar ReentrantLock, ReadWriteLock, CountDownLatch, etc. são menos figura AQS.

Em um ambiente simultâneo, vários bloqueios que implementam a interface de bloqueio são fornecidos no pacote simultâneo.Eles contam com o sincronizador AQS para concluir as operações de bloqueio e desbloqueio. A realização do AQS requer principalmente a coordenação dos seguintes três componentes:

  1. Estado de bloqueio. Precisamos saber se o bloqueio está ocupado por outros threads. Esta é a função do estado. Quando for 0, significa que nenhum thread tem o bloqueio. Você pode competir por este bloqueio. Use o CAS para definir o estado como 1. Se o CAS for bem-sucedido, significa O bloqueio é capturado para que outros threads não possam pegá-lo. Se o bloqueio for inserido novamente, o estado pode ser +1 e o desbloqueio é reduzido em 1 até que o estado se torne 0 novamente, o que significa que o bloqueio foi liberado, então bloquear () e desbloquear () devem ser Precisa ser emparelhado. Em seguida, ative o primeiro thread na fila de sincronização e deixe-o travar.

  2. Bloqueio e desbloqueio de threads. O AQS usa LockSupport.park (thread) para suspender threads e desestacionar para ativar os threads.

  3. Bloqueie a fila. Porque pode haver muitos threads competindo por bloqueios, mas apenas um thread pode obter o bloqueio e outros threads devem esperar. Neste momento, uma fila é necessária para gerenciar esses threads. AQS usa uma fila FIFO, que é uma lista vinculada. Cada nó contém uma referência aos nós subsequentes.

gráfico de amostra

Esta imagem é usada para revisar o processo de aquisição da fechadura. Se você ainda ficar um pouco atordoado depois de lê-la, aqui está outra oportunidade para ajudá-lo a organizar seus pensamentos. Combine esta imagem e pense com cuidado. Você tem essa ideia fluindo em sua mente e, em seguida, analise o código-fonte novamente. .

(Fim deste artigo)


Materiais de referência :

  1. Zhou Zhiming: "Compreensão aprofundada da máquina virtual Java"

  2. Fang Tengfei: "The Art of Java Concurrent Programming"

Acho que você gosta

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