Aqui está uma explicação e exemplo de como isso é realizado. Deixe-me saber se há peças que não estão claras.
Gist com fonte
Universal
Inicialização:
Os índices de encadeamento são aplicados de maneira atomicamente incrementada. Isso é gerenciado usando um AtomicInteger
nome nextIndex
. Esses índices são atribuídos aos threads por meio de uma ThreadLocal
instância que se inicializa, obtendo o próximo índice nextIndex
e incrementando-o. Isso acontece na primeira vez que o índice de cada thread é recuperado na primeira vez. A ThreadLocal
é criado para rastrear a última sequência criada por este encadeamento. É inicializado 0. A referência seqüencial do objeto de fábrica é passada e armazenada. Duas AtomicReferenceArray
instâncias são criadas de tamanho n
. O objeto final é atribuído a cada referência, tendo sido inicializado com o estado inicial fornecido pela Sequential
fábrica. n
é o número máximo de threads permitido. Cada elemento nessas matrizes 'pertence' ao índice de encadeamento correspondente.
Aplicar método:
Este é o método que faz o trabalho interessante. Faz o seguinte:
- Crie um novo nó para esta chamada: mine
- Defina esse novo nó na matriz de anúncios no índice do encadeamento atual
Em seguida, o loop de seqüenciamento começa. Ele continuará até que a chamada atual tenha sido sequenciada:
- encontre um nó na matriz de anúncios usando a sequência do último nó criado por este encadeamento. Mais sobre isso mais tarde.
- se um nó for encontrado na etapa 2, ele ainda não foi sequenciado, continue com ele; caso contrário, concentre-se apenas na chamada atual. Isso tentará apenas ajudar um outro nó por chamada.
- Qualquer que seja o nó selecionado na etapa 3, continue tentando sequenciá-lo após o último nó sequenciado (outros encadeamentos podem interferir.) Independentemente do sucesso, defina a referência principal do encadeamento atual para a sequência retornada por
decideNext()
A chave para o loop aninhado descrito acima é o decideNext()
método Para entender isso, precisamos olhar para a classe Node.
Classe de nó
Esta classe especifica nós em uma lista duplamente vinculada. Não há muita ação nesta classe. A maioria dos métodos são métodos de recuperação simples que devem ser bastante auto-explicativos.
método da cauda
isso retorna uma instância de nó especial com uma sequência de 0. Ele simplesmente atua como um marcador de posição até que uma chamada a substitua.
Propriedades e inicialização
seq
: o número de sequência, inicializado em -1 (significando sem sequência)
invocation
: o valor da chamada de apply()
. Situado na construção.
next
: AtomicReference
para o link direto. uma vez atribuído, isso nunca será alterado
previous
: AtomicReference
para o link reverso atribuído após o seqüenciamento e limpo portruncate()
Decidir Próximo
Este método é apenas um no Nó com lógica não trivial. Em poucas palavras, um nó é oferecido como candidato para ser o próximo nó na lista vinculada. O compareAndSet()
método verificará se sua referência é nula e, em caso afirmativo, defina a referência para o candidato. Se a referência já estiver definida, não fará nada. Esta operação é atômica, portanto, se dois candidatos forem oferecidos no mesmo momento, apenas um será selecionado. Isso garante que apenas um nó será selecionado como o próximo. Se o nó candidato estiver selecionado, sua sequência será configurada para o próximo valor e o link anterior será definido para este nó.
Voltando à classe Universal, aplique o método ...
Depois de chamar decideNext()
o último nó seqüenciado (quando marcado) com nosso nó ou um nó da announce
matriz, há duas ocorrências possíveis: 1. O nó foi sequenciado com êxito 2. Algum outro encadeamento antecipou esse encadeamento.
A próxima etapa é verificar se o nó foi criado para esta chamada. Isso pode acontecer porque esse encadeamento o sequenciou com êxito ou algum outro encadeamento o pegou da announce
matriz e o sequenciou para nós. Se não foi sequenciado, o processo é repetido. Caso contrário, a chamada será concluída limpando a matriz de anúncios no índice desse encadeamento e retornando o valor resultante da chamada. A matriz de anúncio é limpa para garantir que não haja referências ao nó existente que impeçam a coleta de lixo do nó e, portanto, mantenha todos os nós na lista vinculada a partir desse ponto em tempo real no heap.
Avaliar método
Agora que o nó da chamada foi sequenciado com êxito, a chamada precisa ser avaliada. Para fazer isso, o primeiro passo é garantir que as chamadas anteriores a esta tenham sido avaliadas. Se eles não tiverem esse segmento, não esperarão, mas farão esse trabalho imediatamente.
Método GuarantePrior
O ensurePrior()
método faz esse trabalho verificando o nó anterior na lista vinculada. Se o estado não estiver definido, o nó anterior será avaliado. Nó que é recursivo. Se o nó anterior ao nó anterior não tiver sido avaliado, ele chamará a avaliação desse nó e assim por diante.
Agora que o nó anterior é conhecido por ter um estado, podemos avaliar esse nó. O último nó é recuperado e designado a uma variável local. Se essa referência for nula, significa que algum outro encadeamento antecipou esse e já avaliou esse nó; definindo seu estado. Caso contrário, o estado do nó anterior é passado para o Sequential
método de aplicação do objeto, juntamente com a invocação desse nó. O estado retornado é definido no nó e o truncate()
método é chamado, limpando o link para trás do nó, pois ele não é mais necessário.
Método MoveForward
O método avançar tentará mover todas as referências principais para este nó se elas ainda não estiverem apontando para algo mais adiante. Isso é para garantir que, se um encadeamento parar de chamar, seu cabeçalho não reterá uma referência a um nó que não é mais necessário. O compareAndSet()
método garantirá que apenas atualizemos o nó se algum outro encadeamento não o tiver alterado desde que foi recuperado.
Anunciar matriz e ajudar
A chave para tornar essa abordagem livre de espera, em vez de simplesmente sem bloqueio, é que não podemos assumir que o agendador de encadeamentos dará prioridade a cada encadeamento quando necessário. Se cada thread simplesmente tentar sequenciar seus próprios nós, é possível que um thread possa ser continuamente esvaziado sob carga. Para explicar essa possibilidade, cada thread tentará primeiro 'ajudar' outros threads que talvez não possam ser sequenciados.
A idéia básica é que, conforme cada thread cria nós com êxito, as sequências atribuídas aumentam monotonicamente. Se um segmento ou segmentos estão antecipando continuamente outro segmento, o índice usado para encontrar nós não sequenciados na announce
matriz avançará. Mesmo que todos os encadeamentos que atualmente tentam sequenciar um determinado nó sejam continuamente esvaziados por outro encadeamento, eventualmente todos os encadeamentos tentarão sequenciar esse nó. Para ilustrar, construiremos um exemplo com três threads.
No ponto de partida, a cabeça dos três threads e os elementos de anúncio estão apontados para o tail
nó. O lastSequence
para cada thread é 0.
Neste ponto, o Thread 1 é executado com uma invocação. Ele verifica a matriz de anunciantes quanto à sua última sequência (zero), que é o nó que está atualmente programado para indexar. Sequencia o nó e lastSequence
está definido como 1.
O segmento 2 agora é executado com uma invocação, verifica a matriz de anúncios na sua última sequência (zero) e vê que não precisa de ajuda e, portanto, tenta sequenciar sua invocação. É bem-sucedido e agora lastSequence
está definido como 2.
O segmento 3 agora é executado e também vê que o nó em announce[0]
já está sequenciado e sequencia sua própria invocação. Agora lastSequence
está definido como 3.
Agora o segmento 1 é chamado novamente. Ele verifica a matriz de anúncios no índice 1 e descobre que ela já está sequenciada. Simultaneamente, o Thread 2 é chamado. Ele verifica a matriz de anúncios no índice 2 e descobre que ela já está sequenciada. Ambos Thread 1 e Thread 2 agora tentar sequenciar seus próprios nós. O segmento 2 vence e sequencia sua invocação. ElelastSequence
definido como 4. Enquanto isso, o segmento três foi chamado. Ele verifica o índice lastSequence
(mod 3) e descobre que o nó em announce[0]
não foi sequenciado. O segmento 2 é novamente chamado ao mesmo tempo que o segmento 1 está na segunda tentativa. Tópico 1localiza uma chamada não sequencial na announce[1]
qual é o nó recém-criado pelo Thread 2 . Ele tenta sequenciar a chamada do Thread 2 e obtém êxito. O segmento 2 encontra seu próprio nó announce[1]
e foi sequenciado. É definido lastSequence
como 5. O thread 3 é chamado e descobre que o nó no qual o thread 1 colocado announce[0]
ainda não é sequenciado e tenta fazer isso. Enquanto isso, o Thread 2 também foi chamado e pré-impõe o Thread 3. Sequencia seu nó e o define lastSequence
para 6.
Pobre Thread 1 . Mesmo que o Thread 3 esteja tentando sequenciá-lo, ambos os threads foram continuamente impedidos pelo agendador. Mas neste momento. Tópico 2 também está agora apontando para announce[0]
(6 mod 3). Todos os três encadeamentos estão configurados para tentar sequenciar a mesma chamada. Não importa qual thread seja bem-sucedido, o próximo nó a ser sequenciado será a chamada em espera do Thread 1, ou seja, o nó referenciado por announce[0]
.
Isso é inevitável. Para que os encadeamentos sejam antecipados, outros encadeamentos devem ser nós de seqüenciamento e, ao fazê-lo, eles moverão continuamente seuslastSequence
frente. Se o nó de um determinado encadeamento não for continuamente sequenciado, eventualmente todos os encadeamentos apontarão para seu índice na matriz de anúncios. Nenhum encadeamento fará mais nada até que o nó que está tentando ajudar seja sequenciado, o pior cenário é que todos os encadeamentos estejam apontando para o mesmo nó não sequenciado. Portanto, o tempo necessário para sequenciar qualquer chamada é uma função do número de threads e não do tamanho da entrada.