Inter-thread communication: wait() and notify()

Locks stop threads from clashing. But often one thread must wait for another: the waiter waits for the chef, the order waits for payment, the dispatcher waits for messages. Checking a flag in a loop wastes a whole CPU core. Java's answer is wait() and notify(). You'll see exactly how they release and re-acquire a lock, write the classic producer–consumer, and then meet BlockingQueue, which does it all for you.

Module 12 · Multithreading⏱ 25 minIntermediate9 practice problems
1 The story

The waiter who kept asking

In Riya's canteen simulation, the waiter thread checks the counter in a loop: “Is the dosa ready? Is it ready now? Now?” Millions of times a second. Her laptop fan starts screaming, and the chef thread runs slower because the waiter hogs the CPU.

Busy waiting waiter counter: empty ready?ready?ready? CPU 100%, doing nothing useful wait() and notify() waiterresting chefdosa ready! notify() CPU idle until there's real work myphatshala.in
Riya

Sir, how can the waiter wait without checking all the time?

Arjun Sir

He goes to sleep on the counter's lock with wait(). When the chef puts a dish, the chef rings the bell: notify(). The waiter wakes only when something changed.

Riya

But the counter is locked with synchronized. If the waiter sleeps inside, how does the chef get in?

Arjun Sir

That's the clever part: wait() gives up the lock while it sleeps, and takes it back before it returns. Let's watch it inside the monitor.

2 Definition

What is inter-thread communication?

Formal definition (write this in exams)

Inter-thread communication is the coordination of threads so that one thread waits until a condition becomes true and another thread signals when it changes. In Java it is done with the Object methods wait(), notify() and notifyAll(), which must be called while holding the object's monitor. wait() releases the monitor and suspends the thread in the object's wait set until it is notified, interrupted or times out; it then re-acquires the monitor before returning. notify() wakes one thread from the wait set, notifyAll() wakes all of them.

In simple words

wait() = “wake me when something changes” (and let others use the room meanwhile). notifyAll() = “something changed, everyone check again”.

Words you'll meet in this lesson

ConditionWhat a thread is waiting for: “counter not empty”, “payment confirmed”.
Wait setThreads that called wait() on this object (state WAITING).
Entry setThreads trying to get this object's lock (state BLOCKED).
Producer–consumerOne thread makes items, another uses them, through a shared buffer.
Spurious wakeupWaking up without a notify. Rare, but allowed, hence the loop.
BlockingQueueA thread-safe queue whose put/take wait for space/items.

wait() vs sleep()

obj.wait()Thread.sleep(ms)
Defined inObjectThread (static)
Must hold the lock?Yes, of objNo
Releases the lock?YesNo: keeps any locks held
Wakes up whennotified, interrupted, or timeouttime is up, or interrupted
PurposeWait for a conditionPause for a time

Exam tip: three rules to write in every answer: (1) call wait/notify inside synchronized on the same object, else IllegalMonitorStateException; (2) wait releases the lock, sleep doesn't; (3) always call wait in a while loop that re-checks the condition.

3 Types and ways to use it

The pattern, and the tools built on it

The one pattern to memorise

synchronized (lock) {
    while (!condition) {      // ALWAYS a loop, never just "if"
        lock.wait();          // releases lock, waits, re-acquires before returning
    }
    // condition is true and we hold the lock: act on it
}

synchronized (lock) {
    changeTheCondition();
    lock.notifyAll();         // wake the waiters so they re-check
}

Forget the lock and Java stops you

public class Main {
    public static void main(String[] args) {
        Object lock = new Object();
        try {
            lock.notify();
        } catch (IllegalMonitorStateException e) {
            System.out.println(e);
        }
    }
}
java.lang.IllegalMonitorStateException: current thread is not owner

Ways to make threads wait for each other

wait / notifyAll

The low-level tool every Java object supports. Know it for exams and interviews.

built in

BlockingQueue

ArrayBlockingQueue, LinkedBlockingQueue: producer–consumer ready-made.

use in real code

CountDownLatch

Wait until N events have happened (“all 3 services started”).

one-time gate

join()

Wait until a thread has finished: the simplest coordination of all (Lesson 12.3).

wait for the end

Notify without a condition is a lost signal. If notify() runs before the other thread calls wait(), nobody hears it and the waiter may sleep forever. Always pair the signal with a shared variable (a flag, a count, a buffer) that the waiter checks first.

