Multithreading in Java: Thread Lifecycle, synchronized and the Interview Question Set

Understand what Java threads share, trace a lost counter update, and separate BLOCKED, WAITING, and TIMED_WAITING with code you can explain.

KnowledgeGate Team

Exam prep & CS education

Updated 27 Jul 20267 min read

Interviewers ask about threads to see whether you understand shared mutable state, not whether you memorised a class name. Many candidates can start a thread but cannot explain why a counter loses updates or when a thread is BLOCKED rather than WAITING. Those are the distinctions that decide output and debugging questions.

Two ways to create a Java thread

You can extend Thread and override run():

class Worker extends Thread {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}

new Worker().start();

Or implement Runnable and pass the task to a Thread:

class Task implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}

new Thread(new Task()).start();

Prefer Runnable for a task that does not need to be a specialised thread. It separates the work from the execution mechanism and leaves the class free to extend another class.

The interview trap is start() versus run(). Calling run() is an ordinary method call on the current thread. Calling start() asks the JVM to create a new thread of execution, which then invokes run(). A Thread object can be started only once; calling start() again throws IllegalThreadStateException.

The six states in the Java thread lifecycle

Thread.State defines six states. The names describe what the JVM observes, not every operating-system scheduling detail.

State

Meaning

NEW

The Thread object exists, but start() has not been called.

RUNNABLE

The thread is eligible to run or is running in the JVM.

BLOCKED

It is waiting to acquire an intrinsic monitor lock for synchronized.

WAITING

It is waiting without a timeout through operations such as wait(), join(), or park().

TIMED_WAITING

It is waiting up to a time limit through sleep(t), wait(t), or join(t).

TERMINATED

Its run() method has finished, normally or by an uncaught exception.

Do not say that RUNNABLE means definitely executing on a CPU. Java groups ready and running threads in that state. Do not call a thread in sleep() blocked either. It is TIMED_WAITING, even if it went to sleep while holding a lock.

A Java thread state-transition diagram with NEW -> RUNNABLE labelled start(), RUNNABLE <-> BLOCKED labelled lock contention and lock acquired, RUNNABLE <-> WAITING labelled wait()/join() and notify()/join completes, RUNNABLE <-> TIMED_WAITING labelled sleep(t)/wait(t) and timeout, and RUNNABLE -> TERMINATED labelled run() returns.

This vocabulary matters when an interviewer gives you a thread dump or asks which state applies while one thread waits to enter a synchronized block.

The lost-update race every interview asks

Consider a shared counter:

class Counter {
    int c = 0;

    void inc() {
        c++;
    }
}

Two threads call inc() 100,000 times each. The intended result is:

100,000 + 100,000 = 200,000

But c++ is a read-modify-write operation, not one indivisible action. Suppose c is 41:

  1. Thread A reads 41.

  2. Thread B reads 41 before A stores its result.

  3. A computes and stores 42.

  4. B also computes and stores 42.

Two calls completed, but the value increased from 41 to 42, an increase of only 1 instead of 2. Repeated overlaps make the final result unpredictable and it can be below 200,000. A particular run may appear correct by chance, which does not make the program safe.

The direct monitor fix is:

synchronized void inc() {
    c++;
}

Only one thread at a time can execute this method on the same Counter instance. An AtomicInteger is another suitable counter implementation:

private final AtomicInteger c = new AtomicInteger();

void inc() {
    c.incrementAndGet();
}

Its atomic update avoids the lost read-modify-write interleaving. The same race, generalised to the critical-section problem an operating-systems course states formally, is worked through in Process Synchronization and Semaphores: Race Conditions, Peterson's Solution, Wait and Signal.

What synchronized actually guarantees

An instance synchronized method locks that object's monitor. Two threads calling it on the same instance exclude each other, but calls on different instances use different locks.

A static synchronized method locks the Class object. That is not the same monitor as any instance, so an instance-synchronised method and a static-synchronised method can run concurrently unless you deliberately make them use one common lock.

