Руководство по ConcurrentSkipListMap

Руководство по ConcurrentSkipListMap

1. обзор

В этой быстрой статье мы рассмотрим классConcurrentSkipListMap из пакетаjava.util.concurrent.

Эта конструкция позволяет нам создавать поточно-ориентированную логику без блокировки. Он идеально подходит для проблем, когда мы хотим сделать неизменяемый снимок данных, в то время как другие потоки все еще вставляют данные в карту.

Будем решать задачуsorting a stream of events and getting a snapshot of the events that arrived in the last 60 seconds using that construct.

2. Логика сортировки потоков

Допустим, у нас есть поток событий, который постоянно поступает из нескольких потоков. Нам нужно иметь возможность принимать события за последние 60 секунд, а также события старше 60 секунд.

Во-первых, давайте определим структуру данных нашего события:

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

    // standard constructors/getters
}

Мы хотим, чтобы наши события были отсортированы с использованием поляeventTime. Чтобы добиться этого с помощьюConcurrentSkipListMap,, нам нужно передатьComparator его конструктору при создании его экземпляра:

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

Мы будем сравнивать все прибывшие события, используя их временные метки. Мы используем методcomparingLong() и передаем функцию извлечения, которая может принимать временную меткуlong изZonedDateTime.

Когда наши события прибывают, нам нужно только добавить их на карту с помощью методаput(). Обратите внимание, что этот метод не требует явной синхронизации:

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

ConcurrentSkipListMap будет обрабатывать сортировку этих событий ниже, используяComparator, которые были переданы ему в конструкторе.

Наиболее заметные плюсыConcurrentSkipListMap - это методы, которые могут сделать неизменяемый моментальный снимок своих данных без блокировки. Чтобы получить все события, которые прибыли в течение последней минуты, мы можем использовать методtailMap() и передать время, из которого мы хотим получить элементы:

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

Он вернет все события за последнюю минуту. Это будет неизменяемый моментальный снимок, и, что наиболее важно, другие потоки записи могут добавлять новые события вConcurrentSkipListMap без необходимости выполнять явную блокировку.

Теперь мы можем получить все события, которые пришли позже, через одну минуту - с помощью методаheadMap():

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

Это вернет неизменный снимок всех событий старше одной минуты. Все вышеперечисленные методы относятся к классуEventWindowSort, который мы будем использовать в следующем разделе.

3. Тестирование логики сортировки потока

После того, как мы реализовали нашу логику сортировки с использованиемConcurrentSkipListMap,, теперь мы можемtest it by creating two writer threads, который будет отправлять по сто событий каждое:

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);
}

Каждый поток вызывает методacceptEvent(), отправляя события сeventTime с этого момента на «сейчас минус сто секунд».

А пока мы можем вызвать методgetEventsFromLastMinute(), который вернет снимок событий, которые находятся в пределах минутного окна:

ConcurrentNavigableMap eventsFromLastMinute
  = eventWindowSort.getEventsFromLastMinute();

Количество событий вeventsFromLastMinute будет варьироваться в каждом тестовом прогоне в зависимости от скорости, с которой потоки-производители будут отправлять события вEventWindowSort.. Мы можем утверждать, что нет ни одного события в возвращенный снимок старше одной минуты:

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

assertEquals(eventsOlderThanOneMinute, 0);

И что в моментальном снимке больше нуля событий, которые находятся в одноминутном окне:

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

assertTrue(eventYoungerThanOneMinute > 0);

НашgetEventsFromLastMinute() используетtailMap() внизу.

Давайте теперь протестируемgetEventsOlderThatOneMinute(), который использует методheadMap() изConcurrentSkipListMap:

ConcurrentNavigableMap eventsFromLastMinute
  = eventWindowSort.getEventsOlderThatOneMinute();

На этот раз мы получаем снимок событий старше одной минуты. Мы можем утверждать, что таких событий больше нуля:

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

assertTrue(eventsOlderThanOneMinute > 0);

И затем, что нет ни одного события, которое происходит в последнюю минуту:

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

assertEquals(eventYoungerThanOneMinute, 0);

Самое важное, что нужно отметить, это то, что отwe can take the snapshot of data while other threads are still adding new values доConcurrentSkipListMap.

4. Заключение

В этом кратком руководстве мы рассмотрели основыConcurrentSkipListMap, а также несколько практических примеров.

Мы использовали высокую производительностьConcurrentSkipListMap для реализации неблокирующего алгоритма, который может служить нам неизменным моментальным снимком данных, даже если в то же время несколько потоков обновляют карту.

Реализацию всех этих примеров и фрагментов кода можно найти вGitHub project; это проект Maven, поэтому его должно быть легко импортировать и запускать как есть.