blog/disruptor-ringbuffer.md
Java LMAX Disruptor. Часть 1 - разбираем Preallocated RingBuffer
Примеры на 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.
Переиспользование объектов создаёт свою проблему: несколько потоков теперь регулярно пишут в области памяти, которые физически расположены рядом. В следующем посте разберу почему это может замедлить код в разы, даже когда потоки логически друг другу не мешают.