Say It Out Loud

Priya finished the QuickCart multithreading work. Before her first backend interview, her friend Maya offered to do a mock session. “Don’t memorize slides,” Maya said. “Explain it like you’re fixing my shop. Short answers. Clear words.”

They sat with coffee. Maya asked questions. Priya answered. This chapter is that conversation — practice you can read aloud before your own interview.


Basic concepts

Maya: What is a thread? How is it different from a process?

Priya: A process is a whole running program with its own memory — like the whole QuickCart server. A thread is a worker inside that program. Threads in the same process share memory, so they can read the same variables. Threads are lighter than processes. Switching between threads is faster than switching between processes.


Maya: What is the difference between concurrency and parallelism?

Priya: Concurrency means many tasks make progress over time. On one CPU core, threads take turns — it looks simultaneous but only one runs at each instant. Parallelism means tasks actually run at the same time on different cores. QuickCart on a four-core machine can process four orders in parallel.


Maya: What are the different states of a thread?

Priya: Six states in Java:

  1. NEW — created but start() not called yet.
  2. RUNNABLE — running or ready to run.
  3. BLOCKED — waiting to enter a synchronized block.
  4. WAITING — waiting indefinitely (wait(), join() with no timeout).
  5. TIMED_WAITING — waiting for a set time (sleep(), wait(timeout)).
  6. TERMINATED — finished.

When QuickCart hangs, Priya checks thread dumps for threads stuck in BLOCKED or WAITING.


Thread management

Maya: What is the difference between start() and run() on a Thread?

Priya: start() creates a new thread and runs your code there. run() runs the code in the current thread — no new worker, no extra speed. Always call start() when you want multithreading.

thread.start(); // new thread runs run()
thread.run();   // current thread runs run() — no parallelism

Maya: Can you start a thread twice?

Priya: No. Once a thread finishes, it is TERMINATED. Calling start() again throws IllegalThreadStateException. Create a new Thread object if you need to run the task again.


Maya: What is a daemon thread?

Priya: A background thread that does not keep the JVM alive. When only daemon threads remain, the JVM exits. QuickCart’s log writer thread can be a daemon — if the main app shuts down, the JVM does not wait for it forever.

Thread logWriter = new Thread(() -> writeLogs());
logWriter.setDaemon(true);
logWriter.start();

Maya: Why use ExecutorService instead of creating new threads yourself?

Priya: An ExecutorService keeps a pool of reusable threads. It queues extra tasks when all workers are busy. It can return a Future for results. Creating a new Thread for every order is slow and has no limit — you could spawn thousands and crash the server. QuickCart uses a fixed pool of ten order workers instead.


Maya: What is Callable and Future?

Priya: Callable is like Runnable but returns a value and can throw checked exceptions. You pass it to executor.submit(). Future is a handle to a result that is not ready yet. You can call future.get() to wait for the result, get(timeout, unit) to wait with a limit, or isDone() to check without blocking. I use Callable when an order step needs to return a price from an external API.


Maya: What is CompletableFuture? How is it different from Future?

Priya: Future is basic — mostly blocking get() and little composition. CompletableFuture lets you chain steps without blocking: thenApply, thenCompose, allOf, error handlers, timeouts. QuickCart uses it to call payment, shipping, and fraud APIs in parallel and combine the results when all three finish.


Maya: How do you decide thread pool size?

Priya: It depends on the work type:

  • I/O-bound (HTTP, database) — more threads than CPU cores, because threads wait on network. QuickCart’s payment pool runs 20 to 50 threads.
  • CPU-bound (calculations, sorting) — about one thread per CPU core. More threads just fight for cores.
  • Watch queue size and active thread count in production. If the queue always grows, add threads or speed up tasks. If threads sit idle, shrink the pool.

Synchronization

Maya: What is the difference between synchronized and volatile?

Priya: synchronized gives mutual exclusion and visibility — only one thread in the block at a time, and changes are visible to others. volatile gives visibility only — reads and writes go straight to main memory, but it does not make compound operations atomic. count++ is not safe with volatile alone. Use synchronized or AtomicInteger for counters.

Feature synchronized volatile
Visibility yes yes
Atomicity yes no
Mutual exclusion yes no

Maya: Is volatile enough for a counter?

Priya: No. Increment is read-modify-write — three steps. Two threads can both read the same value and both write back the same result. Use AtomicInteger or synchronized instead.

private AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet(); // safe

Maya: What is a race condition?

Priya: Two or more threads access shared data and the outcome depends on who runs first. Classic example: two threads both read stock = 1, both sell one item, both write stock = 0 — but two items were sold when only one was left. The fix is synchronization or atomic operations.


Maya: What is ReentrantLock? Why use it over synchronized?

Priya: Both protect critical sections. ReentrantLock adds tryLock with timeout, lockInterruptibly, and optional fairness (first-come-first-served). synchronized is simpler and the JVM optimizes it well. I use ReentrantLock when I need a timeout so QuickCart does not wait forever on a stuck lock.

