Ainda usa BlockingQueue? Leia este artigo para aprender sobre disruptor

1. O que é uma fila

Ao ouvir a fila, creio que ninguém desconhece ela. As filas podem ser vistas em todos os lugares da nossa vida real. Quando você vai ao supermercado para fazer o check-out, vê todos em fila esperando pelo checkout. Por que eles ficam enfileirados? Imagine que todo mundo não tem qualidade e se aglomera para o caixa. Não só esse supermercado vai entrar em colapso, mas também vai facilmente causar vários incidentes de debandada. Claro, essas coisas acontecem com frequência em nossa realidade.

É claro que, no mundo da informática, a fila pertence a uma estrutura de dados. A fila usa FIFO (primeiro na primeira saída). Novos elementos (elementos esperando para entrar na fila) são sempre inseridos na cauda e lidos na cabeça. Comece a ler. Na computação, a fila é geralmente usada para enfileiramento (como a fila de espera do pool de threads, a fila de espera do bloqueio), desacoplamento (modo produtor-consumidor), assíncrono e assim por diante.

2. Fila no jdk

Todas as filas em jdk implementam a interface java.util.Queue e são divididas em duas categorias na fila, uma é thread-insegura, ArrayDeque, LinkedList, etc., e a outra está sob o pacote java.util.concurrent É thread-safe, e em nosso ambiente real, nossas máquinas são multi-threaded. Quando vários threads enfileiram a mesma fila, se o thread for usado, aparecerá inseguro, sobrescrevendo dados, perda de dados, etc. Imprevisível , Portanto, neste momento, só podemos escolher filas thread-safe. Abaixo está uma breve lista de algumas das filas thread-safe fornecidas no jdk:

Se o nome da fila está bloqueado. O ponto técnico chave da estrutura de dados é se há um bloqueio ou não
ArrayBlockingQueue é uma matriz de matriz ReentrantLock está bloqueado e vinculado
LinkedBlockingQueue é uma lista vinculada ReentrantLock está bloqueado e vinculado
LinkedTransferQueue é uma lista vinculada CAS sem bloqueio ***
DelayQueue sem heap Heap CAS sem bloqueio * **
Podemos ver que nossa fila livre de bloqueio é *** e a fila bloqueada está limitada. Isso envolverá um problema. Estamos em um ambiente online real, a fila ***, sim O impacto do nosso sistema é relativamente grande, o que pode causar o estouro direto de nossa memória, então devemos primeiro eliminar a fila ***. Claro, não é que a fila *** seja inútil, mas deve ser excluída em certos cenários. Em segundo lugar, há duas filas, ArrayBlockingQueue e LinkedBlockingQueue. Ambas são thread-safe controladas por ReentrantLock. A diferença entre as duas é uma matriz e uma lista vinculada. Na fila, o elemento da fila geralmente é obtido imediatamente após a obtenção É possível obter o próximo elemento, ou obter vários elementos da fila de uma vez, e o endereço do array na memória for contínuo, haverá otimização do cache no sistema operacional (a linha do cache também será apresentada abaixo), então a velocidade de acesso será um pouco melhor Primeiro, tentaremos escolher ArrayBlockingQueue. Acontece que em muitas estruturas de terceiros, como o primeiro log4j assíncrono, ArrayBlockingQueue é a escolha.

Claro, ArrayBlockingQueue também tem suas próprias desvantagens, ou seja, o desempenho é relativamente baixo, porque o jdk adicionará algumas filas sem bloqueio, na verdade, para aumentar o desempenho, é muito angustiante e precisa ser livre de bloqueio e precisa ser limitado. Neste momento, não posso deixar de dizer Por que você não vai para o céu? Mas alguém realmente foi para o céu.

3. Disruptor

Disruptor é o dia mencionado acima. Disruptor é uma fila de alto desempenho desenvolvida pela empresa britânica de comércio de câmbio LMAX e é uma estrutura de concorrência de código aberto e ganhou o Prêmio de Inovação do Programa Duke de 2011. Ele pode realizar a operação simultânea da fila de rede sem bloqueio, e o sistema de thread único baseado no Disruptor pode suportar 6 milhões de pedidos por segundo. No momento, estruturas bem conhecidas, incluindo Apache Storm, Camel, Log4j2, etc., integraram o Disruptor internamente para substituir a fila jdk para obter alto desempenho.

3.1 Por que é tão incrível?