Java monitors are reentrant. A thread that already holds a monitor can enter another synchronized method or block guarded by the same monitor without deadlocking itself.

synchronized gives both mutual exclusion and memory visibility. A monitor unlock happens before a later successful lock of the same monitor, so writes made inside one critical section become visible to the next holder. volatile provides visibility for reads and writes of the field, but it does not turn the three-part c++ operation into an atomic update.

A classic deadlock uses two locks in opposite order. Thread A holds first and waits for second; thread B holds second and waits for first. Consistent lock ordering prevents that circular wait.

wait, notify, and notifyAll without the usual mistakes

Call wait(), notify(), or notifyAll() only while holding that object's monitor. Otherwise, Java throws IllegalMonitorStateException.

wait() releases the monitor before suspending the thread. sleep() does not release monitors the thread already holds. That difference is a favourite interview question.

Wait on a condition in a loop:

synchronized (queue) {
    while (queue.isEmpty()) {
        queue.wait();
    }
    consume(queue.remove());
}

The loop checks the condition again after waking, which handles spurious wakeups and cases where another thread consumes the resource first. Prefer notifyAll() unless you can prove that waking one arbitrary waiter is correct for every possible waiting condition.

Modern Java concurrency in three tools

Use ExecutorService and a bounded or well-chosen thread pool when you need to submit many tasks without managing raw threads one by one. Use Callable<V> with Future<V> when a task returns a result or throws a checked exception. Virtual threads make large numbers of blocking I/O tasks cheaper to represent, and they sit beside the other platform changes covered in Java 8 to Java 21 Features Interviewers Actually Ask.

These tools improve task management, but they do not remove races. Shared mutable state still needs a sound ownership or synchronisation strategy.

The interview question set: ten answers to have ready

These ten come up again and again. Answer in a sentence or two and stop; the follow-up is where the interviewer is heading.

  1. What do two threads in one JVM share, and what stays private? Objects on the heap and static fields are shared. Each thread keeps its own stack, locals and program counter, so a local can never be the source of a data race.

  2. Which of start() and run() creates concurrency? Only start(). Calling run() directly executes the body on the calling thread, and nothing new is scheduled.

  3. What happens if you call start() twice on one Thread object? It throws IllegalThreadStateException. A finished thread cannot be restarted either; construct a new Thread.

  4. A thread is waiting to enter a synchronized block. Which state does a thread dump show? BLOCKED. Waiting inside wait() with no timeout is WAITING, while sleep(t), wait(t) and join(t) give TIMED_WAITING.

  5. Does a sleeping thread give up its locks? No. sleep() keeps every monitor the thread already holds, whereas wait() releases the monitor it was called on. That is why a sleep inside a critical section stalls every other contender.

  6. Why is c++ not atomic? It is a read, an add and a write. A second thread can read the old value between your read and your write, so one increment is overwritten.

  7. Would volatile fix the counter? No. volatile makes every read see the latest write, but the three-step update is still interruptible between the steps. Use synchronized or AtomicInteger.

  8. Can an instance synchronized method and a static synchronized method of the same class run at the same time? Yes. One locks the instance monitor and the other locks the Class object, so they never exclude each other.

  9. Why is wait() called inside while rather than if? A thread can wake spuriously, and another consumer may take the item before this one re-acquires the lock. Rechecking the condition after waking is the only safe form.

  10. Two threads take two locks in the opposite order. Name the failure and the fix. A circular wait, which is deadlock. Fix it by ordering the locks globally so every thread acquires them in the same sequence.

The short version and your next step

Concurrency bugs are not caused by a missing keyword. They are caused by shared mutable state with no agreed owner. Decide which field is shared and which monitor guards it before you write the code, and synchronized, volatile and AtomicInteger become choices about how to enforce that decision rather than rescues after the fact.

Our Programming Languages question bank carries more than 1,300 practice questions, close to 300 of them on Java. Drill lifecycle and output questions alongside the Complete Java course, broaden the coding-round context through the Placement Preparation category, and use the Mera Placement Hoga bundle for a structured next step.