Netease Interview: Write a BlockingQueue in 5 Minutes and Explain Design Patterns & Principles
The article breaks down a common Netease interview task—hand‑coding a BlockingQueue in five minutes—by explaining the underlying lock‑and‑condition mechanism, the relevant design patterns and principles, common pitfalls such as spurious wakeups, and provides a concise, 50‑line Java implementation with step‑by‑step commentary.
Interview Context and Assessment Layers
A candidate was asked by Netease to hand‑write a blocking queue within five minutes and discuss the associated design patterns and principles. The interview evaluates three abilities: basic concurrency fundamentals, design‑thinking (patterns and principles), and the ability to articulate the solution under pressure.
Why a BlockingQueue?
In producer‑consumer scenarios, mismatched speeds cause two problems: producers may overflow the buffer, or consumers may starve. A plain queue would throw exceptions on overflow or return null on underflow, and concurrent access would corrupt data. The essence of a BlockingQueue is the “lock + condition variable” collaboration that makes threads wait when resources are unavailable and wake them when resources become ready.
Core Java API
Common implementations in java.util.concurrent are ArrayBlockingQueue and LinkedBlockingQueue, both adhering to the BlockingQueue<E> contract. The four method groups are:
Blocking insert/remove : put(E e), take() – wait indefinitely.
Timed insert/remove : offer(E e, long timeout, TimeUnit unit), poll(long timeout, TimeUnit unit) – wait with timeout.
Non‑blocking operations : offer(E e), poll(), peek() – immediate result.
Capacity query : remainingCapacity().
Typical usage boils down to the two core methods:
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class Demo {
public static void main(String[] args) throws InterruptedException {
BlockingQueue<String> queue = new ArrayBlockingQueue<>(2); // capacity 2
queue.put("task1");
queue.put("task2"); // queue full
new Thread(() -> {
try { Thread.sleep(1000); queue.take(); } catch (InterruptedException e) {}
}).start();
queue.put("task3"); // blocks 1 s until take() frees a slot
System.out.println(queue.take()); // prints task1
}
}Underlying Mechanism
The implementation relies on three indispensable components:
ReentrantLock – guarantees exclusive access to the queue state.
Two Condition variables – notEmpty (consumer wait) and notFull (producer wait) enable precise thread notification.
Underlying container – an array for ArrayBlockingQueue or a linked list for LinkedBlockingQueue.
Producer put() flow: lock → check fullness → if full, notFull.await() (releases lock) → enqueue → notEmpty.signal() → unlock. Consumer take() flow mirrors this with notEmpty and notFull.
Source‑Level Insight (ArrayBlockingQueue)
public class ArrayBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable {
int takeIndex; // next element to remove
int putIndex; // next slot to insert
int count; // current size
private final Condition notEmpty; // wait for non‑empty
private final Condition notFull; // wait for non‑full
public ArrayBlockingQueue(int capacity, boolean fair) {
this.items = new Object[capacity];
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
}The class follows the Single‑Responsibility Principle: the lock handles synchronization, the conditions handle waiting, and the array handles storage.
public void put(E e) throws InterruptedException {
Objects.requireNonNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == items.length) // use while, not if
notFull.await();
enqueue(e);
} finally {
lock.unlock();
}
}
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)
notEmpty.await();
return dequeue();
} finally {
lock.unlock();
}
}Key interview points: use while to guard against spurious wake‑ups, acquire the lock interruptibly, and always release the lock in a finally block.
Design‑Pattern & Principle Highlights
**Minimal Privilege** – lock only the critical section; signal() wakes a single waiting thread.
**Fail‑Wait** – instead of throwing on full/empty, the queue blocks, matching real‑world workflow.
**Interruptible Lock** – lockInterruptibly() lets a thread respond to cancellation.
**Single‑Responsibility** – separation of synchronization, waiting, and storage.
Why ReentrantLock + Condition Beats synchronized ?
Three advantages:
Multiple wait‑sets: two conditions allow targeted wake‑ups; synchronized has a single wait‑set, making notify() nondeterministic and notifyAll() costly.
Interruptible acquisition: lockInterruptibly() can be aborted, whereas a thread stuck in a synchronized block cannot be interrupted.
Fairness option: ReentrantLock can be constructed as fair, preventing thread starvation; synchronized is always non‑fair.
Minimal Hand‑Written Implementation for the Interview
The essential skeleton consists of four fields and two core methods:
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class SimpleBlockingQueue<T> {
private final int capacity;
private final Queue<T> queue;
private final ReentrantLock lock;
private final Condition notEmpty;
private final Condition notFull;
public SimpleBlockingQueue(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException("capacity must be > 0");
this.capacity = capacity;
this.queue = new LinkedList<>();
this.lock = new ReentrantLock();
this.notEmpty = lock.newCondition();
this.notFull = lock.newCondition();
}
public void put(T t) throws InterruptedException {
lock.lockInterruptibly();
try {
while (queue.size() == capacity) {
notFull.await(); // block producer
}
queue.offer(t);
notEmpty.signal(); // wake a consumer
} finally {
lock.unlock();
}
}
public T take() throws InterruptedException {
lock.lockInterruptibly();
try {
while (queue.isEmpty()) {
notEmpty.await(); // block consumer
}
T res = queue.poll();
notFull.signal(); // wake a producer
return res;
} finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
SimpleBlockingQueue<Integer> q = new SimpleBlockingQueue<>(2);
new Thread(() -> {
try {
q.put(1);
q.put(2);
System.out.println("producer: queue full, waiting");
q.put(3); // blocks until consumer takes
System.out.println("producer: inserted 3");
} catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}).start();
Thread.sleep(1000);
new Thread(() -> {
try {
System.out.println("consumer: took " + q.take());
} catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}).start();
}
}Interview‑ready talking points:
Explain the “lock + two conditions” collaboration.
Emphasize the while guard against spurious wake‑ups.
Highlight lockInterruptibly() for responsive cancellation.
Stress that finally guarantees lock release.
Justify using signal() instead of signalAll() for efficiency.
Conclusion
Mastering this problem demonstrates three competencies required by the interview: solid concurrency fundamentals, the ability to map code to design patterns and principles, and clear communication under time pressure. Writing the concise version above hits all evaluation criteria and maximizes the chance of a successful interview.
Tech Freedom Circle
Crazy Maker Circle (Tech Freedom Architecture Circle): a community of tech enthusiasts, experts, and high‑performance fans. Many top‑level masters, architects, and hobbyists have achieved tech freedom; another wave of go‑getters are hustling hard toward tech freedom.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