O disruptor foi destruído acima. Você definitivamente terá perguntas. Ele pode realmente ser tão incrível? Minha resposta é: claro. Existem três grandes assassinos no disruptor:

  • CASO
  • Elimine o falso compartilhamento
  • RingBuffer tem esses três assassinos, Disruptor se tornou tão incrível.

    3.1.1 Bloqueio e CAS

O motivo pelo qual nosso ArrayBlockingQueue foi abandonado é porque usamos bloqueios pesados. Durante nosso processo de bloqueio, suspenderemos o bloqueio e, após desbloquear, retomaremos o encadeamento. Este processo terá uma certa sobrecarga e Uma vez que não adquirimos o bloqueio, este encadeamento só pode esperar para sempre e não pode fazer nada.

CAS (comparar e trocar), como o nome sugere, é a primeira comparação para trocar. Geralmente, é para comparar se é um valor antigo. Se for para trocar configurações, todos que estão familiarizados com o bloqueio otimista sabem que o CAS pode ser usado para implementar o bloqueio otimista. Não há thread no CAS. A comutação de contexto reduz a sobrecarga desnecessária. JMH é usado aqui, com dois threads, uma chamada de cada vez, e o teste é executado na minha máquina local. O código é o seguinte:

@BenchmarkMode
({
Mode
.
SampleTime
})
@OutputTimeUnit
(
TimeUnit
.
MILLISECONDS
)
@Warmup
(
iterations
=
3
,
 time 
=

5
,
 timeUnit 
=

TimeUnit
.
MILLISECONDS
)
@Measurement
(
iterations
=
1
,
batchSize 
=

100000000
)
@Threads
(
2
)
@Fork
(
1
)
@State
(
Scope
.
Benchmark
)
public

class

Myclass

{

Lock

lock

=

new

ReentrantLock
();

long
 i 
=

0
;

AtomicLong
 atomicLong 
=

new

AtomicLong
(
0
);

@Benchmark

public

void
 measureLock
()

{

lock
.
lock
();
        i
++;

lock
.
unlock
();

}

@Benchmark

public

void
 measureCAS
()

{
        atomicLong
.
incrementAndGet
();

}

@Benchmark

public

void
 measureNoLock
()

{
        i
++;

}
}

Os resultados do teste são os seguintes:

Resultados do teste:
Bloqueio 26000ms
CAS 4840ms
Sem bloqueio 197ms
Pode ser visto que Bloqueio é um número de cinco dígitos, CAS é um número de quatro dígitos e um menor livre de bloqueio é um número de três dígitos. A partir disso, podemos saber Lock> CAS> No lock.

E nosso Disruptor usa CAS, que usa CAS para definir alguns subscritos na fila, o que reduz os conflitos de bloqueio e melhora o desempenho.

Além disso, o CAS também é usado para outras filas sem bloqueio no jdk e o CAS também é usado para classes atômicas.

3.1.2 Compartilhamento falso

Ao falar sobre pseudo-compartilhamento, devo dizer que o cache da CPU do computador. O tamanho do cache é um dos indicadores importantes da CPU, e a estrutura e o tamanho do cache têm uma grande influência na velocidade da CPU. A frequência operacional do cache na CPU é extremamente alta. A mesma operação de freqüência, a eficiência do trabalho é muito maior do que a memória do sistema e disco rígido. No trabalho real, a CPU muitas vezes precisa ler o mesmo bloco de dados repetidamente, e o aumento da capacidade do cache pode melhorar muito a taxa de acerto dos dados lidos na CPU, em vez de pesquisar na memória ou disco rígido, melhorando assim o desempenho do sistema . Mas, considerando os fatores de área e custo do chip da CPU, o cache é muito pequeno.

Ainda usa BlockingQueue?  Leia este artigo para aprender sobre disruptor

O cache do processador pode ser dividido em um cache de primeiro nível e um cache de segundo nível.Atualmente, os processadores convencionais também possuem um cache de terceiro nível, e alguns processadores têm até um cache de quarto nível. Todos os dados armazenados em cada nível de cache fazem parte do próximo nível de cache.A dificuldade técnica e o custo de fabricação desses três caches estão relativamente diminuindo, então sua capacidade está relativamente aumentando.

Por que a CPU tem um design de cache como L1, L2, L3? O principal motivo é que o processador atual é muito rápido e muito lento para ler dados da memória (uma é porque a memória em si não é rápida o suficiente e a outra é porque está muito longe da CPU. Em geral, você precisa deixar a CPU esperar alguns minutos. Dez ou até centenas de ciclos de clock) Neste momento, para garantir a velocidade do processador, é necessário um atraso menor e uma memória mais rápida para ajudar, e este é o cache. Se você estiver interessado nisso, pode remover a CPU do computador e brincar com ela você mesmo.

