Перейти к основному содержимому

Руководство по параллельным очередям в Java

· 5 мин. чтения

Задача: Медиана двух отсортированных массивов

Даны два отсортированных массива размерами n и m. Найдите медиану слияния этих двух массивов.
Временная сложность решения должна быть O(log(m + n)) ...

ANDROMEDA

1. Обзор

В этом руководстве мы рассмотрим некоторые основные реализации параллельных очередей в Java. Общие сведения об очередях см. в нашей статье «Руководство по интерфейсу очередей Java » .

2. Очереди

В многопоточных приложениях очереди должны обрабатывать несколько параллельных сценариев производителей-потребителей. Правильный выбор параллельной очереди может иметь решающее значение для достижения хорошей производительности наших алгоритмов.

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

2. Блокирующая и неблокирующая очередь

BlockingQueue предлагает простой потокобезопасный механизм . В этой очереди потоки должны ожидать доступности очереди. Производители будут ждать доступной мощности, прежде чем добавлять элементы, а потребители будут ждать, пока очередь не опустеет. В этих случаях неблокирующая очередь либо выдаст исключение, либо вернет специальное значение, например null или false .

Для реализации этого механизма блокировки интерфейс BlockingQueue предоставляет две функции поверх обычных функций Queue : put и take . Эти функции эквивалентны добавлению и удалению в стандартной очереди .

3. Параллельные реализации очередей

3.1. ArrayBlockingQueue

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

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

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

Кроме того, ArrayBlockingQueue использует одну блокировку как для операций ввода, так и для операций взятия . Это гарантирует отсутствие перезаписи записей за счет снижения производительности.

3.2. LinkedBlockingQueue

LinkedBlockingQueue использует вариант LinkedList , где каждый элемент очереди является новым узлом. Хотя это в принципе делает очередь неограниченной, она по-прежнему имеет жесткое ограничение Integer.MAX_VALUE .

С другой стороны, мы можем установить размер очереди с помощью конструктора LinkedBlockingQueue(int capacity) .

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

Поскольку LinkedBlockingQueue может быть как ограниченным, так и неограниченным, зачем нам использовать ArrayBlockingQueue вместо этого? LinkedBlockingQueue необходимо выделять и освобождать узлы каждый раз, когда элемент добавляется или удаляется из очереди . По этой причине ArrayBlockingQueue может быть лучшей альтернативой, если очередь быстро растет и быстро сжимается.

Говорят, что производительность LinkedBlockingQueue непредсказуема. Другими словами, нам всегда нужно профилировать наши сценарии, чтобы убедиться, что мы используем правильную структуру данных.

3.3. ПриоритетБлокингОчередь

PriorityBlockingQueue — это наше решение, когда нам нужно потреблять элементы в определенном порядке . Для этого PriorityBlockingQueue использует двоичную кучу на основе массива.

Хотя внутри используется единый механизм блокировки, операция взятия может выполняться одновременно с операцией помещения . Использование простой спин-блокировки делает это возможным.

Типичным вариантом использования является использование задач с разными приоритетами. Мы не хотим, чтобы задача с низким приоритетом заменяла задачу с высоким приоритетом .

3.4. DelayQueue

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

Поскольку это не очередь общего назначения, она не охватывает столько сценариев, сколько ArrayBlockingQueue или LinkedBlockingQueue . Например, мы можем использовать эту очередь для реализации простого цикла событий, аналогичного тому, что есть в NodeJS. Мы помещаем асинхронные задачи в очередь для последующей обработки по истечении срока их действия.

3.5. LinkedTransferQueue

LinkedTransferQueue представляет метод передачи . В то время как другие очереди обычно блокируются при производстве или потреблении элементов, LinkedTransferQueue позволяет производителю ожидать потребления элемента .

Мы используем LinkedTransferQueue , когда нам нужна гарантия того, что определенный элемент, который мы помещаем в очередь, был кем-то взят. Кроме того, мы можем реализовать простой алгоритм обратного давления, используя эту очередь. Действительно, блокируя производителей до момента потребления, потребители могут управлять потоком производимых сообщений .

3.6. Синхронная очередь

В то время как очереди обычно содержат много элементов, SynchronousQueue всегда будет иметь максимум один элемент. Другими словами, нам нужно рассматривать SynchronousQueue как простой способ обмена некоторыми данными между двумя потоками .

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

3.7. ConcurrentLinkedQueue

ConcurrentLinkedQueue — единственная неблокирующая очередь в этом руководстве. Следовательно, он предоставляет алгоритм «без ожидания», в котором операции добавления и опроса гарантированно являются потокобезопасными и возвращаются немедленно . Вместо блокировок эта очередь использует CAS (Compare-And-Swap) .

Внутри он основан на алгоритме из книги «Простые, быстрые и практичные неблокирующие и блокирующие алгоритмы параллельных очередей » Магеда М. Майкла и Майкла Л. Скотта.

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

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

4. Вывод

В этом руководстве мы рассмотрели различные реализации параллельных очередей, обсудив их сильные и слабые стороны. Имея это в виду, мы лучше подготовлены для разработки эффективных, надежных и доступных систем.