4 Step by step

One wait() and one notify(), in order

  1. The consumer takes the lock and checks

    Inside synchronized take() the waiter sees dish == null. It can't proceed.
  2. wait(): release and sleep

    The JVM releases the monitor and moves the thread to the object's wait set (WAITING). The lock is now free for others.
  3. The producer changes the state

    The chef enters synchronized put(), sets dish and calls notifyAll(). The waiter moves from the wait set to the entry set (BLOCKED): it still needs the lock.
  4. The producer leaves

    When put() returns, the monitor is released. The waiter acquires it and wait() finally returns.
  5. Re-check, then act

    The while loop checks again (another consumer might have taken the dish first). The condition is true, so the waiter takes the dish and notifies in turn.
5 See it happen

Chef, waiter and the monitor: both views

Left: the kitchen as you'd describe it. Right: inside the counter object's monitor, with its owner, entry set and wait set.

wait() and notify(), both views 6 steps
What you see counter (1 slot) dosa dosa idli vada empty empty chef waiter empty → wait() WAITING WAITING put dosa notify() took dosa full → wait() WAITING took idli notify() put vada while (counter empty) wait(); counter = dish; notify(); dish = counter; counter = null; while (counter full) wait(); take; notify(); // wake the chef chef wakes, re-checks, puts Nothing to serve: the waiter waits. Chef puts a dish and notifies. Waiter wakes and takes it. Counter full: the chef waits. Waiter takes it and notifies. Chef continues. myphatshala.in
Behind the screen monitor of the counter object owner waiter chef waiter chef waiter chef entry set (BLOCKED) waiter chef wait set (WAITING) waiter chef wait(): give up the lock, join the wait set notify(): move one waiter to the entry set lock free: waiter re-acquires it, returns from wait() chef calls wait(): releases the lock notify(): chef moves to the entry set chef gets the lock back, loop re-checks wait/notify must be called while owning this monitor (inside synchronized) Waiter parks in the wait set. Chef notifies: waiter must re-lock. Waiter owns the lock again. Chef parks in the wait set. Waiter notifies the chef. Chef owns the lock again. myphatshala.in
6 Real-world use

Producers and consumers everywhere

Most back-end systems are chains of producers and consumers connected by queues.

Watch it with an example: restaurant orders and delivery riders

A food app's restaurant thread produces orders into a queue that holds at most 2. A rider thread takes them. BlockingQueue does all the waiting and notifying for you.

Orders through a BlockingQueue (capacity 2) 6 steps
Animated example restaurant put() rider take() queue (max 2) #1 #1 #2 #1 #2 #2 #3 #3 WAITING WAITING got #1 got #2 queue full: put() blocks space freed: #3 goes in queue empty: take() blocks Order #1 queued. Order #2 queued: full. #3 must wait for space. Rider takes #1; #3 goes in. Rider takes #2. Rider takes #3; next take waits. myphatshala.in

1. Order and notification pipelines

Checkout produces events; email, SMS and invoice workers consume them from queues at their own pace.

order placed→BlockingQueue→SMS worker

2. Logging

Request threads put log lines into an in-memory queue; one background writer thread takes them and writes to disk, so requests never wait for the disk.

200 request threads→log queue→1 writer

3. Startup and readiness

A server waits until its database pool, caches and configs are ready (a latch) before accepting traffic.

3 services→CountDownLatch(3)→open for traffic

4. Waiting for confirmations

Payment flows, OTP verification and seat holds wait for an external event, always with a timeout so nothing waits forever.

payment sent→wait(timeout) in a loop→ship or release

Simplified. Large systems connect services with message brokers (such as Kafka or RabbitMQ), which apply the same producer–consumer idea between machines.

7 Run the code

Chef and waiter, four dishes

class Counter {
    private String dish;

    synchronized void put(String d) throws InterruptedException {
        while (dish != null) wait();
        dish = d;
        System.out.println("chef put " + d);
        notifyAll();
    }

