Utilitário de simultaneidade Java com JCTools
1. Visão geral
Neste tutorial, apresentaremos a bibliotecaJCTools (Java Concurrency Tools).
Simplificando, isso fornece várias estruturas de dados de utilidade adequadas para trabalhar em um ambiente multithread.
2. Algoritmos de Não Bloqueio
Traditionally, multi-threaded code which works on a mutable shared state uses locks para garantir a consistência dos dados e publicações (alterações feitas por um segmento que são visíveis para outro).
Essa abordagem tem várias desvantagens:
-
threads podem ser bloqueados na tentativa de adquirir um bloqueio, sem progredir até que a operação de outra thread seja concluída - isso evita efetivamente o paralelismo
-
quanto maior a contenção de bloqueio, mais tempo a JVM gasta lidando com encadeamentos de planejamento, gerenciando contenção e filas de encadeamentos em espera e menos trabalho real está realizando
-
os bloqueios são possíveis se mais de um bloqueio estiver envolvido e eles forem adquiridos / liberados na ordem errada
-
um risco depriority inversion é possível - um thread de alta prioridade está bloqueado na tentativa de obter um bloqueio mantido por um thread de baixa prioridade
-
na maioria das vezes são usados bloqueios de granulação grossa, prejudicando muito o paralelismo - o bloqueio de granulação fina requer um design mais cuidadoso, aumenta a sobrecarga do bloqueio e é mais propenso a erros
Uma alternativa é usar umnon-blocking algorithm, i.e. an algorithm where failure or suspension of any thread cannot cause failure or suspension of another thread.
Um algoritmo sem bloqueio élock-free se pelo menos um dos threads envolvidos tem garantia de progresso em um período arbitrário de tempo, ou seja, os conflitos não podem surgir durante o processamento.
Além disso, esses algoritmos sãowait-free se também houver um progresso garantido por thread.
Aqui está um exemploStack não bloqueador do excelente livroJava Concurrency in Practice; define o estado básico:
public class ConcurrentStack {
AtomicReference> top = new AtomicReference>();
private static class Node {
public E item;
public Node next;
// standard constructor
}
}
E também alguns métodos de API:
public void push(E item){
Node newHead = new Node(item);
Node oldHead;
do {
oldHead = top.get();
newHead.next = oldHead;
} while(!top.compareAndSet(oldHead, newHead));
}
public E pop() {
Node oldHead;
Node newHead;
do {
oldHead = top.get();
if (oldHead == null) {
return null;
}
newHead = oldHead.next;
} while (!top.compareAndSet(oldHead, newHead));
return oldHead.item;
}
Podemos ver que o algoritmo usa instruções refinadas de comparação e troca (CAS) e élock-free (mesmo que vários threads chamemtop.compareAndSet() simultaneamente, um deles é garantido bem-sucedido), mas nãowait-free, pois não há garantia de que o CAS eventualmente terá êxito para qualquer thread em particular.
3. Dependência
Primeiro, vamos adicionar a dependência JCTools ao nossopom.xml:
org.jctools
jctools-core
2.1.2
Observe que a última versão disponível está disponível emMaven Central.
4. JCTools Queues
A biblioteca oferece várias filas para usar em um ambiente multithread, ou seja, um ou mais encadeamentos gravam em uma fila e um ou mais encadeamentos leem dela de maneira livre de trava e sem trava.
A interface comum para todas as implementações deQueue éorg.jctools.queues.MessagePassingQueue.
4.1. Tipos de filas
Todas as filas podem ser categorizadas em suas políticas de produtor / consumidor:
-
single producer, single consumer – tais classes são nomeadas usando o prefixoSpsc, por exemplo SpscArrayQueue
-
single producer, multiple consumers – usa o prefixoSpmc, por exemplo SpmcArrayQueue
-
multiple producers, single consumer – usa o prefixoMpsc, por exemplo MpscArrayQueue
-
multiple producers, multiple consumers – usa o prefixoMpmc, por exemplo MpmcArrayQueue
É importante observar quethere are no policy checks internally, i.e. a queue might silently misfunction in case of incorrect usage.
E.g. o teste abaixo preenche uma filasingle-producer de dois encadeamentos e passa, embora o consumidor não tenha garantia de ver dados de produtores diferentes:
SpscArrayQueue queue = new SpscArrayQueue<>(2);
Thread producer1 = new Thread(() -> queue.offer(1));
producer1.start();
producer1.join();
Thread producer2 = new Thread(() -> queue.offer(2));
producer2.start();
producer2.join();
Set fromQueue = new HashSet<>();
Thread consumer = new Thread(() -> queue.drain(fromQueue::add));
consumer.start();
consumer.join();
assertThat(fromQueue).containsOnly(1, 2);
4.2. Implementações de fila
Resumindo as classificações acima, aqui está a lista de filas do JCTools:
-
SpscArrayQueue – único produtor, único consumidor, usa uma matriz internamente, capacidade limitada
-
SpscLinkedQueue – único produtor, único consumidor, usa lista vinculada internamente, capacidade não ligada
-
SpscChunkedArrayQueue – único produtor, único consumidor, começa com a capacidade inicial e cresce até a capacidade máxima
-
SpscGrowableArrayQueue – único produtor, único consumidor, começa com a capacidade inicial e cresce até a capacidade máxima. Este é o mesmo contrato queSpscChunkedArrayQueue, a única diferença é o gerenciamento de blocos internos. É recomendável usarSpscChunkedArrayQueue porque tem uma implementação simplificada
-
SpscUnboundedArrayQueue – único produtor, único consumidor, usa uma matriz internamente, capacidade não ligada
-
SpmcArrayQueue – único produtor, vários consumidores, usa uma matriz internamente, capacidade limitada
-
MpscArrayQueue – múltiplos produtores, único consumidor, usa uma matriz internamente, capacidade limitada
-
MpscLinkedQueue – múltiplos produtores, único consumidor, usa uma lista vinculada internamente, capacidade não vinculada
-
MpmcArrayQueue – vários produtores, vários consumidores, usa uma matriz internamente, capacidade limitada
4.3. Filas Atômicas
Todas as filas mencionadas na seção anterior usamsun.misc.Unsafe. No entanto, com o advento do Java 9 e doJEP-260, essa API se torna inacessível por padrão.
Portanto, existem filas alternativas que usamjava.util.concurrent.atomic.AtomicLongFieldUpdater (API pública, menos desempenho) em vez desun.misc.Unsafe.
Eles são gerados a partir das filas acima e seus nomes têm a palavraAtomic inserida no meio, por exemplo, SpscChunkedAtomicArrayQueue ouMpmcAtomicArrayQueue.
É recomendado usar filas "regulares" se possível e recorrer aAtomicQueues apenas em ambientes ondesun.misc.Unsafe é proibido / ineficaz como HotSpot Java9 + e JRockit.
4.4. Capacidade
Todas as filas do JCTools também podem ter uma capacidade máxima ou não serem acopladas. When a queue is full and it’s bound by capacity, it stops accepting new elements.
No exemplo a seguir, nós:
-
preencha a fila
-
garantir que ele pare de aceitar novos elementos depois disso
-
drene dele e certifique-se de que é possível adicionar mais elementos depois
Observe que algumas instruções de código são descartadas para facilitar a leitura. A implementação completa pode ser encontrada emon GitHub:
SpscChunkedArrayQueue queue = new SpscChunkedArrayQueue<>(8, 16);
CountDownLatch startConsuming = new CountDownLatch(1);
CountDownLatch awakeProducer = new CountDownLatch(1);
Thread producer = new Thread(() -> {
IntStream.range(0, queue.capacity()).forEach(i -> {
assertThat(queue.offer(i)).isTrue();
});
assertThat(queue.offer(queue.capacity())).isFalse();
startConsuming.countDown();
awakeProducer.await();
assertThat(queue.offer(queue.capacity())).isTrue();
});
producer.start();
startConsuming.await();
Set fromQueue = new HashSet<>();
queue.drain(fromQueue::add);
awakeProducer.countDown();
producer.join();
queue.drain(fromQueue::add);
assertThat(fromQueue).containsAll(
IntStream.range(0, 17).boxed().collect(toSet()));
5. Outras estruturas de dados JCTools
O JCTools também oferece algumas estruturas de dados que não são da fila.
Todos eles estão listados abaixo:
-
NonBlockingHashMap – uma alternativaConcurrentHashMap sem bloqueio com propriedades de melhor dimensionamento e custos de mutação geralmente mais baixos. É implementado viasun.misc.Unsafe, portanto, não é recomendado usar esta classe em um ambiente HotSpot Java9 + ou JRockit
-
NonBlockingHashMapLong – comoNonBlockingHashMap, mas usa chaveslong primitivas
-
NonBlockingHashSet – um invólucro simples em torno deNonBlockingHashMap like JDK'sjava.util.Collections.newSetFromMap()
-
NonBlockingIdentityHashMap – comoNonBlockingHashMap, mas compara as chaves por identidade.
-
NonBlockingSetInt – a conjunto de vetores de bits multithread implementado como uma matriz delongs primitivos. Funciona ineficaz em caso de caixa automática silenciosa
6. Teste de performance
Vamos usarJMH para compararArrayBlockingQueue do JDK vs. Desempenho da fila JCTools. JMH é uma estrutura de micro-benchmark de código aberto dos gurus da Sun / Oracle JVM que nos protege do indeterminismo dos algoritmos de otimização de compilador / jvm). Fique à vontade para obter mais detalhes emthis article.
Observe que o trecho de código abaixo perde algumas instruções para melhorar a legibilidade. Encontre o código-fonte completo emGitHub:
public class MpmcBenchmark {
@Param({PARAM_UNSAFE, PARAM_AFU, PARAM_JDK})
public volatile String implementation;
public volatile Queue queue;
@Benchmark
@Group(GROUP_NAME)
@GroupThreads(PRODUCER_THREADS_NUMBER)
public void write(Control control) {
// noinspection StatementWithEmptyBody
while (!control.stopMeasurement && !queue.offer(1L)) {
// intentionally left blank
}
}
@Benchmark
@Group(GROUP_NAME)
@GroupThreads(CONSUMER_THREADS_NUMBER)
public void read(Control control) {
// noinspection StatementWithEmptyBody
while (!control.stopMeasurement && queue.poll() == null) {
// intentionally left blank
}
}
}
Resultados (trecho do percentil 95, nanossegundos por operação):
MpmcBenchmark.MyGroup:MyGroup·p0.95 MpmcArrayQueue sample 1052.000 ns/op
MpmcBenchmark.MyGroup:MyGroup·p0.95 MpmcAtomicArrayQueue sample 1106.000 ns/op
MpmcBenchmark.MyGroup:MyGroup·p0.95 ArrayBlockingQueue sample 2364.000 ns/op
Podemos ver que MpmcArrayQueue performs just slightly better than MpmcAtomicArrayQueue and ArrayBlockingQueue is slower by a factor of two.
7. Desvantagens do uso de JCTools
Usar JCTools tem uma desvantagem importante -it’s not possible to enforce that the library classes are used correctly. Por exemplo, considere uma situação em que começamos a usarMpscArrayQueue em nosso projeto grande e maduro (observe que deve haver um único consumidor).
Infelizmente, como o projeto é grande, existe a possibilidade de alguém cometer um erro de programação ou configuração e a fila agora é lida em mais de um encadeamento. O sistema parece funcionar como antes, mas agora há uma chance de os consumidores perderem algumas mensagens. Esse é um problema real que pode ter um grande impacto e é muito difícil de depurar.
Idealmente, deve ser possível executar um sistema com uma propriedade específica do sistema que força o JCTools a garantir a política de acesso ao encadeamento. E.g. local/test/staging environments (but not production) might have it turned on. Infelizmente, o JCTools não fornece essa propriedade.
Outra consideração é que, embora tenhamos garantido que o JCTools seja significativamente mais rápido do que a contraparte do JDK, isso não significa que nosso aplicativo ganhe a mesma velocidade à medida que começamos a usar as implementações de fila personalizadas. A maioria dos aplicativos não trocam muitos objetos entre threads e são principalmente ligados por I / O.
8. Conclusão
Agora temos uma compreensão básica das classes de utilitários oferecidas pelo JCTools e vimos como eles funcionam, em comparação com as contrapartes do JDK sob carga pesada.
Em conclusão,it’s worth to use the library only if we exchange a lot of objects between threads and even then it’s necessary to be very careful to preserve thread access policy.
Como sempre, o código-fonte completo dos exemplos acima pode ser encontradoover on GitHub.