Core Java

The Disruptor and Mechanical Sympathy: Ring Buffers Against Queues

Most Java developers reach for a queue whenever two threads need to talk. It feels natural, and it usually works well. However, at the extreme end of performance, that habit can quietly become the most expensive line in your system.

The LMAX team discovered this while building a financial exchange. As a result, they designed the Disruptor, a ring buffer that outpaces Java’s best bounded queue by a wide margin. In this article, we will look at why it is fast, what the published numbers really show, and when you should leave it alone.

What Mechanical Sympathy Really Means

Martin Thompson, one of the Disruptor’s authors, popularised the term on his Mechanical Sympathy blog. The idea comes from motor racing: a driver who understands the machine gets more out of it. In software, it means designing code around how CPUs, caches and memory actually behave.

The LMAX technical paper uses the phrase in exactly this way. Instead of tuning an existing queue, the authors asked what modern hardware likes. Then they built a data structure around the answer.

Why Ordinary Queues Struggle

Contention is far more expensive than it looks

To show the cost, the paper increments a 64-bit counter 500 million times on a 2.4 GHz Intel Westmere EP. The work never changes. Only the coordination does.

MethodTime (ms)
Single thread300
Single thread with volatile write4,700
Single thread with CAS5,700
Single thread with lock10,000
Two threads with CAS30,000
Two threads with lock224,000

Source: LMAX Disruptor technical paper, section 2.1 (2011 hardware).

Look at the last row. Adding a second thread that fights for the lock turns 300 ms of work into 224,000 ms. In short, contended writes are the costliest operation in concurrent code, and queues are full of them.

Queues live at the extremes

Producers and consumers rarely run at the same pace. Consequently, a queue sits close to full or close to empty most of the time. That means the head, tail and size variables are written constantly, and they often share the same cache line. On top of that, a linked-list queue allocates a node for every item, which keeps the garbage collector busy.

How the Disruptor Works

One pre-allocated ring

All entries are created at startup and then reused. As a result, the garbage collector treats them as immortal. Because the array is contiguous, the CPU can also prefetch it efficiently. Furthermore, the ring size is a power of two, so finding a slot needs a cheap bit mask instead of a costly remainder operation.

Sequences instead of locks

Each producer and consumer tracks its own sequence number. A producer claims the next slot, writes into it, and then publishes by advancing a cursor. Meanwhile, consumers wait for that cursor, and producers watch consumer positions so the ring never wraps. With a single producer, the paper states that no locks or CAS operations are needed, only memory barriers.

Batching smooths out bursts

If a consumer falls behind, it simply processes everything up to the cursor in one pass. Therefore, a burst from the producer does not create a growing backlog. According to the authors, latency stays almost flat until the memory subsystem saturates, whereas queues show the familiar “J curve”.

What the Numbers Say

Latency: nanoseconds against milliseconds

The paper measures a three-stage pipeline, sending an event every microsecond. The mean latency per hop was 52 ns for the Disruptor and 32,757 ns for ArrayBlockingQueue. The tail tells the same story, since the worst case drops from about 5.07 ms to 0.18 ms.

Data: LMAX paper, Table 4 (2.2 GHz Core i7-2720QM, Java 1.6, Ubuntu 11.04). The 99% and 99.99% figures are the paper’s “observations less than” buckets, and the axis is logarithmic.

One detail deserves a careful look. The abstract claims a difference of three orders of magnitude, yet the table works out to roughly 630 times (32,757 divided by 52). That is closer to 2.8 orders. The gap is still enormous, but it is better to quote the exact figure.

Throughput: a modern re-run

The project documentation also includes a newer run on an AMD EPYC 9374F with OpenJDK 11. Here, ArrayBlockingQueue is compared with Disruptor 3 and Disruptor 4 across five topologies.

Data: LMAX paper, Table 3 (AMD EPYC 9374F, Linux 5.4, OpenJDK 11). Values rounded to 0.1 million ops/sec.
TopologyABQDisruptor 3Disruptor 4Speedup
Unicast 1P-1C20.9134.6160.47.7x
Pipeline 1P-3C5.276.1101.319.4x
Sequencer 3P-1C18.816.029.71.6x
Multicast 1P-3C2.468.270.029.7x
Diamond 1P-3C3.461.263.118.4x

Speedup is calculated from the paper’s raw figures (Disruptor 4 divided by ABQ).

The pattern is clear: the more stages and consumers, the bigger the win. However, the sequencer row is the honest outlier. With three producers, Disruptor 3 was actually slower than the queue (16.0 against 18.8 million), and Disruptor 4 only reached 1.6 times. Multiple producers must race to claim slots, so the advantage shrinks.

Read these results with care

These figures come from the project’s own documentation, so they are the authors’ numbers rather than an independent audit. The paper also says results vary between JVM runs. For that reason, treat them as a direction, not a promise, and measure your own workload.

When Should You Use It?

For most business applications, a plain queue or an executor is perfectly fine. A hand-off that costs tens of microseconds rarely matters next to a database call. The Disruptor pays off when that hand-off is the bottleneck. In addition, its wait strategies trade CPU for latency, so a spinning consumer is not free.

Checklist: is the Disruptor worth a try?

  • Microsecond latency and low jitter genuinely matter.
  • Profiling shows queue hand-offs, not I/O, as the cost.
  • Your events fit pre-allocated, reusable objects.
  • You have one producer, or can live with a smaller gain.
  • You can spare CPU for spinning or yielding consumers.

What We Have Learned

Queues are slow at the extremes because they mix three jobs together: storage, producer coordination and consumer notification. The Disruptor separates them. It uses a pre-allocated ring, per-thread sequences and batching, so threads rarely fight over the same memory. The published data shows up to about 630 times lower mean latency and 8 to 30 times higher throughput in multi-stage pipelines. Even so, multiple producers narrow the gap, the benchmarks are the authors’ own, and the pattern costs CPU and design effort. In short, measure first, then adopt it only where a queue is truly the bottleneck.

Sources

Eleftheria Drosopoulou

Eleftheria is an Experienced Business Analyst with a robust background in the computer software industry. Proficient in Computer Software Training, Digital Marketing, HTML Scripting, and Microsoft Office, they bring a wealth of technical skills to the table. Additionally, she has a love for writing articles on various tech subjects, showcasing a talent for translating complex concepts into accessible content.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Back to top button