put blocks when the queue is full and take blocks when it is empty, so a bounded queue applies back-pressure without any manual locking.
var queue = new ArrayBlockingQueue<Integer>(2);
var results = Collections.synchronizedList(new ArrayList<Integer>());
Thread producer = new Thread(() -> {
try {
for (int i = 1; i <= 5; i++) queue.put(i);
queue.put(-1); // poison pill
} catch (InterruptedException e) { Thread.currentThread().interrupt(); }
});
Thread consumer = new Thread(() -> {
try {
int v;
while ((v = queue.take()) != -1) results.add(v);
} catch (InterruptedException e) { Thread.currentThread().interrupt(); }
});
producer.start(); consumer.start();
producer.join(); consumer.join();
System.out.println("consumed in order: " + results);
consumed in order: [1, 2, 3, 4, 5]
Run this yourself in the Online Java Compiler, spin up a live REST API in the API Sandbox, or practise with Java interview questions.
Published 2026-07-30