Cada vez que você ouvir a Intel lançar uma nova cpu, como i7-7700k, 8700k, o tamanho do cache da cpu será otimizado. Se você estiver interessado, pode descer e pesquisar essas conferências ou publicar artigos.

cabeçalho 1 cabeçalho 2
linha 1 coluna 1 linha 1 coluna 2
linha 2 coluna 1 linha 2 coluna 2
A apresentação de QConpresentation de Martin e Mike deu parte do tempo para cada cache:

De CPU para ciclos de CPU necessários sobre o tempo aproximado necessário para
a memória principal
de cerca de 60-80 nanossegundos
transferência de barramento QPI (entre soquetes, não desenhado)
cerca de 20ns
L3 Cache cerca de 40-45 ciclos de cerca de 15ns
L2 de cerca de 10 ciclos Cache cerca de 3ns
Ll Cache Cerca de 3-4 ciclos Cerca de 1ns
Registre
1
linha de cache de ciclo

No cache multinível da cpu, ele não é armazenado como um item separado, mas uma estratégia semelhante a pageCahe, que é armazenada em uma linha de cache, e o tamanho da linha de cache é geralmente de 64 bytes. Em Java, Longo São 8 bytes, portanto, 8 Longs podem ser armazenados. Por exemplo, quando você acessa uma variável longa, ele carrega mais 7 auxiliares. Dissemos acima por que escolhemos arrays e não listas vinculadas, é por isso que, Na matriz, você pode contar com linhas em buffer para obter acesso rápido.
Ainda usa BlockingQueue?  Leia este artigo para aprender sobre disruptor
A linha do cache é tudo? NÃO, porque ainda traz uma deficiência, darei um exemplo aqui para ilustrar essa deficiência. Você pode imaginar uma fila de array, ArrayQueue, cuja estrutura de dados é a seguinte:

class

ArrayQueue
{

long
 maxSize
;

long
 currentIndex
;
}

Para maxSize, definimos o tamanho da matriz no início, e para currentIndex, ele marca a posição de nossa fila atual. Essa mudança é relativamente rápida. Você pode imaginar que, ao visitar maxSize, carregará currentIndex? Desta vez Se outros threads atualizarem o currentIndex, eles invalidarão a linha do cache na cpu. Observe que este é um regulamento da CPU. Não é apenas que o currentIndex está invalidado. Se continuar a acessar maxSize neste momento, você ainda terá que continuar a partir da memória. Ler, mas MaxSize é definido no início, devemos acessar o cache, mas é afetado pelo currentIndex que mudamos frequentemente.
Ainda usa BlockingQueue?  Leia este artigo para aprender sobre disruptor

A magia do enchimento

A fim de resolver o problema da linha de cache acima, o método Padding é usado no Disruptor,

class

LhsPadding
{

protected

long
 p1
,
 p2
,
 p3
,
 p4
,
 p5
,
 p6
,
 p7
;
}
class

Value

extends

LhsPadding
{

protected

volatile

long
 value
;
}
class

RhsPadding

extends

Value
{

protected

long
 p9
,
 p10
,
 p11
,
 p12
,
 p13
,
 p14
,
 p15
;
}

O valor é preenchido com outras variáveis ​​longas inúteis. Desta forma, ao modificar o valor, ele não afetará as linhas de cache de outras variáveis.

Finalmente, a propósito, a anotação @Contended é fornecida no jdk8. Claro, de modo geral, apenas o Jdk é permitido internamente. Se você usar você mesmo, deve configurar o parâmetro Jvm -RestricContentended = fase, que restringirá a configuração e o cancelamento desta anotação. Muitos artigos analisaram ConcurrentHashMap, mas ignoraram esta anotação. Esta anotação é usada em ConcurrentHashMap. Cada intervalo em ConcurrentHashMap usa um contador separado para cálculo, e esse contador está mudando o tempo todo. Essa anotação é usada para otimizar o preenchimento das linhas de cache para aumentar o desempenho.
Ainda usa BlockingQueue?  Leia este artigo para aprender sobre disruptor

3.1.3RingBuffer

