Monitor Object — Practice Tasks¶
Ten graded, build-it-yourself tasks for the Monitor Object pattern. Each gives a goal, requirements, hints, and a solution sketch. Work in Java (intrinsic lock or
ReentrantLock+Condition); add C++ where noted. See junior · middle.
Table of Contents¶
- Task 1 — Thread-Safe Counter
- Task 2 — Bounded Buffer (intrinsic lock)
- Task 3 — Bounded Buffer (two conditions)
- Task 4 — Timed
poll/offer - Task 5 — CountDownLatch from scratch
- Task 6 — Bounded Semaphore
- Task 7 — Connection Pool
- Task 8 — Read/Write Gate (single writer)
- Task 9 — One-Shot Future / Promise
- Task 10 — Reproduce and Fix Nested Monitor Lockout
- How to Practice
Task 1 — Thread-Safe Counter¶
Goal: Build a counter safe for concurrent increment/get and prove visibility.
Requirements: increment(), decrement(), get(), all correct under N threads doing M increments each so the final value equals N×M.
Hints: Synchronize the reader too — not just the writer. Why? Visibility under the JMM.
Solution sketch
Test: 8 threads × 1,000,000 increments; assert `get() == 8_000_000`. Remove `synchronized` from `get()` and observe stale reads under contention. The lock gives both atomicity (`value++` is read-modify-write) and visibility.Task 2 — Bounded Buffer (intrinsic lock)¶
Goal: Implement the canonical Monitor example with synchronized + wait/notifyAll.
Requirements: put(x) blocks when full, take() blocks when empty; FIFO order; no busy-waiting.
Hints: while (full) wait(); and while (empty) wait();. Use notifyAll() (single condition, mixed waiters). Null out slots on removal to avoid leaks.
Solution sketch
public synchronized void put(T x) throws InterruptedException {
while (count == items.length) wait();
items[tail] = x; tail = (tail + 1) % items.length; count++;
notifyAll();
}
public synchronized T take() throws InterruptedException {
while (count == 0) wait();
T x = (T) items[head]; items[head] = null;
head = (head + 1) % items.length; count--;
notifyAll();
return x;
}
Task 3 — Bounded Buffer (two conditions)¶
Goal: Refactor Task 2 to ReentrantLock + notFull/notEmpty and replace notifyAll with signal.
Requirements: signal() (not signalAll()); lock() outside try, unlock() in finally.
Hints: Producers await on notFull and signal notEmpty; consumers the mirror. Argue why signal is now safe (homogeneous waiters per condition).
Solution sketch
Safety argument: only producers wait on `notFull`, only consumers on `notEmpty`, so a single `signal` always wakes a thread that can proceed — no wrong-thread wakeup, no thundering herd.Task 4 — Timed poll / offer¶
Goal: Add deadline-aware variants that give up after a timeout.
Requirements: poll(timeout, unit) returns null on timeout; offer(x, timeout, unit) returns false. Deadline must survive spurious wakeups.
Hints: Use awaitNanos, which returns the remaining nanos; subtract across loop iterations.
Solution sketch
public T poll(long timeout, TimeUnit unit) throws InterruptedException {
long nanos = unit.toNanos(timeout);
lock.lock();
try {
while (count == 0) {
if (nanos <= 0L) return null;
nanos = notEmpty.awaitNanos(nanos); // remaining time
}
// dequeue...
notFull.signal();
return x;
} finally { lock.unlock(); }
}
Task 5 — CountDownLatch from scratch¶
Goal: Implement a one-shot latch: workers await() until count reaches zero.
Requirements: await() blocks until count == 0; countDown() decrements; once open, stays open (all future await() return immediately).
Hints: Single condition "count == 0". countDown() signals only when it hits zero. await() uses while (count > 0) wait();.
Solution sketch
`notifyAll` is correct: all waiters become unblocked simultaneously when the gate opens.Task 6 — Bounded Semaphore¶
Goal: Implement a counting semaphore with N permits.
Requirements: acquire() blocks when no permits; release() returns one; never exceed max permits.
Hints: Condition "permits > 0". release signals one waiter. Make acquire interruptible.
Solution sketch
public final class Sem {
private final Lock lock = new ReentrantLock();
private final Condition avail = lock.newCondition();
private int permits;
public Sem(int p) { permits = p; }
public void acquire() throws InterruptedException {
lock.lock();
try { while (permits == 0) avail.await(); permits--; }
finally { lock.unlock(); }
}
public void release() {
lock.lock();
try { permits++; avail.signal(); }
finally { lock.unlock(); }
}
}
Task 7 — Connection Pool¶
Goal: A fixed-size pool that blocks borrowers when exhausted.
Requirements: borrow(timeout) returns a connection or times out; release(conn) returns it; never hand out the same connection twice.
Hints: Hold idle connections in a deque; condition "pool not empty". Combine with Task 4's timed wait. Always release in a finally at the call site.
Solution sketch
Maintain `DequeTask 8 — Read/Write Gate (single writer)¶
Goal: Allow many readers OR one writer, never both — a hand-rolled read/write lock as a Monitor.
Requirements: readLock/readUnlock, writeLock/writeUnlock. Writers wait for active readers to drain; readers wait while a writer holds or is queued (writer preference to avoid starvation).
Hints: Track activeReaders, writerActive, waitingWriters. Conditions: "can read", "can write". This is genuinely tricky — get starvation policy explicit.
Solution sketch
`readLock`: `while (writerActive || waitingWriters > 0) okToRead.await(); activeReaders++;` `writeLock`: `waitingWriters++; while (activeReaders > 0 || writerActive) okToWrite.await(); waitingWriters--; writerActive = true;` On unlock, signal the appropriate condition(s). Then compare against `ReentrantReadWriteLock` and explain why you'd almost always use the JDK's.Task 9 — One-Shot Future / Promise¶
Goal: A set(value) once, get() blocks until set.
Requirements: get() blocks until a value (or exception) is available; multiple getters all unblock; set is idempotent-or-throws.
Hints: Condition "isDone". Store either a value or a throwable. notifyAll on completion.
Solution sketch
This is the Monitor core of `CompletableFuture`'s blocking `get()`.Task 10 — Reproduce and Fix Nested Monitor Lockout¶
Goal: Deliberately create a nested-monitor deadlock, observe it, then fix it.
Requirements: Two objects A and B; a thread holds A, enters B, and wait()s on B while still holding A; a second thread needs A to change B's condition. Show the hang (timeout test), then fix.
Hints: The fix is to not wait while holding the outer lock — restructure so the wait happens with only one lock held, or pass the needed data without nesting.
Solution sketch
Reproduce: `synchronized(a){ synchronized(b){ while(!ready) b.wait(); } }` while another thread needs `a` to set `ready`. The waiter releases only `b`, keeps `a`, and the setter blocks on `a` → deadlock; a JUnit timeout proves it. Fix: lift the wait out so only `b` is held during `b.wait()`, or merge into a single lock guarding both pieces of state. Add a lock-order assertion to prevent regression.How to Practice¶
- Write the stress test first. For every task, a multi-threaded harness asserting the invariant (count bounds, conservation of items, no double-issue) is worth more than the implementation. Run it for millions of ops.
- Deliberately break it. Change
whiletoif, swapsignalfor the wrong condition, drop reader synchronization — watch the stress test catch it. Bugs you've seen fail you'll recognize in code review. - Pin interleavings with latches. Use
CountDownLatch/CyclicBarrierto force the exact two-thread order that triggers lost-wakeup or nested-lockout, turning flaky bugs into deterministic tests. - Build both versions. Do each task once with
synchronized+wait/notifyAlland once withReentrantLock+Condition; feel where the explicit version's targetedsignalwins. - Then read the JDK. After building Task 3, read
ArrayBlockingQueue; after Task 5, readCountDownLatch(AQS). Your hand-rolled version makes the source legible. - Add a timeout to every blocking test — a hang is the failure signal for deadlock and lost-wakeup bugs.
In this topic