    synchronized String take() throws InterruptedException {
        while (dish == null) wait();
        String d = dish;
        dish = null;
        System.out.println("waiter took " + d);
        notifyAll();
        return d;
    }
}

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        Thread chef = new Thread(() -> {
            try {
                for (String d : new String[]{"dosa", "idli", "vada", "upma"}) counter.put(d);
            } catch (InterruptedException e) { }
        });
        Thread waiter = new Thread(() -> {
            try {
                for (int i = 0; i < 4; i++) counter.take();
            } catch (InterruptedException e) { }
        });
        chef.start();
        waiter.start();
        chef.join();
        waiter.join();
    }
}
chef put dosa waiter took dosa chef put idli waiter took idli chef put vada waiter took vada chef put upma waiter took upma
  • while (dish != null) wait();The chef waits while the counter is full, releasing the lock.
  • while (dish == null) wait();The waiter waits while it's empty.
  • notifyAll()After every change, wake the other side so it re-checks.
  • printing inside the methodsPrints happen while holding the lock, in the true order of puts and takes, so the output is the same every run.
8 Line by line

main waits for the chef's dish

Recorded from the real JVM, following main, which plays the waiter. Watch it enter take(), find nothing, and wait; when wait() returns, the dish set by the chef thread is in the shared object.

Worked examples: real problems, solved and traced in memory

Three coordination problems from real apps: a one-slot hand-off, waiting for a confirmation, and a message queue. Notice that every wait() sits inside a while loop and a synchronized method.

Example 1Restaurant · kitchen

Chef and waiter with a one-dish counter

Problem statement

A chef cooks 3 dishes and a waiter serves them through a counter that holds one dish. The chef must wait if the counter is full; the waiter must wait if it's empty. Print each put and take.

Input
dishes: dosa, idli, vada
Expected output
chef put dosa
waiter took dosa
chef put idli
waiter took idli
chef put vada
waiter took vada
all dishes served

Approach

  1. A Counter class with synchronized put and take.
  2. put waits while full; take waits while empty. Each calls notifyAll() after changing the counter.
  3. Print inside the synchronized methods so the order of lines is always put, take, put, take…

Solution: run it live

What happens in memory, step by step

Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.

Example 2Payments · confirmation

Wait until the gateway confirms

Problem statement

After a customer pays, the order thread must not ship until the payment gateway confirms. The gateway runs on its own thread and confirms after a short delay. Use a lock object, a flag, wait() and notifyAll().

Input
payment id PAY-551; gateway answers after 100 ms
Expected output
order: waiting for payment
gateway: PAY-551 confirmed
order: shipping now

Approach

  1. A small Payment class with a confirmed flag.
  2. awaitConfirmation() waits in a loop while the flag is false.
  3. confirm() sets the flag and calls notifyAll().

Solution: run it live

What happens in memory, step by step

Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.

Example 3Messaging · SMS

An SMS dispatcher with a BlockingQueue

Problem statement

A notification service queues SMS messages; a dispatcher thread sends them one by one. To shut down, the producer puts a special STOP message (a “poison pill”). Use ArrayBlockingQueue instead of writing wait/notify yourself.

Input
messages: OTP 4821, Order shipped, Refund done, then STOP
Expected output
sent: OTP 4821
sent: Order shipped
sent: Refund done
dispatcher stopped

Approach

  1. Create an ArrayBlockingQueue<String> with capacity 2.
  2. The dispatcher loops on take() and stops when it receives STOP.
  3. main puts the messages and then STOP, and joins the dispatcher.

Solution: run it live

What happens in memory, step by step

Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.

9 Quiz and revision

Check yourself

Quick revision

Lesson 12.5 in one screen

  • wait/notify/notifyAll are Object methods; call them while holding that object's lock.
  • Otherwise: IllegalMonitorStateException: current thread is not owner.
  • wait() releases the lock, waits in the wait set, and re-acquires the lock before returning.
  • Always while (!condition) wait();: never if.
  • Signal with a shared condition variable, then notifyAll(); a bare notify can be lost.
  • wait() → WAITING; wait(ms) → TIMED_WAITING; both throw InterruptedException.
  • In real code prefer BlockingQueue, CountDownLatch and friends.

Interview and viva questions

Why are wait() and notify() in Object and not in Thread?

Because threads wait on a condition associated with a lock, and every object can be a lock (monitor). The method is called on the object whose monitor you hold, not on a thread.

Why must wait() be called from a synchronized context?