No Disruptor, um array é usado para salvar nossos dados. Acima também introduzimos o uso de um array para salvar nosso acesso ao cache, mas no Disruptor, optamos por usar um array circular para salvar os dados, que é o RingBuffer. Deixe-me explicar primeiro que a matriz de anel não é uma matriz de anel real. Em RingBuffer, o restante é usado para acessar. Por exemplo, o tamanho da matriz é 10 e 0 acessa a posição do índice da matriz como 0. Na verdade, 10, 20, etc. É também a posição em que o índice da matriz é 0.

Na verdade, nessas estruturas, o restante não usa a operação%, mas a operação & e, que requer que você defina o tamanho para a enésima potência de 2, ou seja, 10, 100, 1000, etc., então subtraia 1 Se for 1, 11, 111, o índice & (tamanho -1) pode ser usado bem, de modo que o uso de operações de bit aumente a velocidade de acesso. Se você não definir o tamanho no disruptor para a enésima potência de 2, será lançada uma exceção de que o tamanho do buffer deve ser a enésima potência de 2.

Claro, isso não só resolve o problema de acesso rápido ao array, mas também resolve o problema de não precisar alocar memória novamente, e reduz a coleta de lixo, porque nós 0, 10, 20, etc. somos todos executados na mesma área de memória, então não há necessidade de alocar novamente A memória é freqüentemente recuperada pelo coletor de lixo JVM.
Ainda usa BlockingQueue?  Leia este artigo para aprender sobre disruptor

Desde então, os três principais assassinos foram eliminados. Com esses três principais assassinos, foi lançada a base para um disruptor de alto desempenho. A seguir, explicarei como usar o disruptor e o princípio de funcionamento específico do disruptor.

3.2 Como usar o disruptor

Aqui está um exemplo simples:

ublic 
static

void
 main
(
String
[]
 args
)

throws

Exception

{

// 队列中的元素

class

Element

{

@Contended

private

String
 value
;

public

String
 getValue
()

{

return
 value
;

}

public

void
 setValue
(
String
 value
)

{

this
.
value 
=
 value
;

}

}

// 生产者的线程工厂

ThreadFactory
 threadFactory 
=

new

ThreadFactory
()

{

int
 i 
=

0
;

@Override

public

Thread
 newThread
(
Runnable
 r
)

{

return

new

Thread
(
r
,

"simpleThread"

+

String
.
valueOf
(
i
++));

}

};

// RingBuffer生产工厂,初始化RingBuffer的时候使用

EventFactory
<
Element
>
 factory 
=

new

EventFactory
<
Element
>()

{

@Override

public

Element
 newInstance
()

{

return

new

Element
();

}

};

// 处理Event的handler

EventHandler
<
Element
>
 handler 
=

new

EventHandler
<
Element
>()

{

@Override

public

void
 onEvent
(
Element
 element
,

long
 sequence
,

boolean
 endOfBatch
)

throws

InterruptedException

{

System
.
out
.
println
(
"Element: "

+

Thread
.
currentThread
().
getName
()

+

": "

+
 element
.
getValue
()

+

": "

+
 sequence
);
//                Thread.sleep(10000000);

}

};

// 阻塞策略

BlockingWaitStrategy
 strategy 
=

new

BlockingWaitStrategy
();

// 指定RingBuffer的大小

int
 bufferSize 
=

8
;

// 创建disruptor,采用单生产者模式

Disruptor
<
Element
>
 disruptor 
=

new

Disruptor
(
factory
,
 bufferSize
,
 threadFactory
,

ProducerType
.
SINGLE
,
 strategy
);

// 设置EventHandler
        disruptor
.
handleEventsWith
(
handler
);

// 启动disruptor的线程
        disruptor
.
start
();

for

(
int
 i 
=

0
;
 i 
<

10
;
 i
++)

{
            disruptor
.
publishEvent
((
element
,
 sequence
)

->

{

System
.
out
.
println
(
"之前的数据"

+
 element
.
getValue
()

+

"当前的sequence"

+
 sequence
);
                element
.
setValue
(
"我是第"

+
 sequence 
+

"个"
);

});

}

}

Existem várias chaves no Disruptor: ThreadFactory: Esta é uma fábrica de threads para as threads necessárias para o produtor em nosso Disruptor quando é consumido. EventFactory: A fábrica de eventos, a fábrica usada para gerar nossos elementos de fila. No Disruptor, ele preencherá o RingBuffer diretamente quando for inicializado e estará no lugar imediatamente. EventHandler: Um manipulador usado para processar o Evento. Aqui, um EventHandler pode ser considerado um consumidor, mas vários EventHandlers são filas para consumo independente. WorkHandler: também é um manipulador usado para processar eventos.A diferença do acima é que vários consumidores compartilham a mesma fila. WaitStrategy: Estratégia de espera Existem várias estratégias no Disruptor para determinar quando os consumidores obtêm o consumo, qual é a estratégia a ser adotada se não houver dados? Aqui está uma breve lista de algumas estratégias no Disruptor

