Утилита параллелизма Java с JCTools

Утилита параллелизма Java с JCTools

1. обзор

В этом руководстве мы познакомимся с библиотекойJCTools (Java Concurrency Tools).

Проще говоря, это предоставляет ряд служебных структур данных, подходящих для работы в многопоточной среде.

2. Неблокирующие алгоритмы

Traditionally, multi-threaded code which works on a mutable shared state uses locks для обеспечения согласованности данных и публикаций (изменения, сделанные одним потоком, которые видны другому).

Этот подход имеет ряд недостатков:

  • потоки могут быть заблокированы при попытке получить блокировку, не продвигаясь до тех пор, пока операция другого потока не будет завершена - это эффективно предотвращает параллелизм

  • чем тяжелее конфликт блокировок, тем больше времени JVM тратит на планирование потоков, управление конфликтами и очередями ожидающих потоков и тем меньше реальную работу она выполняет

  • возможны взаимоблокировки, если задействовано более одной блокировки, и они получены / разблокированы в неправильном порядке

  • Возможна опасностьpriority inversion - поток с высоким приоритетом заблокирован в попытке получить блокировку, удерживаемую потоком с низким приоритетом

  • в большинстве случаев используются грубые блокировки, что сильно вредит параллелизму - мелкозернистая блокировка требует более тщательного проектирования, увеличивает накладные расходы блокировки и более подвержена ошибкам

Альтернативой является использованиеnon-blocking algorithm, i.e. an algorithm where failure or suspension of any thread cannot cause failure or suspension of another thread.

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

Кроме того, эти алгоритмы -wait-free, если также есть гарантированный прогресс для каждого потока.

Вот неблокирующий примерStack из превосходной книгиJava Concurrency in Practice; он определяет основное состояние:

public class ConcurrentStack {

    AtomicReference> top = new AtomicReference>();

    private static class Node  {
        public E item;
        public Node next;

        // standard constructor
    }
}

А также пара методов 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;
}

Мы можем видеть, что алгоритм использует подробные инструкции сравнения и замены (CAS) и равенlock-free (даже если несколько потоков одновременно вызываютtop.compareAndSet(), один из них гарантированно будет успешно), но неwait-free, поскольку нет гарантии, что CAS в конечном итоге завершится успешно для любого конкретного потока.

3. зависимость

Во-первых, давайте добавим зависимость JCTools к нашемуpom.xml:


    org.jctools
    jctools-core
    2.1.2

Обратите внимание, что последняя доступная версия доступна наMaven Central.

4. Очереди JCTools

Библиотека предлагает несколько очередей для использования в многопоточной среде, т.е. один или несколько потоков записывают в очередь, а один или несколько потоков считывают из нее потокобезопасным способом без блокировки.

Общий интерфейс для всех реализацийQueue -org.jctools.queues.MessagePassingQueue.

4.1. Типы очередей

Все очереди могут быть классифицированы по их политике производителя / потребителя:

  • single producer, single consumer – такие классы именуются с использованием префиксаSpsc, например SpscArrayQueue

  • single producer, multiple consumers – использует префиксSpmc, например SpmcArrayQueue

  • multiple producers, single consumer – использует префиксMpsc, например MpscArrayQueue

  • multiple producers, multiple consumers – использует префиксMpmc, например MpmcArrayQueue

Важно отметить, чтоthere are no policy checks internally, i.e. a queue might silently misfunction in case of incorrect usage.

E.g. приведенный ниже тест заполняет очередьsingle-producer из двух потоков и проходит, даже если потребитель не гарантированно увидит данные от разных производителей:

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. Реализация очереди

Суммируя приведенные выше классификации, вот список очередей JCTools:

  • SpscArrayQueue одиночный производитель, единственный потребитель, внутренне использует массив, ограниченная емкость

  • SpscLinkedQueue одиночный производитель, единственный потребитель, использует связанный список внутри, несвязанная емкость

  • SpscChunkedArrayQueue один производитель, один потребитель, начинается с начальной мощности и увеличивается до максимальной мощности

  • SpscGrowableArrayQueue одиночный производитель, одиночный потребитель, начинает с начальной мощности и увеличивается до максимальной мощности. Это тот же контракт, что иSpscChunkedArrayQueue, единственное отличие - внутреннее управление чанками. Рекомендуется использоватьSpscChunkedArrayQueue, потому что он имеет упрощенную реализацию

  • SpscUnboundedArrayQueue одиночный производитель, единственный потребитель, использует массив внутри, несвязанная емкость

  • SpmcArrayQueue один производитель, несколько потребителей, внутренне использует массив, ограниченная емкость

  • MpscArrayQueue несколько производителей, один потребитель, внутренне использует массив, ограниченная емкость

  • MpscLinkedQueue несколько производителей, один потребитель, внутренне использует связанный список, несвязанная емкость

  • MpmcArrayQueue несколько производителей, несколько потребителей, внутренне использует массив, ограниченная емкость

