Java Exchanger: Synchronized Data Exchange Between Two Threads
Exchanger is a synchronization point at which exactly two threads can exchange data: each thread presents an object, blocks until its partner arrives, then receives the partner's object in return. Both threads are blocked until both have arrived at the rendezvous point. This article covers the exchange() and exchange(timeout) overloads, the classic double-buffer pattern, and guidance on when Exchanger is, and is not, the right tool.
What Is an Exchanger?
Exchanger<V>java.util.concurrent
The design constraint is intentional: Exchanger
The canonical use case is double-buffering: a producer thread fills one buffer while a consumer thread processes another buffer filled in the previous cycle. When both finish, they swap buffers at the exchanger. This decouples production speed from consumption speed with no allocation overhead and no shared mutable state between cycles.
Basic Exchange Between Two Threads
Both threads must call exchange(V)Exchangerexchange()exchange()InterruptedException
import java.util.concurrent.Exchanger;
public class BasicExchangeDemo {
public static void main(String[] args) throws InterruptedException {
Exchanger<String> exchanger = new Exchanger<>();
Thread producer = new Thread(() -> {
try {
String dataToSend = "Hello from producer";
System.out.println("Producer sending: " + dataToSend);
// Blocks until consumer also calls exchange()
String received = exchanger.exchange(dataToSend);
System.out.println("Producer received: " + received);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
Thread consumer = new Thread(() -> {
try {
Thread.sleep(500); // simulate work before arriving at the exchange
String dataToSend = "Hello from consumer";
System.out.println("Consumer sending: " + dataToSend);
String received = exchanger.exchange(dataToSend);
System.out.println("Consumer received: " + received);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
producer.start();
consumer.start();
producer.join();
consumer.join();
}
}
Producer sending: Hello from producer
Consumer sending: Hello from consumer
Producer received: Hello from consumer
Consumer received: Hello from producer
Exchange with Timeout
The blocking exchange(V)exchange(V, long timeout, TimeUnit unit)TimeoutException
import java.util.concurrent.*;
Exchanger<Integer> exchanger = new Exchanger<>();
Thread workerA = new Thread(() -> {
try {
Integer result = 42;
// Wait at most 2 seconds for partner; throw TimeoutException if it doesn't come
Integer partnerResult = exchanger.exchange(result, 2, TimeUnit.SECONDS);
System.out.println("Worker A got: " + partnerResult);
} catch (TimeoutException e) {
System.err.println("Worker A timed out — partner never arrived");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
// Worker B never starts — Worker A will time out after 2 seconds
workerA.start();
workerA.join();
TimeoutExceptionexchange()The Double-Buffer Pattern
Double-buffering is the most common reason to reach for Exchanger
import java.util.concurrent.*;
public class DoubleBufferDemo {
private static final int BUFFER_SIZE = 8;
private static final int CYCLES = 4;
public static void main(String[] args) throws InterruptedException {
Exchanger<int[]> exchanger = new Exchanger<>();
// Filler: writes incrementing integers into its buffer, then swaps
Thread filler = new Thread(() -> {
int[] buffer = new int[BUFFER_SIZE];
int value = 0;
try {
for (int cycle = 0; cycle < CYCLES; cycle++) {
// Fill the buffer
for (int i = 0; i < BUFFER_SIZE; i++) {
buffer[i] = ++value;
}
System.out.println("Filler filled: " + java.util.Arrays.toString(buffer));
// Swap — hand over the filled buffer, get back the drained one
buffer = exchanger.exchange(buffer);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
// Drainer: receives filled buffer, sums it, then swaps back an empty one
Thread drainer = new Thread(() -> {
int[] buffer = new int[BUFFER_SIZE]; // starts with an empty buffer
try {
for (int cycle = 0; cycle < CYCLES; cycle++) {
// Swap — hand over the empty buffer, get back the filled one
buffer = exchanger.exchange(buffer);
// Drain the buffer
int sum = 0;
for (int v : buffer) sum += v;
System.out.println("Drainer sum: " + sum);
java.util.Arrays.fill(buffer, 0); // clear for next cycle
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
filler.start();
drainer.start();
filler.join();
drainer.join();
}
}
When to Use, and When to Avoid, Exchanger
Exchanger
Avoid Exchanger
- More than two threads.
only supports pairs. For N-thread barriers useExchanger ; for one-way handoff useCyclicBarrier orSynchronousQueue .BlockingQueue - One-directional data flow. If data always flows one way (producer to consumer), a
is simpler and more flexible: it decouples the threads without requiring both to arrive simultaneously.BlockingQueue - Optional or conditional exchange.
always blocks. If the exchange is optional or the partner may not always participate, useExchanger /SynchronousQueue.offer() with timeouts instead.poll() - Unbounded parallelism. If you have a pool of worker threads that pair off dynamically,
is not designed for that. The pairing is arbitrary and can lead to starvation if the number of threads is odd.Exchanger
| Need | Best tool |
|---|---|
| Two threads swap data each cycle | Exchanger |
| One-way producer → consumer handoff | BlockingQueue |
| N threads rendezvous at a phase boundary | CyclicBarrier |
| One-shot countdown to completion | CountDownLatch |
| Direct thread-to-thread handoff (no buffer) | SynchronousQueue |