BlockingWaitStrategy: Por meio do bloqueio de thread, aguarde o produtor acordar e, após ser ativado, será reciclado para verificar se a sequência dependente foi consumida.

  • BusySpinWaitStrategy: o encadeamento está girando e esperando, o que pode consumir cpu

  • LiteBlockingWaitStrategy: Blocos de thread esperando o produtor acordar. Comparado com BlockingWaitStrategy, a diferença é signalNeeded.getAndSet. Se dois threads acessam um acesso waitfor e um acesso signalAll ao mesmo tempo, o número de bloqueios de bloqueio pode ser reduzido.

  • LiteTimeoutBlockingWaitStrategy: Comparado com LiteBlockingWaitStrategy, o tempo de bloqueio é definido e uma exceção é lançada após o tempo expirar.

  • YieldingWaitStrategy: tente 100 vezes, então Thread.yield () retorna a cpu

    EventTranslator: a implementação dessa interface pode converter nossas outras estruturas de dados em eventos que circulam no disruptor.

3.3 princípio de funcionamento

Os três assassinos do CAS, reduzindo o falso compartilhamento, e o RingBuffer já foram apresentados acima. Vamos apresentar todo o processo de produtores e consumidores no Disruptor.

3.3.1 Produtor

Para produtores, eles podem ser divididos em multiprodutores e produtores individuais, que são diferenciados por ProducerType.Single e ProducerType.MULTI. Para multiprodutores e produtores individuais, há mais CAS, porque os produtores individuais são de thread único. Portanto, não há necessidade de garantir a segurança do thread.

No disruptor, disruptor.publishEvent e disruptor.publishEvents () são geralmente usados ​​para envio individual e em grupo.

Postar um evento no disruptor na fila requer as seguintes etapas:

  1. Primeiro, obtenha o próximo local no RingBuffer que pode ser publicado no RingBuffer. Isso pode ser dividido em duas categorias:
    • Posição nunca escrita
    • Foi lido por todos os consumidores e pode estar na posição de escrita. Se você não ler até que continue tentando ler, o disruptor é muito inteligente, e nem sempre ocupa a CPU, mas através do LockSuport.park (), a thread é bloqueada e suspensa por um tempo, para evitar que a CPU continue. Este tipo de loop vazio, caso contrário, nenhum outro thread pode capturar a fatia de tempo da CPU.
      Ainda usa BlockingQueue?  Leia este artigo para aprender sobre disruptor
      Após a obtenção da posição, o cas será eliminado, se for um único fio não será necessário.
  2. Em seguida, chame o EventTranslator que apresentamos acima para fornecer o evento naquela posição no RingBuffer na primeira etapa ao EventTranslator para reescrita.
  3. Para publicação, há uma matriz adicional no disruptor para registrar o número de sequência atual do ringBuffer atual. Pegue o acima 0, 10 e 20. Ao gravar em 10, o avliableBuffer é gravado na posição correspondente. No momento, isso pertence a 10. Qual é a utilidade? Vou apresentá-lo mais tarde. Ao publicar, você precisa atualizar este avliableBuffer e, em seguida, despertar todos os produtores bloqueados.

Vamos desenhar resumidamente o processo abaixo. O exemplo acima não está correto quando tomamos 10 como exemplo, porque o bufferSize deve ser 2 elevado à enésima potência, então tomamos Buffersize = 8 como exemplo: o seguinte mostra que quando empurramos 8 eventos, é um círculo. Quando chegar a hora, o próximo processo de empurrar 3 mensagens: 1. Primeira chamada seguinte (3), estamos atualmente na posição 7, então os próximos três são 8, 9, 10 e o restante é 0, 1, 2 . 2. Reescreva os dados nas três áreas de memória 0, 1 e 2. 3. Escreva availableBuffer.
Ainda usa BlockingQueue?  Leia este artigo para aprender sobre disruptor

A propósito, não sei se você está familiarizado com o processo acima. É semelhante ao nosso 2PC, envio de dois estágios, primeiro bloqueie a localização do RingBuffer e, em seguida, envie e notifique os consumidores. Para a introdução específica de 2PC, por favor, refira-se a outro artigo meu.Então alguém irá perguntar sobre transações distribuídas e mostrar a ele este artigo.

