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

blog/disruptor-ringbuffer.md

Java LMAX Disruptor. Часть 1 - разбираем Preallocated RingBuffer

2 мин чтенияДоступно на английском

Примеры на Kotlin: Disruptor — Java-библиотека, но доступна из Kotlin через обычный interop, и всё написанное ниже 1:1 применимо к Java-коду — отличается только синтаксис.


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

Возьмите LinkedBlockingQueue. На каждое сообщение она создаёт новый узел-обёртку. При высокой частоте это постоянная нагрузка на GC: короткоживущие объекты копятся в young generation, сборщик просыпается чаще, паузы растут. Плюс сам узел в связном списке, а такие узлы разбросаны по куче как попало. Процессор гоняется за указателями (pointer chasing) вместо последовательного чтения памяти, и кэш-промахи съедают то, что должно было быть быстрым чтением.

Disruptor решает это отказом от аллокаций как таковых. При старте он выделяет RingBuffer фиксированного размера и сразу заполняет его объектами-событиями через EventFactory. Producer не создаёт новый объект на каждое сообщение, а получает индекс в буфере и мутирует уже существующий объект по этому индексу. Размер буфера обязан быть степенью двойки: тогда индекс считается через & (bufferSize - 1) вместо % bufferSize — побитовая операция быстрее целочисленного деления.

data class OrderEvent(var orderId: Long = 0)

val ringBuffer = RingBuffer.createSingleProducer(
    { OrderEvent() },
    1024 // степень двойки
)

ringBuffer.publishEvent { event, sequence ->
    event.orderId = orderId
}

publishEvent не создаёт OrderEvent — он берёт следующий слот и передаёт в лямбду уже существующий объект. За всё время работы producer’а в hot path не появляется ни одной новой аллокации, а данные в буфере лежат подряд, а не разбросаны по куче.

Итог: предсказуемая latency без GC-пауз и последовательный доступ к памяти вместо pointer chasing.

Pointer chasing разобран подробнее в отдельной статье: Pointer chasing.

Переиспользование объектов создаёт свою проблему: несколько потоков теперь регулярно пишут в области памяти, которые физически расположены рядом. В следующем посте разберу почему это может замедлить код в разы, даже когда потоки логически друг другу не мешают.

Похожие записи

blog/pointer-chasing.md

9 мин чтения

Pointer chasing: почему связный список медленнее массива, хотя оба — O(n)

Замените связный список на массив, не трогая алгоритм — и получите ускорение в разы. Разбираемся почему на бенчмарках JMH: кэш-промахи, цепочка зависимостей чтений и во сколько раз реально медленнее pointer chasing.

blog/transactional.md

3 мин чтения

@Transactional

Разбор аннотации @Transactional в Spring: транзакции, уровни изолированности ACID, феномены грязного чтения и фантомов, механизм Proxy.