Guia para o ConcurrentSkipListMap

Guia para o ConcurrentSkipListMap

1. Visão geral

Neste artigo rápido, veremos a classeConcurrentSkipListMap do pacotejava.util.concurrent.

Essa construção nos permite criar uma lógica segura de thread de maneira livre de bloqueios. É ideal para problemas quando queremos fazer um instantâneo imutável dos dados enquanto outros threads ainda estão inserindo dados no mapa.

Estaremos resolvendo um problema desorting a stream of events and getting a snapshot of the events that arrived in the last 60 seconds using that construct.

2. Lógica de classificação de fluxo

Digamos que temos um fluxo de eventos que vêm continuamente de vários segmentos. Precisamos conseguir eventos dos últimos 60 segundos e também eventos anteriores a 60 segundos.

Primeiro, vamos definir a estrutura de nossos dados de evento:

public class Event {
    private ZonedDateTime eventTime;
    private String content;

    // standard constructors/getters
}

Queremos manter nossos eventos classificados usando o campoeventTime. Para conseguir isso usandoConcurrentSkipListMap,, precisamos passar umComparator para seu construtor ao criar uma instância dele:

ConcurrentSkipListMap events
 = new ConcurrentSkipListMap<>(
 Comparator.comparingLong(v -> v.toInstant().toEpochMilli()));

Estaremos comparando todos os eventos recebidos usando seus carimbos de data / hora. Estamos usando o métodocomparingLong() e passando a função de extração que pode receber um carimbo de data / horalong deZonedDateTime.

Quando nossos eventos estão chegando, precisamos apenas adicioná-los ao mapa usando o métodoput(). Observe que este método não requer nenhuma sincronização explícita:

public void acceptEvent(Event event) {
    events.put(event.getEventTime(), event.getContent());
}

OConcurrentSkipListMap tratará da classificação desses eventos por baixo usando oComparator que foi passado para ele no construtor.

Os prós mais notáveis ​​deConcurrentSkipListMap são os métodos que podem fazer um instantâneo imutável de seus dados de uma forma sem bloqueio. Para obter todos os eventos que chegaram no último minuto, podemos usar o métodotailMap() e passar o tempo do qual queremos obter os elementos:

public ConcurrentNavigableMap getEventsFromLastMinute() {
    return events.tailMap(ZonedDateTime.now().minusMinutes(1));
}

Ele retornará todos os eventos do último minuto. Será um instantâneo imutável e o mais importante é que outros threads de gravação podem adicionar novos eventos aConcurrentSkipListMap sem qualquer necessidade de bloqueio explícito.

Agora podemos obter todos os eventos que chegaram mais tarde naquele minuto a partir de agora - usando o métodoheadMap():

public ConcurrentNavigableMap getEventsOlderThatOneMinute() {
    return events.headMap(ZonedDateTime.now().minusMinutes(1));
}

Isso retornará um instantâneo imutável de todos os eventos com mais de um minuto. Todos os métodos acima pertencem à classeEventWindowSort, que usaremos na próxima seção.

3. Testando a lógica do fluxo de classificação

Depois de implementar nossa lógica de classificação usandoConcurrentSkipListMap,, agora podemostest it by creating two writer threads que enviará cem eventos cada:

ExecutorService executorService = Executors.newFixedThreadPool(3);
EventWindowSort eventWindowSort = new EventWindowSort();
int numberOfThreads = 2;

Runnable producer = () -> IntStream
  .rangeClosed(0, 100)
  .forEach(index -> eventWindowSort.acceptEvent(
      new Event(ZonedDateTime.now().minusSeconds(index), UUID.randomUUID().toString()))
  );

for (int i = 0; i < numberOfThreads; i++) {
    executorService.execute(producer);
}

Cada thread está invocando o métodoacceptEvent(), enviando os eventos que têmeventTime de agora para “agora menos cem segundos”.

Enquanto isso, podemos invocar o métodogetEventsFromLastMinute() que retornará o instantâneo dos eventos que estão dentro da janela de um minuto:

ConcurrentNavigableMap eventsFromLastMinute
  = eventWindowSort.getEventsFromLastMinute();

O número de eventos emeventsFromLastMinute irá variar em cada teste executado, dependendo da velocidade com que os encadeamentos produtores estarão enviando os eventos paraEventWindowSort.. Podemos afirmar que não há um único evento em o instantâneo retornado com mais de um minuto:

long eventsOlderThanOneMinute = eventsFromLastMinute
  .entrySet()
  .stream()
  .filter(e -> e.getKey().isBefore(ZonedDateTime.now().minusMinutes(1)))
  .count();

assertEquals(eventsOlderThanOneMinute, 0);

E que há mais de zero eventos no instantâneo que estão dentro da janela de um minuto:

long eventYoungerThanOneMinute = eventsFromLastMinute
  .entrySet()
  .stream()
  .filter(e -> e.getKey().isAfter(ZonedDateTime.now().minusMinutes(1)))
  .count();

assertTrue(eventYoungerThanOneMinute > 0);

NossogetEventsFromLastMinute() usa otailMap() abaixo.

Vamos testar agora ogetEventsOlderThatOneMinute() que está usando o métodoheadMap() doConcurrentSkipListMap:

ConcurrentNavigableMap eventsFromLastMinute
  = eventWindowSort.getEventsOlderThatOneMinute();

Desta vez, obtemos um instantâneo de eventos com mais de um minuto. Podemos afirmar que existem mais de zero desses eventos:

long eventsOlderThanOneMinute = eventsFromLastMinute
  .entrySet()
  .stream()
  .filter(e -> e.getKey().isBefore(ZonedDateTime.now().minusMinutes(1)))
  .count();

assertTrue(eventsOlderThanOneMinute > 0);

E a seguir, não há um único evento que seja do último minuto:

long eventYoungerThanOneMinute = eventsFromLastMinute
  .entrySet()
  .stream()
  .filter(e -> e.getKey().isAfter(ZonedDateTime.now().minusMinutes(1)))
  .count();

assertEquals(eventYoungerThanOneMinute, 0);

A coisa mais importante a notar é quewe can take the snapshot of data while other threads are still adding new values para oConcurrentSkipListMap.

4. Conclusão

Neste tutorial rápido, demos uma olhada no básico doConcurrentSkipListMap, junto com alguns exemplos práticos.

Aproveitamos o alto desempenho deConcurrentSkipListMap para implementar um algoritmo sem bloqueio que pode nos fornecer um instantâneo imutável de dados, mesmo se ao mesmo tempo vários threads estiverem atualizando o mapa.

A implementação de todos esses exemplos e trechos de código pode ser encontrada emGitHub project; este é um projeto Maven, portanto, deve ser fácil importar e executar como está.