BlockingQueue: Producer/Consumer with ArrayBlockingQueue and SynchronousQueue
Part 8 of 8 in Java Concurrency: Deep Dive
A BlockingQueue makes a producer thread wait when there’s no room to put an item, and a consumer thread wait when there’s nothing to take. This post runs two small Java programs, each compiled and run for real, to show a bounded queue’s put() actually blocking when full, and a SynchronousQueue — a queue with zero capacity — not letting put() return until a consumer is actually there.
ArrayBlockingQueue: put() blocks when full
BoundedQueueDemo creates an ArrayBlockingQueue<Integer> with capacity 2. A producer thread tries to put() 5 items back to back, timing when each call was requested and when it actually returned. A consumer thread waits 1000ms before taking anything, then takes 5 items with a 300ms pause between each.
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class BoundedQueueDemo {
public static void main(String[] args) throws InterruptedException {
BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(2);
long start = System.nanoTime();
Thread producer = new Thread(() -> {
for (int i = 1; i <= 5; i++) {
try {
long before = (System.nanoTime() - start) / 1_000_000;
queue.put(i);
long after = (System.nanoTime() - start) / 1_000_000;
System.out.println("producer: put(" + i + ") requested at " + before + "ms, returned at " + after + "ms");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
});
Thread consumer = new Thread(() -> {
try {
Thread.sleep(1000); // let producer fill the queue (capacity 2) and block on put(3) first
for (int i = 1; i <= 5; i++) {
int val = queue.take();
long at = (System.nanoTime() - start) / 1_000_000;
System.out.println("consumer: took " + val + " at " + at + "ms");
Thread.sleep(300);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
producer.start();
consumer.start();
producer.join();
consumer.join();
}
}
Real javac+java output, JDK 25 (Zulu):
producer: put(1) requested at 6ms, returned at 6ms
producer: put(2) requested at 11ms, returned at 11ms
producer: put(3) requested at 11ms, returned at 1006ms
consumer: took 1 at 1006ms
consumer: took 2 at 1309ms
producer: put(4) requested at 1006ms, returned at 1309ms
consumer: took 3 at 1609ms
producer: put(5) requested at 1309ms, returned at 1609ms
consumer: took 4 at 1910ms
consumer: took 5 at 2210ms
put(1) and put(2) returned immediately (6ms, 11ms) — the queue had room for both. put(3) was requested at 11ms but didn’t return until 1006ms, the same moment the consumer took its first item — the queue was full at capacity 2, so put(3) sat blocked until space opened up. put(4) and put(5) show the same pattern, each returning right when the consumer’s next take() freed a slot.
SynchronousQueue: put() waits for an actual consumer
SynchronousQueueDemo creates a SynchronousQueue<Integer>, which has no internal capacity at all. A producer calls put(1) right away. A consumer thread sleeps 800ms before calling take().
import java.util.concurrent.SynchronousQueue;
public class SynchronousQueueDemo {
public static void main(String[] args) throws InterruptedException {
SynchronousQueue<Integer> queue = new SynchronousQueue<>();
long start = System.nanoTime();
Thread producer = new Thread(() -> {
try {
long before = (System.nanoTime() - start) / 1_000_000;
System.out.println("producer: put(1) requested at " + before + "ms");
queue.put(1);
long after = (System.nanoTime() - start) / 1_000_000;
System.out.println("producer: put(1) returned at " + after + "ms");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
Thread consumer = new Thread(() -> {
try {
Thread.sleep(800); // consumer isn't ready for a while
long at = (System.nanoTime() - start) / 1_000_000;
System.out.println("consumer: about to take() at " + at + "ms");
int val = queue.take();
long after = (System.nanoTime() - start) / 1_000_000;
System.out.println("consumer: took " + val + " at " + after + "ms");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
producer.start();
consumer.start();
producer.join();
consumer.join();
}
}
Real javac+java output, JDK 25 (Zulu):
producer: put(1) requested at 5ms
consumer: about to take() at 805ms
producer: put(1) returned at 806ms
consumer: took 1 at 806ms
put(1) was requested at 5ms but didn’t return until 806ms — the same moment the consumer actually called take() (805ms) and received the value (806ms). There was no buffer for the item to sit in; put() stayed blocked for roughly 800ms until a consumer was there to hand it to directly.
Takeaway
The ArrayBlockingQueue run showed put() returning immediately while the queue had room (capacity 2) and blocking once it filled, resuming only when the consumer’s take() freed a slot. The SynchronousQueue run showed an even tighter version of the same idea: with zero capacity, put(1) requested at 5ms didn’t return until 806ms, the exact moment a consumer showed up to take it.