Feature synchronized ReentrantLock
Timeout no yes
Interrupt while waiting no yes
Try-lock no yes
Fairness option no yes

Concurrent collections

Maya: What is ConcurrentHashMap? Why not just synchronize a HashMap?

Priya: ConcurrentHashMap is built for many threads. Reads are usually lock-free. Writes lock only part of the map. A fully synchronized HashMap blocks every thread on every operation — fine for low traffic, bad under load. QuickCart’s product cache uses ConcurrentHashMap.


Maya: When would you use CopyOnWriteArrayList?

Priya: Read-heavy lists where writes are rare — like a list of active promo codes that almost never changes but gets read on every request. Reads are fast with no locking. Writes copy the entire array, so they are expensive. Do not use it when writes are frequent.


Maya: What is the difference between BlockingQueue and ConcurrentLinkedQueue?

Priya: BlockingQueue has blocking operations — put() waits when full, take() waits when empty. Good for producer-consumer. ConcurrentLinkedQueue is lock-free and non-blocking — offer() and poll() never wait. It is also unbounded. QuickCart uses BlockingQueue between kitchen and counter so a full queue slows producers down.


Maya: What is an AtomicInteger?

Priya: A thread-safe integer with atomic operations like incrementAndGet(), compareAndSet(), and addAndGet(). No explicit lock from your side. Good for counters, sequence numbers, and simple stats. QuickCart uses one to count orders processed today.


Advanced topics

Maya: What is ThreadLocal?

Priya: Each thread gets its own copy of a value. Thread A’s copy is invisible to Thread B. MDC for logging uses ThreadLocal internally — that is why order IDs do not cross to pool threads automatically. Also useful for non-thread-safe objects like SimpleDateFormat. Clean up ThreadLocal in pools or you leak memory.


Maya: What is deadlock?

Priya: Two or more threads each hold a lock the other needs. Everyone waits forever. QuickCart had it when one thread locked inventory then payment, and another locked payment then inventory. Fix: always lock in the same order, or use tryLock with timeout and back off.


Maya: What are the four conditions for deadlock?

Priya: All four must be true:

  1. Mutual exclusion — only one thread holds a resource.
  2. Hold and wait — holding one lock while waiting for another.
  3. No preemption — locks cannot be taken away by force.
  4. Circular wait — a cycle of threads waiting on each other.

Break any one — usually circular wait with lock ordering, or hold-and-wait with tryLock timeout.


Maya: What is the difference between deadlock and livelock?

Priya: Deadlock — threads are blocked, nothing moves. Livelock — threads are active but make no progress, like two people in a hallway both stepping aside the same way forever. In code: two threads both detect a conflict, both back off, both retry at the same instant, repeat forever.


Maya: What is the Producer-Consumer pattern?

Priya: Producers add work to a shared queue. Consumers take work from the queue. They run at different speeds without crashing each other. QuickCart’s kitchen packs orders (produce) and the counter sends confirmations (consume). BlockingQueue with put() and take() handles waiting and thread safety.


Maya: What is the difference between put() and offer() on a BlockingQueue?

Priya: put() blocks if the queue is full — the producer waits. offer() returns false immediately if full — no wait. Same idea on the consumer side: take() blocks when empty, poll() returns null. I use put/take when workers should wait politely. I use offer when dropping work is acceptable, like overflow log lines.


Maya: What is MDC and why does it break in thread pools?

Priya: MDC stores logging context like order ID per thread. Thread pools reuse different threads, and MDC does not copy automatically because it is ThreadLocal. Fix: MDC.getCopyOfContextMap() before submit, MDC.setContextMap(context) in the worker, MDC.clear() in finally. Or use a custom ThreadPoolTaskExecutor that wraps every task.


Maya: What is a Semaphore?

Priya: A Semaphore limits how many threads can use a resource at once. It has a fixed number of permits. acquire() takes one (waits if none free). release() gives one back. QuickCart uses one to cap concurrent payment API calls at fifty so the gateway is not overwhelmed.

Semaphore paymentSlots = new Semaphore(50);
paymentSlots.acquire(); // wait for a slot
try {
    paymentClient.charge(orderId);
} finally {
    paymentSlots.release();
}

Maya: What is CountDownLatch?

Priya: A one-time gate. You set a count. Threads call countDown() when they finish. One waiting thread calls await() and stays blocked until the count hits zero. I used it in a load test — start two hundred threads at the exact same moment, then wait for all to finish before checking results.

CountDownLatch startGate = new CountDownLatch(1);
CountDownLatch doneGate = new CountDownLatch(200);

for (int i = 0; i < 200; i++) {
    executor.submit(() -> {
        startGate.await();  // all threads wait here
        processOrder();
        doneGate.countDown();
    });
}
startGate.countDown(); // all 200 start at once
doneGate.await();      // wait for all to finish

