Producer and consumer with a BlockingQueue

Read the explanation
What if

A fast producer and a slow consumer share a queue with room for 2. The queue makes each one wait for the other.

0123456789put(a)producer: put(b)producer: put(c)producer: put(d): fullproducer: put(d)consumer: take(): emptyhandle(a)handle(b)handle(c)handle(d)

Tap any block to see what that thread is doing.

  • Running
  • Waiting / parked
Step 1/5

The consumer starts first. The queue is empty, so take() parks it.

1static final BlockingQueue<String> queue = new ArrayBlockingQueue<>(2); // room for 22 3static void produce() throws InterruptedException {4    for (String job : List.of("a", "b", "c", "d")) {5        queue.put(job); // waits while the queue is full6    }7}8 9static void consume() throws InterruptedException {10    for (int i = 0; i < 4; i++) {11        String job = queue.take(); // waits while the queue is empty12        handle(job);               // slow: takes a while per job13    }14}

Explanation

What just happened

The consumer started first and parked in take(), because the queue was empty. The producer put a, which woke the consumer. The producer then filled the queue with b and c. With no room left, put(d) parked the producer. As soon as the consumer took b, a slot opened and d went in.

Try the what-if switch

unbounded: with new LinkedBlockingQueue<>(), put() never waits. The producer finishes at once and the jobs pile up in the queue. Nothing slows the producer down, so with a fast producer the queue grows until memory runs out.

The queue is the contract

Neither side knows about the other. They only share the queue:

That automatic slowing down is called backpressure.

Pick the right queue

Stopping cleanly

A common pattern is a special “poison pill” item: the producer puts a marker like "STOP" last, and the consumer exits when it takes it.

You’ve already used this pattern: a ThreadPoolExecutor’s threads are consumers taking tasks from a BlockingQueue.

Common questions

How do put() and take() work on a BlockingQueue?

put() waits while the queue is full, and take() waits while it is empty. That keeps a fast producer at the consumer's speed, which is called backpressure.

ArrayBlockingQueue or LinkedBlockingQueue?

ArrayBlockingQueue(n) always has a limit, so put() can wait. new LinkedBlockingQueue<>() has no limit unless you pass one, so put() never waits and a fast producer fills memory.

How do I stop a consumer thread cleanly?

A common pattern is a poison pill: the producer puts a special marker last, and the consumer exits when it takes it.

Verified: this timeline matches a real program, checked in CI on JDK 21 and 25. How we verify. Unclear or wrong? Tell us