4.3. Атомные очереди

Все очереди, упомянутые в предыдущем разделе, используютsun.misc.Unsafe. Однако с появлением Java 9 иJEP-260 этот API по умолчанию становится недоступным.

Итак, есть альтернативные очереди, которые используютjava.util.concurrent.atomic.AtomicLongFieldUpdater (публичный API, менее производительный) вместоsun.misc.Unsafe.

Они генерируются из очередей выше, и в их имена вставлено словоAtomic, например SpscChunkedAtomicArrayQueue илиMpmcAtomicArrayQueue.

По возможности рекомендуется использовать «обычные» очереди и прибегать кAtomicQueues только в средах, гдеsun.misc.Unsafe запрещен / неэффективен, например, HotSpot Java9 + и JRockit.

4.4. Вместимость

Все очереди JCTools также могут иметь максимальную емкость или быть несвязанными. When a queue is full and it’s bound by capacity, it stops accepting new elements.с

В следующем примере мы:

  • заполнить очередь

  • убедитесь, что он перестает принимать новые элементы после этого

  • слейте с него воду и убедитесь, что впоследствии можно будет добавить больше элементов

Обратите внимание, что несколько операторов кода отброшены для удобства чтения. Полную реализацию можно найти вon 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. Другие структуры данных JCTools

JCTools также предлагает несколько структур данных без очереди.

Все они перечислены ниже:

  • NonBlockingHashMap - это безблокировочная альтернативаConcurrentHashMap с лучшими масштабируемыми свойствами и, как правило, более низкой стоимостью мутации. Он реализован черезsun.misc.Unsafe, поэтому не рекомендуется использовать этот класс в среде HotSpot Java9 + или JRockit.

  • NonBlockingHashMapLong похож наNonBlockingHashMap, но использует примитивные ключиlong

  • NonBlockingHashSet простая оболочка вокругNonBlockingHashMap like JDK'sjava.util.Collections.newSetFromMap()

  • NonBlockingIdentityHashMap похож наNonBlockingHashMap, но сравнивает ключи по идентичности.

  • NonBlockingSetInt – - многопоточный набор битовых векторов, реализованный как массив примитивовlongs. Работает неэффективно в случае бесшумного автобокса

6. Тестирование производительности

Давайте использоватьJMH для сравненияArrayBlockingQueue JDK и Производительность очереди JCTools. JMH - это микропроцессорная среда с открытым исходным кодом от гуру Sun / Oracle JVM, которая защищает нас от неопределенности алгоритмов оптимизации компилятора / jvm). Не стесняйтесь получить более подробную информацию об этом вthis article.

Обратите внимание, что приведенный ниже фрагмент кода пропускает пару операторов, чтобы улучшить читаемость. Пожалуйста, найдите полный исходный код наGitHub:

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

Результаты (отрывок для 95-го процентиля, наносекунды за операцию):

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

Мы видим, что MpmcArrayQueue performs just slightly better than MpmcAtomicArrayQueue and ArrayBlockingQueue is slower by a factor of two.

7. Недостатки использования JCTools

Использование JCTools имеет важный недостаток -it’s not possible to enforce that the library classes are used correctly.. Например, рассмотрим ситуацию, когда мы начинаем использоватьMpscArrayQueue в нашем большом и зрелом проекте (обратите внимание, что должен быть один потребитель).

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

В идеале должна быть возможность запустить систему с определенным системным свойством, которое заставляет JCTools обеспечивать политику доступа к потокам. E.g. local/test/staging environments (but not production) might have it turned on. К сожалению, JCTools не предоставляет такую ​​собственность.

Еще одно соображение заключается в том, что даже несмотря на то, что мы гарантируем, что JCTools значительно быстрее, чем аналог JDK, это не означает, что наше приложение набирает такую ​​же скорость, как мы начинаем использовать реализации настраиваемой очереди. Большинство приложений не обмениваются большим количеством объектов между потоками и в основном связаны с вводом-выводом.

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

Теперь у нас есть базовое представление о служебных классах, предлагаемых JCTools, и мы увидели, насколько хорошо они работают по сравнению с аналогами JDK при большой нагрузке.

В заключение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.

Как всегда, полный исходный код примеров, приведенных выше, можно найти вover on GitHub.