3.3.1 Consumidor

Para os consumidores, há dois tipos descritos acima: um é que vários consumidores consomem de forma independente e o outro é que vários consumidores consomem a mesma fila. Aqui, apresentamos os mais complicados vários consumidores que consomem a mesma fila. , Pode entender isso também pode entender o consumo independente. Em nosso método disruptor.strat (), nosso thread de consumidor será iniciado para consumo em segundo plano. Existem duas filas em consumidores que precisam de nossa atenção, uma é a fila de progresso compartilhada por todos os consumidores e a outra é a fila de progresso de consumo independente de cada consumidor. 1. Execute a próxima preempção do Next CAS na fila compartilhada do consumidor e marque o progresso atual na fila de seu próprio progresso de consumo. 2. Inscreva-se para a próxima posição do RingBuffer legível. A inscrição aqui não é apenas para o próximo, mas pode se inscrever para um intervalo maior do que o Next. O processo de aplicação da estratégia de bloqueio é o seguinte:

  • Obtenha a última posição escrita pelo produtor para RingBuffer
  • Determinar se é menor do que a posição para a qual desejo me candidatar
  • Se for maior que, prova que a posição foi escrita e retorna ao produtor.
  • Caso o menor que a prova não tenha sido gravado nesta posição, ele será bloqueado na estratégia de bloqueio e será despertado durante a fase de submissão do produtor. 3. Realize uma verificação legível nesta posição, porque a posição para a qual você se inscreveu pode ser contínua. Por exemplo, o produtor está atualmente com 7 anos e, em seguida, solicite a leitura. Se o consumidor escreveu a posição dos números de série 8 e 10, Mas a posição de 9 não teve tempo de escrever, porque a primeira etapa retornará 10, mas 9 na verdade não é legível, então você deve encolher a posição para 8.
    Ainda usa BlockingQueue?  Leia este artigo para aprender sobre disruptor
    4. Se for menor que a próxima corrente após a redução, continue aplicando em um loop. 5. Passe para handler.onEvent () para o
    mesmo processamento . Vejamos um exemplo. Queremos nos inscrever para a posição next = 8. 1. Primeiro, preencha o progresso 8 na fila compartilhada e grave o progresso 7 na fila independente 2. Obtenha a posição legível máxima de 8. Aqui, de acordo com diferentes estratégias, escolhemos bloquear. Porque o produtor produz 8, 9, 10, Portanto, o valor retornado é 10, portanto, não há necessidade de compará-lo com o availableBuffer novamente. 3. Por fim, entregue-o ao manipulador para processamento.
    Ainda usa BlockingQueue?  Leia este artigo para aprender sobre disruptor

    4. Disruptor em Log4j

A figura a seguir mostra a comparação do uso de Disruptor pelo Log4j, ArrayBlockingQueue e throughput sincronizado do Log4j. Você pode ver que o uso de Disruptor explodiu outros. Claro, existem mais estruturas que usam Disruptor, então não vou apresentá-lo aqui.
Ainda usa BlockingQueue?  Leia este artigo para aprender sobre disruptor

Finalmente

Este artigo apresenta as deficiências das filas de bloqueio tradicionais. O artigo a seguir enfoca o Disruptor, o motivo pelo qual ele é tão incrível e o fluxo de trabalho específico.

Se alguém lhe pedir para projetar uma fila sem bloqueio eficiente no futuro, como você a projeta? Acredito que você possa resumir a resposta do artigo. Se você tiver dúvidas sobre isso ou quiser trocar ideias comigo, pode seguir minha conta oficial e adicionar meus amigos para discutir comigo.

Se você tiver alguma dúvida sobre as perguntas acima, pode prestar atenção à conta oficial, venha e discuta comigo, siga-o para receber os vídeos de materiais de aprendizagem Java mais recentes e os materiais de entrevista mais recentes gratuitamente.

Se você acha que este artigo é útil para você, ou se você tem alguma dúvida e deseja fornecer o serviço VIP gratuito 1v1, você pode seguir minha conta pública e receber os vídeos de materiais de aprendizagem java e os materiais de entrevista mais recentes gratuitamente. Seguir e encaminhar são meu maior apoio, O (∩_∩) O:

Ainda usa BlockingQueue?  Leia este artigo para aprender sobre disruptor

Acho que você gosta

Origin blog.51cto.com/14980978/2544834
Recomendado
Clasificación