Antes do Java 5.0, os únicos mecanismos que poderiam ser usados para coordenar o acesso a objetos compartilhados foram sincronizados e voláteis. Sabemos que a palavra-chave sincronizada implementa bloqueios internos, enquanto a palavra-chave volátil garante a visibilidade da memória para multi-threads. Na maioria dos casos, esses mecanismos podem fazer bem o trabalho, mas não podem implementar algumas funções mais avançadas, como não conseguir interromper um tópico esperando para adquirir uma fechadura, sendo incapaz de implementar um mecanismo de trava com tempo limitado, sendo incapaz de implementar regras de bloqueio para estruturas não bloqueadoras, etc. e esses mecanismos de alcov para alcoólicos mais flexíveis geralmente oferecem melhor atividade ou desempenho. Portanto, um novo mecanismo foi adicionado no Java 5.0: Reentrantlock. A classe Reentrantlock implementa a interface de bloqueio e fornece a mesma visibilidade mutex e memória que sincronizada. Sua camada subjacente é obter sincronização com vários thread através do AQS. Comparado com bloqueios embutidos, o Reentrantlock não apenas fornece um mecanismo de travamento mais rico, mas também não é inferior a bloqueios internos no desempenho (ainda melhor do que os bloqueios embutidos nas versões anteriores). Tendo falado sobre tantas vantagens do Reentrantlock, vamos descobrir seu código -fonte e ver sua implementação específica.
1. Introdução às palavras -chave sincronizadas
O Java fornece bloqueios embutidos para suportar a sincronização de vários threads. A JVM identifica o bloco de código sincronizado de acordo com a palavra -chave sincronizada. Quando um thread entra no bloco de código sincronizado, ele adquirirá automaticamente o bloqueio. Ao sair do bloco de código sincronizado, o bloqueio será lançado automaticamente. Depois que um thread adquire a trava, outros threads serão bloqueados. Cada objeto Java pode ser usado como uma trava que implementa a sincronização. A palavra -chave sincronizada pode ser usada para modificar métodos de objeto, métodos estáticos e blocos de código. Ao modificar métodos de objeto e métodos estáticos, o bloqueio é o objeto em que o método está localizado e o objeto de classe. Ao modificar o bloco de código, objetos adicionais precisam ser fornecidos como bloqueios. A razão pela qual cada objeto Java pode ser usado como um bloqueio é que um objeto de monitor (manipulação) está associado no cabeçalho do objeto. Quando o encadeamento entra no bloco de código síncrono, ele manterá automaticamente o objeto Monitor e, quando sair, liberará automaticamente o objeto Monitor. Quando o objeto do monitor é mantido, outros threads serão bloqueados. Obviamente, essas operações de sincronização são implementadas pela camada JVM subjacente, mas ainda existem algumas diferenças na implementação subjacente do método de modificação de palavras -chave sincronizadas e bloco de código. O método de modificação da palavra -chave sincronizado é implicitamente sincronizado, ou seja, ele não precisa ser controlado por meio de instruções de bytecode. A JVM pode distinguir se um método é um método sincronizado com base no sinalizador de acesso acc_synchronizado na tabela de métodos; Enquanto os blocos de código modificados pela palavra -chave sincronizados são explicitamente sincronizados, que controlam a retenção e a liberação do thread do pipeline através do monitorente e das instruções de bytecode bytexit monitorexit. O objeto Monitor mantém o campo _Count internamente. _Count igual a 0 significa que o pipeline não é mantido e _Count maior que 0 significa que o pipeline foi mantido. Cada vez que a reentrada do thread de retenção, _count será adicionada 1 e, toda vez que a saída do encadeamento de holding, _Count será reduzida em 1. Este é o princípio da implementação da reentrada de bloqueio embutida. Além disso, existem duas filas dentro do objeto Monitor _EntryList e _WaitSet, que correspondem à fila de sincronização e à fila condicional do AQS. Quando o thread falhar em adquirir o bloqueio, ele bloqueará no _EntryList. Quando o método de espera do objeto de bloqueio for chamado, o thread entrará no _waitset para esperar. Este é o princípio da implementação da sincronização do encadeamento e da aguardação condicional por bloqueios embutidos.
2. Comparação entre reentrantlock e sincronizado
A palavra-chave sincronizada é um mecanismo de bloqueio interno fornecido pelo Java. Suas operações de sincronização são implementadas pela JVM subjacente. O ReentrantLock é um bloqueio explícito fornecido pelo pacote java.util.Concurrent, e suas operações de sincronização são alimentadas pelo sincronizador AQS. O ReentrantLock fornece a mesma semântica sobre bloqueio e memória que bloqueios embutidos, além disso, fornece alguns outros recursos, incluindo espera de trava cronometrada, espera interrompível à espera, bloqueio justo e implementação de bloqueio estruturado sem blocos. Além disso, o Reentrantlock também teve certas vantagens de desempenho nas primeiras versões do JDK. Como o ReentrantLock tem tantas vantagens, por que devemos usar a palavra -chave sincronizada? De fato, muitas pessoas usam o ReentrantLock para substituir a operação de bloqueio de palavras -chave sincronizadas. No entanto, os bloqueios embutidos ainda têm suas próprias vantagens. Os bloqueios embutidos são familiares para muitos desenvolvedores e são mais simples e compactos em uso. Como os bloqueios explícitos devem ser chamados manualmente de desbloqueio no bloco finalmente, é relativamente mais seguro usar bloqueios embutidos. Ao mesmo tempo, é mais provável que melhore o desempenho de sincronizado em vez de reentrante no futuro. Como o sincronizado é uma propriedade interna da JVM, ela pode realizar algumas otimizações, como a otimização de eliminação de travamento para objetos de trava entre roscas, eliminando a sincronização de bloqueios internos, aumentando a granularidade da trava e, se essas funções são implementadas por meio de bloqueios de biblioteca de classe, é improvável. Portanto, quando alguns recursos avançados são necessários, o ReentrantLock deve ser usado, que incluem: operações de aquisição de bloqueio cronometráveis, pesquisadas e interrompíveis, filas justas e bloqueios de estrutura não-block. Caso contrário, o sincronizado deve ser usado primeiro.
3. Operações de aquisição e liberação de bloqueios
Vamos primeiro olhar para o código de amostra usando o ReentrantLock para adicionar bloqueios.
public void Dosomething () {// O padrão é obter um bloqueio não-fair reentrantlock Lock = new reentrantlock (); tente {// bloqueio bloqueado.lock () antes da execução; // execute a operação ...} finalmente {// o bloqueio de bloqueio.unlock () finalmente libera; }}A seguir, é apresentada a API para adquirir e liberar bloqueios.
// a operação da obtenção de bloqueio public void Lock () {sync.lock ();} // a operação da liberação de bloqueio public void desbloqueio () {sync.release (1);}Você pode ver que as operações de aquisição do bloqueio e liberação do bloqueio são delegadas ao método de bloqueio e ao método de liberação do objeto Sync, respectivamente.
classe pública Reentrantlock implementa Lock, java.io.Serializable {private final Sync Sync; A classe estática abstrata sincroniza a abstratoqueedsynchronizer {abstrata void bloqueio (); } // Sincronizador que implementa a classe final de trava não-fair estática não-FairSync estende a Sync {Final void Lock () {...}} // Syncronizer que implementa a classe final da classe estática Fairsync estende a sincronização {Final Void Lock () {...}}}}}}}}}}}Cada objeto reentrantlock contém uma referência de sincronização do tipo. Esta classe de sincronização é uma classe interna abstrata. Herda do abstratoqueedSynchronizer. O método de bloqueio dentro é um método abstrato. A variável de membro da Reentrantlock é atribuída o valor durante a construção. Vamos dar uma olhada no que os dois métodos construtores de reentrantlock fazem?
// o construtor padrão sem parameterless public reentrantlock () {sync = new nonFairSync ();} // O construtor parametrizado public reentrantlock (boolean feira) {sync = Fair? New FairSync (): New NonFairSync ();}Chamar o construtor padrão sem parâmetros atribuirá a instância não-FAIRSYNC para sincronizar, e o bloqueio é um bloqueio não fair neste momento. O construtor de parâmetros permite que os parâmetros especifiquem se atribuem uma instância FairSync ou uma instância não -FAIRSYNC. Não-FairSync e FairSync herdam da classe Sync e reescreveram o método Lock (); portanto, existem algumas diferenças entre bloqueios justos e bloqueios não fair na maneira de obter bloqueios, sobre os quais falaremos abaixo. Vamos dar uma olhada na operação de liberar a fechadura. Cada vez que você chama o método desbloqueio (), basta executar a operação Sync.Release (1). Esta operação chamará o método Release () da classe AbstractQueedSynchronizer. Vamos revisá -lo novamente.
// Libere a operação de bloqueio (modo exclusivo) Public Final Boolean Release (int arg) {// gira o bloqueio de senha para ver se ele pode desbloquear se (tryRelease (arg)) {// obtenha o nó da cabeça h = cabeça; // Se o nó da cabeça não estiver vazio e o estado de espera não for igual a 0, acorde o nó sucessor se (h! } retornar true; } retornar false;}Este método de liberação é a API para liberar operações de bloqueio fornecidas pelo AQS. Primeiro, ele chama o método de tryrelease para tentar adquirir o bloqueio. O método TryRelease é um método abstrato e sua lógica de implementação está na sincronização da subclasse.
// Tente liberar o bloqueio final protegido Boolean TryRerelease (INT versões) {int c = getState () - liberações; // Se o thread segurando o bloqueio não for o encadeamento atual, será lançada uma exceção se (Thread.currentThread ()! = GetExclusivewnerthread ()) {lança new ilegalMonitorStateException (); } boolean livre = false; // Se o status de sincronização for 0, significa que o bloqueio é liberado se (c == 0) {// Defina o sinalizador do bloqueio sendo liberado como true free = true; // Defina o thread ocupado para esvaziar o setexclusivewnerthread (null); } setState (c); retornar livre;}Esse método TryRelease adquirirá primeiro o estado de sincronização atual, subtraia o estado de sincronização de corrente dos parâmetros passados para o novo estado de sincronização e, em seguida, determina se o novo estado de sincronização é igual a 0. Se for igual a 0, significa que o bloqueio atual é liberado. Em seguida, defina o estado de liberação do bloqueio como true e limpe o thread que atualmente ocupa o bloqueio e, finalmente, ligue para o método setState para definir o novo estado de sincronização e retornar o estado de liberação do bloqueio.
4. Fair Lock e Lock Deslear
Sabemos qual instância específica é o reentrantlock apontando para basear na sincronização. Durante a construção, a sincronização da variável de membro será atribuída. Se o valor for atribuído à instância não-FAIRSYNC, significa que é um bloqueio não fair e se o valor for atribuído à instância do FairSync, significa que é um bloqueio justo. Se for um bloqueio justo, os threads obterão a trava na ordem em que fazem os pedidos, mas na trava injusta, o comportamento de corte é permitido: quando um fio solicita uma trava injusta, se o estado da fechadura estiver disponível ao mesmo tempo em que a solicitação for emitida, o fio irá pular todos os threads que esperavam na fila para obter a trava diretamente. Vamos dar uma olhada em como obter bloqueios injustos.
// Syncronizador injusto Classe final estática O não -FairSync estende Sync {// implementa o método abstrato da classe pai para adquirir o bloqueio de bloqueio Lock Lock () {// use o método Cas para definir o estado de sincronização (seread.mermcrusive); } else {// Caso contrário, significa que o bloqueio foi ocupado, a chamada adquirir e deixar a fila de thread para sincronizar a fila para obter adquirir (1); }} // O método para tentar adquirir o booleano final protegido por bloqueio TryAcquire (int adquire) {return Non -FairtryAcquire (adquire); }} // Adquira bloqueios no modo não interruptivo (modo exclusivo) public Final Void adquirir (int arg) {if (! TryAcquire (arg) && adquirequeed (addwaiter (node.exclusive), arg)) {selfInterrupt (); }}Pode -se observar que, no método de bloqueio de trava injusta, o encadeamento alterará o valor do estado de sincronização de 0 para 1 na primeira etapa do CAS. De fato, esta operação é equivalente a tentar adquirir o bloqueio. Se a alteração for bem -sucedida, significa que o thread adquiriu o bloqueio agora e não há mais necessidade de fila na fila de sincronização. Se a alteração falhar, significa que o bloqueio não foi liberado quando o thread chegar primeiro, portanto, o método adquirir é chamado a seguir. Sabemos que esse método de aquisição é herdado do método abstractQueedSynchronizer. Vamos revisar esse método. Depois que o thread entra no método adquirido, o primeiro método TryAcquire para tentar adquirir o bloqueio. Como o Non -FairSync substitui o método TryAcquire e chama o método não -FairtryAcquire da sincronização da classe pai no método, o método Non -FairtryAcquire será chamado aqui para tentar adquirir o bloqueio. Vamos ver o que esse método faz especificamente.
// aquisição injusta do bloqueio final booleano não -FairtryAcquire (int adquire) {// Obtenha o thread atual atual Current = thread.currentThread (); // Obtenha o estado de sincronização atual int c = getState (); // Se o estado de sincronização for 0, significa que o bloqueio não está ocupado se (c == 0) {// use Cas para atualizar o estado de sincronização se (comparaDSetState (0, adquire)) {// Defina o thread atualmente ocupando o bloqueio setexclerownerthread (atual); retornar true; } // Caso contrário, é determinado se o bloqueio é o encadeamento atual} else if (current == getExclusivewnerthread ()) {// Se o bloqueio for mantido pelo encadeamento atual, modifique diretamente o estado de sincronização atual int nextc = c + adquirir; if (nextc <0) {lança um novo erro ("contagem máxima de bloqueio excedida"); } setState (NextC); retornar true; } // Se o bloqueio não for o segmento atual, retorne o sinalizador de falha Retorno false;}O método Non -FairtryAcquire é um método de sincronização. Podemos ver que, depois que o encadeamento entra nesse método, ele primeiro adquire o estado de sincronização. Se o estado de sincronização for 0, use a operação CAS para alterar o estado de sincronização. De fato, isso é para adquirir a fechadura novamente. Se o estado de sincronização não for 0, significa que a trava está ocupada. Neste momento, primeiro determinaremos se o thread segurando a fechadura é o encadeamento atual. Nesse caso, o estado de sincronização será aumentado em 1. Caso contrário, a operação de tentar adquirir o bloqueio falhará. Portanto, o método Addwaiter será chamado para adicionar o thread à fila de sincronização. Para resumir, no modo de bloqueio injusto, um encadeamento tentará adquirir dois bloqueios antes de inserir a fila de sincronização. Se a aquisição for bem -sucedida, ela não entrará na fila da fila da fila de sincronização, caso contrário, ele entrará na fila da fila da fila da fila de sincronização. Em seguida, vamos dar uma olhada em como obter bloqueios justos.
// O sincronizador que implementa a classe final de bloqueio justo FairSync estende a Sync {// implemente o método abstrato da classe pai para adquirir o bloqueio de bloqueio Lock Lock () {// Aquisição de chamada e deixe a fila de thread para sincronizar a fila para obter aquisição (1); } // Tente adquirir o bloqueio final protegido BOOLEAN TRYACQUIRE (INT adquire) {// Obtenha o thread atual atual Current = Thread.currentThread (); // Obtenha o estado de sincronização atual int c = getState (); // Se o estado de sincronização 0 significa que o bloqueio não está ocupado se (c == 0) {// defende se existe um nó para a frente na fila de sincronização se (! setExclusivewnerthread (atual); retornar true; } // Caso contrário, determine se o encadeamento atual segura o bloqueio} else if (current == getExclusivewnerthread ()) {// Se o thread atual mantiver o bloqueio, modifique diretamente o estado de sincronização int nextc = c + adquirir; if (nextc <0) {lança um novo erro ("contagem máxima de bloqueio excedida"); } setState (NextC); retornar true; } // Se o encadeamento atual não mantiver o bloqueio, a aquisição falha retorna falsa; }} Ao chamar o método de bloqueio de bloqueio justo, o método adquirir será chamado diretamente. Da mesma forma, o método adquirente chama primeiro o método FairSync Rewrite Tryacquire para tentar adquirir o bloqueio. Neste método, o valor do estado de sincronização é obtido pela primeira vez. Se o estado de sincronização for 0, significa que o bloqueio é liberado neste momento. A diferença da trava injusta é que ele chamará primeiro o método HasqueedPredeCessors para verificar se alguém está na fila de sincronização. Se ninguém estiver na fila, o valor do estado de sincronização será modificado. Você pode ver que o Fair Lock adota um método de cortesia aqui, em vez de adquirir o bloqueio imediatamente. Exceto por esta etapa que é diferente da trava injusta, as outras operações são as mesmas. Para resumir, podemos ver que o Fair Lock verifica apenas o status da trava uma vez antes de entrar na fila de sincronização. Mesmo se você achar que o bloqueio está aberto, não o adquirirá imediatamente. Em vez disso, você permitirá que os threads na fila de sincronização o adquiram primeiro. Portanto, pode -se garantir que a ordem em que todos os threads adquiram as fechaduras sob a trava justa seja a primeira e depois chegam, o que também garante a justiça de obter as fechaduras.
Então, por que não queremos que todas as fechaduras sejam justas? Afinal, a justiça é um bom comportamento, e a injustiça é um mau comportamento. Como as operações de suspensão e despertar do thread têm uma grande sobrecarga, isso afeta o desempenho do sistema, especialmente no caso de concorrência feroz, os bloqueios justos levarão a operações frequentes de suspensão e despertar de threads, enquanto bloqueios não fair podem reduzir essas operações, portanto, serão melhores que os bloqueios justos no desempenho. Além disso, como a maioria dos threads usa bloqueios por um tempo muito curto, e a operação de despertar do thread terá um atraso, é possível que o encadeamento B adquira o bloqueio imediatamente e solte o bloqueio depois de usá-lo. Isso leva a uma situação em que todos saem ganhando. O momento em que o encadeamento A adquire o bloqueio não é atrasado, mas o Thread B usa a trava com antecedência e sua taxa de transferência também foi melhorada.
5. O mecanismo de implementação das filas condicionais
Existem alguns defeitos na fila de condições internas. Cada bloqueio embutido pode ter apenas uma fila de condição associada, o que faz com que vários threads aguardem predicados de diferentes condições na mesma fila de condições. Então, toda vez que o NotifyAll é chamado, todos os threads de espera serão despertados. Quando o fio acorda, descobre que não é o predicado da condição que está esperando e será suspenso. Isso leva a muitas operações inúteis de despertar e suspender de threads, o que desperdiçará muitos recursos do sistema e reduzirá o desempenho do sistema. Se você deseja escrever um objeto simultâneo com vários predicados condicionais, ou se deseja obter mais controle do que a visibilidade da fila condicional, precisará usar bloqueio e condição explícitos em vez de bloqueios internos e filas condicionais. Uma condição e uma trava estão associadas, assim como uma fila de condição e uma trava embutida. Para criar uma condição, você pode ligar para o método Lock.NewCondition no bloqueio associado. Vamos primeiro olhar para um exemplo usando a condição.
classe pública limitada boundBuffer {Final Lock = new ReentrantLock (); condição final notfull = Lock.NewCondition (); // Predicado da condição: Notfull Final Condition NotEmpty = Lock.NewCondition (); // Predicado da condição: NotEmpty Final Object [] itens = novo objeto [100]; int putptr, Takeptr, contagem; // Método de produção public void put (objeto x) lança interruptedException {Lock.lock (); tente {while (count == items.length) notfull.await (); // A fila está cheia e o thread está aguardando itens [putptr] na fila noturna. itens [putptr] = x; if (++ putptr == items.length) putptr = 0; ++ contagem; NotEmpty.signal (); // A produção é bem -sucedida, acorde o nó da fila notável} finalmente {Lock.unlock (); }} // Método de consumo public objet Take () lança interruptedException {Lock.lock (); tente {while (count == 0) notempty.await (); // A fila está vazia, o thread aguarda o objeto x = itens [Takeptr] na fila NotEpty; if (++ Takeptr == items.length) TakePtr = 0; --contar; notfull.signal (); // O consumo é bem -sucedido, acorde o nó da fila noturna retorna x; } finalmente {Lock.unlock (); }}}Um objeto de bloqueio pode gerar várias filas de condição, e duas filas de condição são geradas aqui notadas e não expectativas. Quando o contêiner está cheio, o encadeamento que chama o método PUT precisa ser bloqueado. Aguarde até que o predicado da condição seja verdadeiro (o contêiner não está satisfeito) acorda e continua a executar; Quando o contêiner está vazio, o thread que chama o método Take precisa ser bloqueado. Aguarde até que o predicado da condição seja verdadeiro (o contêiner não está vazio) acorda e continua a executar. Esses dois tipos de threads esperam de acordo com diferentes predicados de condições, para que digam duas filas de condição diferentes para bloquear e aguardam até a hora certa antes de acordar chamando a API no objeto de condição. A seguir, é apresentado o código de implementação do método da New Condition.
// Crie uma fila de condição condição pública newcondition () {return sync.newcondition ();} classe estática abstrata síncil estende abstrateUedsynchronizer {// crie um novo objeto de condição final ConditionObject newcondition () {return newObject (); }}A implementação da fila de condições no ReentrantLock é baseada no abstratoQueedSynchronizer. O objeto de condição que obtemos ao chamar o método de NewCondition é uma instância do objeto de classe de classe interna do AQS. Todas as operações nas filas de condição são realizadas chamando a API fornecida pelo condicionalObject. Para uma implementação específica do ConditionObject, você pode verificar meu artigo "Série Java Concurrency [4] ----- AbstractQueUedSynchronizer Código-fonte Análise Condicional da fila" e não a repetirei aqui. Nesse ponto, nossa análise do código -fonte do Reentrantlock chegou ao fim. Espero que a leitura deste artigo ajude os leitores a entender e mestre reentrantlock.
O exposto acima é todo o conteúdo deste artigo. Espero que seja útil para o aprendizado de todos e espero que todos apoiem mais o wulin.com.