Definition
Exchanger– a synchronization point where exactly two threads meet and swap objects.exchange(V x)– offers your object and blocks until the partner calls exchange, then returns the partner's object.- Symmetric – there is no sender or receiver role; both give and receive.
- Timed variant –
exchange(V, long, TimeUnit)throwsTimeoutExceptionif no partner arrives. - Happens-before – everything written to the buffer before exchange is visible to the other thread after exchange returns.
Picture two clerks at National Bank of India standing on either side of a swinging door. The data-entry clerk fills a tray with trade forms; the posting clerk empties a tray into the ledger. When both reach the door they swap trays in one motion. Neither waits idle with nothing to do, and no tray is ever touched by both at once.
Understanding the Concepts
What is Exchanger?
You create one with Exchanger. The only essential method is exchange(V x), which blocks until the other thread also calls exchange, then each thread receives the other's object. The swap is atomic and symmetric.
How does double buffering work?
Double buffering keeps two buffers alive. The filler fills one while the drainer drains the other. When both are done, they swap buffers and repeat. This keeps both threads busy and avoids contention on a shared buffer.
package com.bank.buffer;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Exchanger;
public class DoubleBufferDemo {
private static final int CAPACITY = 100;
private static final Exchanger<List<String>> exchanger = new Exchanger<>();
public static void main(String[] args) {
new Thread(() -> filler(), "filler").start();
new Thread(() -> drainer(), "drainer").start();
}
static void filler() {
List<String> buffer = new ArrayList<>(CAPACITY);
try {
for (int i = 0; i < 500; i++) {
buffer.add("Trade-" + i);
if (buffer.size() == CAPACITY) {
buffer = exchanger.exchange(buffer); // hand full, get empty
buffer.clear();
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
static void drainer() {
List<String> buffer = new ArrayList<>(CAPACITY);
try {
for (int round = 0; round < 5; round++) {
buffer = exchanger.exchange(buffer); // hand empty, get full
buffer.forEach(trade -> System.out.println("Posted: " + trade));
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
How does the timed variant protect against stalls?
If one thread crashes or stalls, the other blocks forever in exchange(). The timed variant prevents this by throwing TimeoutException after the specified duration.
try {
buffer = exchanger.exchange(buffer, 5, TimeUnit.SECONDS);
} catch (TimeoutException e) {
System.err.println("Partner stalled. Raising alert for operations.");
alertOps("Exchanger partner not responding");
}
How does Exchanger compare to BlockingQueue?
| Feature | Exchanger | BlockingQueue |
|---------|-----------|---------------|
| Threads | Exactly two | Many producers and consumers |
| Direction | Bidirectional swap | One-directional flow |
| Internal storage | None (rendezvous) | Bounded or unbounded buffer |
| Coupling | Tight (both must meet) | Loose (decoupled) |
| Buffer reuse | Yes (two objects bounce) | No (elements flow through) |
| GC pressure | Low (reuse same buffers) | Higher (elements created and discarded) |
What about memory visibility?
Exchange establishes a happens-before relationship as defined by the Java Memory Model. Everything written to the buffer before calling exchange is visible to the partner after exchange returns. No additional synchronization is needed on the buffer contents.
How is Exchanger used in a banking trade ingest pipeline?
At National Bank of India, the trade ingest path uses an Exchanger to swap trade lists between a reader thread (reads trades from a network socket) and a writer thread (batches inserts to the database). Two lists are always alive. The reader fills one while the writer drains the other. After swap, both continue with fresh buffers. No garbage churn, no shared-buffer contention.
package com.bank.ingest;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Exchanger;
public class TradeIngestPipeline {
private static final int BATCH_SIZE = 200;
private final Exchanger<List<Trade>> exchanger = new Exchanger<>();
public void startReader() {
List<Trade> buffer = new ArrayList<>(BATCH_SIZE);
while (running) {
Trade trade = readFromSocket();
buffer.add(trade);
if (buffer.size() >= BATCH_SIZE) {
try {
buffer = exchanger.exchange(buffer);
buffer.clear();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
}
}
public void startWriter() {
List<Trade> buffer = new ArrayList<>(BATCH_SIZE);
while (running) {
try {
buffer = exchanger.exchange(buffer);
batchInsert(buffer);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
}
}
What are common mistakes?
Using Exchanger with more than two threads leads to unpredictable pairing. Thread A might exchange with Thread B or Thread C, depending on timing. For multi-party exchange, use a different mechanism.
What are common interview follow-up questions?
- Can you use Exchanger for more than two threads? Technically yes, but pairing is non-deterministic and usually incorrect.
- How does double buffering reduce GC pressure? Only two buffer objects are ever created and reused. No intermediate objects are garbage collected.
- When is BlockingQueue a better choice? When you have multiple producers or consumers, or when you want decoupling and buffering between unequally-paced threads.
Visual Overview
Double Buffering with Exchanger:
Filler Thread Exchanger Drainer Thread
───────────── ───────── ──────────────
fills buffer A │ drains buffer B
│ │ │
└──── exchange(A) ─────┤───── exchange(B) ─┘
│
SWAP: A ↔ B
│
┌──── receives B ──────┤───── receives A ──┐
│ │ │
clears B, fills B │ drains A
│ │ │
└──── exchange(B) ─────┤───── exchange(A) ─┘
│
SWAP: B ↔ A
Only two buffers ever exist. No garbage churn.
Key Takeaway
Exchanger is a synchronization point where exactly two threads meet and swap objects. The textbook use is double buffering, where a filler works on one buffer while a drainer works on another, and at the rendezvous they swap in one motion. Only two buffers ever exist, there is no garbage churn, and exchange establishes a happens-before relationship so the contents are safely visible.
At National Bank of India we used this on the trade ingest path, swapping lists between a reader and a writer to keep both busy and never share a list between them.
Remember one sentence: Exchanger is a two-thread rendezvous that swaps objects atomically, and its killer application is double buffering where two buffers bounce back and forth between a producer and a consumer.
---