Maya: When should you use parallel streams?

Priya: Large datasets (thousands of items), CPU-heavy work, and operations that do not depend on order. QuickCart sums fifty thousand order totals with a parallel stream. I do not use them for small lists, I/O calls, or when order matters — the overhead is not worth it.


Maya: What is reactive programming?

Priya: A style where you start async work and react when results arrive instead of blocking threads on I/O. QuickCart calls payment, shipping, and fraud APIs in parallel with CompletableFuture. The thread does not sit idle waiting for each network call. Same idea as Mutiny Uni in Quarkus — different API, same mindset.


Maya: What is backpressure?

Priya: When producers are faster than consumers, something has to slow the producers down. A full BlockingQueue creates backpressure — the kitchen blocks on put() until the counter catches up. Without backpressure, memory fills up with work nobody is processing.


Problem-solving

Maya: How would you make a thread-safe counter?

Priya: Three common ways:

// Option 1: AtomicInteger — simplest
private AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet();

// Option 2: synchronized
private int count = 0;
public synchronized void increment() { count++; }

// Option 3: ReentrantLock — when you need tryLock
private final ReentrantLock lock = new ReentrantLock();
public void increment() {
    lock.lock();
    try { count++; } finally { lock.unlock(); }
}

For QuickCart’s order counter, AtomicInteger is my first choice.


Maya: How would you implement a connection pool?

Priya: A Semaphore limits how many connections are in use. A queue holds idle connections:

public class PaymentConnectionPool {
    private final Semaphore semaphore;
    private final Queue<Connection> connections;

    public PaymentConnectionPool(int maxConnections) {
        this.semaphore = new Semaphore(maxConnections);
        this.connections = new ConcurrentLinkedQueue<>();
    }

    public Connection acquire() throws InterruptedException {
        semaphore.acquire();
        Connection conn = connections.poll();
        return conn != null ? conn : createNewConnection();
    }

    public void release(Connection conn) {
        connections.offer(conn);
        semaphore.release();
    }
}

Maya: How would you process a large list in parallel?

Priya: Three options depending on the situation:

// Option 1: Parallel stream — simple for CPU work
List<Result> results = orders.parallelStream()
    .map(this::calculateTotal)
    .collect(Collectors.toList());

// Option 2: ExecutorService — more control
List<Future<Result>> futures = new ArrayList<>();
for (Order order : orders) {
    futures.add(executor.submit(() -> calculateTotal(order)));
}

// Option 3: CompletableFuture — best for composing async steps
List<CompletableFuture<Result>> futures = orders.stream()
    .map(o -> CompletableFuture.supplyAsync(() -> calculateTotal(o), executor))
    .collect(Collectors.toList());

Maya: How would you implement producer-consumer?

Priya:

BlockingQueue<String> queue = new ArrayBlockingQueue<>(10);

// Producer
queue.put("QC-8842"); // blocks if full

// Consumer
String orderId = queue.take(); // blocks if empty
processOrder(orderId);

For multiple workers, share one queue. Any producer can put. Any consumer can take. BlockingQueue handles all synchronization.


Maya: How would you prevent deadlock in a wallet transfer?

Priya: Lock both wallets in a fixed order — always the lower account ID first:

Wallet first = from.getId() < to.getId() ? from : to;
Wallet second = from.getId() < to.getId() ? to : from;
synchronized (first) {
    synchronized (second) {
        from.withdraw(amount);
        to.deposit(amount);
    }
}

Every thread follows the same rule. No circle of waiting.


Maya: How do you handle errors in parallel execution?

Priya: Several layers:

  • try-catch inside each async task.
  • future.get(timeout) on the caller side so one slow task does not block forever.
  • exceptionally() or handle() in CompletableFuture chains.
  • andCollectFailures() in reactive code so one failure does not stop all parallel tasks.
  • MDC context in every async path so error logs show the order ID.

Maya: Last one — how did you use multithreading in QuickCart?

Priya: Fixed thread pools for order processing and email sending. CompletableFuture for parallel payment, shipping, and fraud checks — cut checkout wait from 1.5 seconds to about 400 milliseconds. BlockingQueue between packing and notification steps. ConcurrentHashMap for the product cache. AtomicInteger for daily order metrics. MDC copy into async tasks so logs always show the order ID. Lock ordering on wallet transfers to avoid deadlock. tryLock with timeout on payment and inventory locks. Semaphore to cap concurrent payment API calls at fifty.


Maya nodded. “You sound ready. Keep answers that short in the real interview. One idea. One example from your project.”


Course complete

You followed Priya from her first raw thread through pools, futures, synchronization, concurrent collections, producer-consumer, deadlock, MDC, and reactive async. You have the words and the code to explain multithreading in interviews and on the job.

Keep learning:

Good luck. Build something. Break it on purpose. Fix it. That is how it sticks.