The check of the condition and the decision to wait must be atomic with respect to the thread that changes it; otherwise a notify could slip in between and be lost. Requiring the lock enforces this, and the JVM throws IllegalMonitorStateException if you don't hold it.

Why use while instead of if around wait()?

After waking, the condition may no longer be true: another thread may have consumed the item, notifyAll may have woken threads waiting for different conditions, or a spurious wakeup may occur. The loop re-checks before acting.

Difference between notify() and notifyAll()?

notify() wakes one arbitrary thread from the wait set; notifyAll() wakes them all, and each competes for the lock and re-checks. notify is a small optimisation that is only safe when all waiters wait for the same condition and any one of them can handle it.

How would you implement producer–consumer?

With a bounded buffer whose put waits while full and take waits while empty, both synchronized, each calling notifyAll() after changing the buffer. In production, use ArrayBlockingQueue or LinkedBlockingQueue, which provide exactly this.

Practice problems

Coordinate threads with wait/notifyAll, then with the ready-made tools in java.util.concurrent. Outputs are fixed because every print happens in a guaranteed order. The last three are business tasks.

P1Easy

wait() outside synchronized

Call wait() on an object without holding its lock, catch the exception and print it.

Input
(none)
Expected output
error: current thread is not owner
Show solution and explanation
public class Main {
    public static void main(String[] args) throws InterruptedException {
        Object lock = new Object();
        try {
            lock.wait();
        } catch (IllegalMonitorStateException e) {
            System.out.println("error: " + e.getMessage());
        }
    }
}
error: current thread is not owner

How it works: wait, notify and notifyAll must be called by the thread that owns the object's monitor, i.e. inside synchronized (lock). Otherwise the JVM throws IllegalMonitorStateException.

P2Easy

Wake a waiting thread

A thread waits until ready becomes true. main sets it and calls notifyAll(). The thread then prints got the signal.

Input
(none)
Expected output
got the signal
Show solution and explanation
public class Main {
    static boolean ready = false;
    static final Object LOCK = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            synchronized (LOCK) {
                while (!ready) {
                    try { LOCK.wait(); } catch (InterruptedException e) { return; }
                }
                System.out.println("got the signal");
            }
        });
        t.start();
        synchronized (LOCK) {
            ready = true;
            LOCK.notifyAll();
        }
        t.join();
    }
}
got the signal

How it works: the flag makes it safe even if main signals before the thread starts waiting: the thread checks ready first and doesn't wait at all. A notify with no flag would be lost.

P3Easy

A bounded queue refuses extras

Create an ArrayBlockingQueue of capacity 2, offer three items, and print the results and the queue.

Input
a, b, c
Expected output
true true false
[a, b]
Show solution and explanation
import java.util.concurrent.ArrayBlockingQueue;

public class Main {
    public static void main(String[] args) {
        ArrayBlockingQueue<String> q = new ArrayBlockingQueue<>(2);
        System.out.println(q.offer("a") + " " + q.offer("b") + " " + q.offer("c"));
        System.out.println(q);
    }
}
true true false [a, b]

How it works: offer returns false at once when full; put would wait instead; add would throw. Pick the method that matches what should happen when the system is overloaded.

P4Medium

Ping-pong in turns

Two threads print ping and pong alternately, 3 times each, starting with ping. Use a shared turn flag with wait/notifyAll.

Input
(none)
Expected output
ping
pong
ping
pong
ping
pong
Show solution and explanation
public class Main {
    static boolean pingTurn = true;
    static final Object LOCK = new Object();

    static void play(String word, boolean mine, int times) {
        for (int i = 0; i < times; i++) {
            synchronized (LOCK) {
                while (pingTurn != mine) {
                    try { LOCK.wait(); } catch (InterruptedException e) { return; }
                }
                System.out.println(word);
                pingTurn = !pingTurn;
                LOCK.notifyAll();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread ping = new Thread(() -> play("ping", true, 3));
        Thread pong = new Thread(() -> play("pong", false, 3));
        pong.start();
        ping.start();
        ping.join();
        pong.join();
    }
}
ping pong ping pong ping pong

How it works: each thread waits until it's its turn, prints, flips the flag and wakes the other. Even though pong starts first, it waits for ping.

P5Medium

notifyAll wakes everyone

Three threads wait for a gateOpen flag. main opens the gate with one notifyAll(). Count how many threads passed.

Input
3 waiting threads
Expected output
fans through the gate: 3
Show solution and explanation
import java.util.concurrent.atomic.AtomicInteger;

public class Main {
    static boolean gateOpen = false;
    static final Object GATE = new Object();

    public static void main(String[] args) throws InterruptedException {
        AtomicInteger passed = new AtomicInteger();
        Thread[] fans = new Thread[3];
        for (int i = 0; i < 3; i++) {
            fans[i] = new Thread(() -> {
                synchronized (GATE) {
                    while (!gateOpen) {
                        try { GATE.wait(); } catch (InterruptedException e) { return; }
                    }
                }
                passed.incrementAndGet();
            });
            fans[i].start();
        }
        synchronized (GATE) {
            gateOpen = true;
            GATE.notifyAll();
        }
        for (Thread f : fans) f.join();
        System.out.println("fans through the gate: " + passed.get());
    }
}
fans through the gate: 3

How it works: notify() wakes only one waiting thread; notifyAll() wakes all, and each re-checks the condition. When several threads may be waiting, notifyAll is the safe default.

P6Medium

Wait for three services with CountDownLatch

An app starts only when the database, cache and payment services are ready. Each starts on its own thread and counts down a latch. main waits on the latch.

Input
3 services
Expected output
all 3 services ready, latch = 0
Show solution and explanation
import java.util.concurrent.CountDownLatch;

public class Main {
    public static void main(String[] args) throws InterruptedException {
        String[] services = {"database", "cache", "payments"};
        CountDownLatch ready = new CountDownLatch(services.length);
        for (String s : services) {
            new Thread(() -> ready.countDown(), s).start();
        }
        ready.await();
        System.out.println("all " + services.length + " services ready, latch = " + ready.getCount());
    }
}
all 3 services ready, latch = 0

How it works: await() waits until the count reaches 0. It's a ready-made “wait until N things have happened”, built on the same waiting machinery as wait/notify.

P7Business problem

Odd and even invoice numbers

Two printers share the invoice numbers 1–10: one prints odd numbers, the other even, strictly in order. Use wait/notifyAll on a shared counter.

Input
1..10
Expected output
printer-A: INV-1
printer-B: INV-2
printer-A: INV-3
printer-B: INV-4
printer-A: INV-5
printer-B: INV-6
printer-A: INV-7
printer-B: INV-8
printer-A: INV-9
printer-B: INV-10
Show solution and explanation
public class Main {
    static int next = 1;
    static final Object LOCK = new Object();

    static void printer(String name, int parity) {
        while (true) {
            synchronized (LOCK) {
                while (next <= 10 && next % 2 != parity) {
                    try { LOCK.wait(); } catch (InterruptedException e) { return; }
                }
                if (next > 10) {
                    LOCK.notifyAll();
                    return;
                }
                System.out.println(name + ": INV-" + next);
                next++;
                LOCK.notifyAll();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread odd = new Thread(() -> printer("printer-A", 1));
        Thread even = new Thread(() -> printer("printer-B", 0));
        odd.start();
        even.start();
        odd.join();
        even.join();
    }
}
printer-A: INV-1 printer-B: INV-2 printer-A: INV-3 printer-B: INV-4 printer-A: INV-5 printer-B: INV-6 printer-A: INV-7 printer-B: INV-8 printer-A: INV-9 printer-B: INV-10

How it works: each printer waits until the next number has its parity. The next > 10 check lets both threads finish; without the final notifyAll one of them could wait forever.

P8Business problem

Order pipeline with a BlockingQueue

A restaurant thread produces 5 orders with ids 101–105 into a queue of capacity 2. A rider thread takes exactly 5 orders and adds up their bill amounts (id × 2). Print the total.

Input
orders 101..105
Expected output
delivered 5 orders, bills total Rs 1030
Show solution and explanation
import java.util.concurrent.*;

public class Main {
    public static void main(String[] args) throws InterruptedException {
        BlockingQueue<Integer> orders = new ArrayBlockingQueue<>(2);
        int[] total = {0};
        Thread restaurant = new Thread(() -> {
            try {
                for (int id = 101; id <= 105; id++) orders.put(id);
            } catch (InterruptedException e) { }
        });
        Thread rider = new Thread(() -> {
            try {
                for (int i = 0; i < 5; i++) total[0] += orders.take() * 2;
            } catch (InterruptedException e) { }
        });
        restaurant.start();
        rider.start();
        restaurant.join();
        rider.join();
        System.out.println("delivered 5 orders, bills total Rs " + total[0]);
    }
}
delivered 5 orders, bills total Rs 1030

How it works: the queue's capacity limits how far the restaurant can get ahead of the rider. total is written only by the rider and read after join(), so no lock is needed for it.

P9Business problem

Seat hold that expires

A cinema holds a seat while the customer pays, but for at most 200 ms. The payment never arrives. Use wait(timeout) in a loop with a deadline, then release the seat.

Input
hold 200 ms, no payment
Expected output
hold expired, seat released
Show solution and explanation
public class Main {
    static boolean paid = false;
    static final Object LOCK = new Object();

    public static void main(String[] args) throws InterruptedException {
        long deadline = System.currentTimeMillis() + 200;
        synchronized (LOCK) {
            long left;
            while (!paid && (left = deadline - System.currentTimeMillis()) > 0) {
                LOCK.wait(left);
            }
            System.out.println(paid ? "seat confirmed" : "hold expired, seat released");
        }
    }
}
hold expired, seat released

How it works: wait(ms) returns on notify, on timeout, or spuriously, so the loop recomputes the time left and re-checks the flag. This is the correct pattern for “wait for a condition, but not forever”.

Real-world scenario questions (asked in company interviews)

Coordination bugs freeze systems silently, so interviewers like to see you reason about who waits for whom.

Code review · CPUFresher

The worker that eats a CPU core

A report worker waits for jobs with while (jobs.isEmpty()) { }. The server shows one core at 100% even when there are no jobs. How would you rewrite it?

The interviewer is checking: busy waiting vs blocking.

Show a strong answer
  • The empty loop spins constantly. Replace the list with a BlockingQueue and call take(), which waits without using CPU.
  • If a plain list must stay, guard it with a lock and use while (jobs.isEmpty()) lock.wait(); with notifyAll() when adding.

Avoid: Thread.sleep(10) in the loop as the solution: it still wastes wakeups and adds latency.

Production · hangJunior

Occasionally, the consumer never wakes up

Once in a few thousand runs, a consumer thread stays WAITING forever although an item was added. The producer does buffer.add(x); synchronized (lock) { lock.notify(); }, and the consumer does synchronized (lock) { lock.wait(); } item = buffer.poll();. What's wrong?

The interviewer is checking: lost notifications and the guarded-wait pattern.

Show a strong answer
  • The consumer waits without checking a condition. If the notify happens before the consumer reaches wait(), it is lost.
  • Fix: update the buffer and check it under the same lock: synchronized (lock) { while (buffer.isEmpty()) lock.wait(); item = buffer.poll(); }, and the producer adds and notifies inside the lock.
  • Or replace it all with a BlockingQueue.

Avoid: adding a sleep before wait to “make the timing work”.

Backend · memoryJunior

OutOfMemoryError during a sale

Order events go into an unbounded LinkedBlockingQueue consumed by an email worker. During a sale, producers add 5,000 events/s, the worker sends 300 emails/s, and after an hour the JVM runs out of memory. Fix the design.

The interviewer is checking: back-pressure and bounded queues.

Show a strong answer
  • The queue grows without limit because consumers are slower than producers.
  • Use a bounded queue (new ArrayBlockingQueue<>(10_000)) so producers slow down, or use offer with a timeout and handle rejection (store for later, drop low-priority events).
  • Add more consumer threads if the email provider allows it, and monitor queue length.

Avoid: just increasing the heap; it only delays the crash.

Design · shutdownMid

Stopping consumers cleanly

Four consumer threads loop on queue.take(). At shutdown the app must finish processing everything already queued, then stop all four. How?

The interviewer is checking: poison pills and interruption.

Show a strong answer
  • After the last real item, put one “poison pill” per consumer (4 STOP messages). Each consumer exits when it takes one, so all real items are processed first.
  • Alternatively stop producers, wait until the queue is empty, then interrupt the consumers; take() throws InterruptedException and they exit.
  • With an ExecutorService, use shutdown() + awaitTermination() (Lesson 12.6).

Avoid: daemon consumers that are killed with work still in the queue.