This explores essential and advanced interview questions on Java multithreading, focusing on thread creation using Thread vs Runnable, lifecycle states, and core thread methods like start(), join(), sleep(), and yield(). It also covers thread priorities, synchronization basics, intrinsic locks, and common pitfalls like race conditions and visibility issues, key for building robust concurrent systems.
1. Compare thread creation using the Thread class and the Runnable interface. When would you prefer one over the other in a scalable multithreaded application?
Threads in Java can be created in two main ways: by extending the Thread class or by implementing the Runnable interface. Both approaches achieve the same goal, but they differ in design flexibility, scalability, and best practices in real-world applications.
| Criteria | Thread class | Runnable interface |
|---|---|---|
| Inheritance | Cannot extend any other class (since Java allows single inheritance only) | Allows extending another class, since Runnable is just an interface |
| Design Principle | Leads to tighter coupling | Promotes abstraction and loose coupling |
| Scalability | Less preferred in large applications | Preferred, especially in enterprise/multithreaded environments |
Example (Runnable preferred):
Runnable task = () -> System.out.println("Task");
new Thread(task).start();
Why Prefer Runnable?
- Promotes reusability of code
- Works seamlessly with ExecutorService / thread pools
- Follows better OOP design principles
2. What happens if you call the run() method directly instead of start() on a Thread object? Illustrate with output.
In Java, a thread’s execution begins only when you call the start() method, which internally triggers the JVM to create a new call stack for the thread. If you call run() directly, it behaves like a normal method call, running on the current thread instead of starting a new one.
Example:
public class Geeks{
public static void main(String[] args) {
Thread t = new Thread(() ->
System.out.println("Running in: " + Thread.currentThread().getName())
);
t.run(); // Direct call - runs in main thread
t.start(); // Proper way - runs in new thread
}
}
Output
Running in: main Running in: Thread-0
3. Describe the complete lifecycle of a Java thread. What transitions can occur between states and under what circumstances? Draw a diagram if needed.
A thread in Java does not run continuously from start to finish—it moves through a series of well-defined states. Understanding these states is essential for debugging, designing concurrent programs, and avoiding issues like deadlocks or unnecessary waiting.
Thread States:
- NEW: Thread is created but not yet started
- RUNNABLE: Eligible to run, waiting for CPU scheduling
- RUNNING: (Not in enum, but conceptually) thread actually executes
- BLOCKED: Waiting to acquire an intrinsic lock
- WAITING: Waiting indefinitely until notified
- TIMED_WAITING: Waiting with timeout (sleep, join)
- TERMINATED: Completed or crashed
- start() -> NEW -> RUNNABLE
- wait() -> WAITING
- join(timeout) -> TIMED_WAITING
- Lock contention -> BLOCKED
- run() exits -> TERMINATED
4. How does the join() method work in Java? How is it internally implemented and what are the concurrency pitfalls of improper join() usage?
Sometimes you want one thread to finish its work before another thread continues. Java provides the join() method for this purpose, allowing one thread to wait until another completes execution. This is especially useful in scenarios like aggregating results from worker threads
t1.join(); // join() pauses one thread until another is done.
Internal Implementation:
- join() is built using synchronized + wait().
- When the target thread (t) dies, it calls notifyAll() to wake up any waiting threads.
Pitfalls:
- Deadlock if join() is called inside the thread it’s waiting for
- Blocking unnecessarily can hurt performance
5. What is the difference between sleep() and wait()? How does monitor ownership affect them?
In java both sleep() and wait() pause a thread’s execution, but they are not the same. The key difference lies in monitor (lock) ownership and how the thread wakes up.
| Aspect | sleep() | wait() |
|---|---|---|
| Defined in | Thread class | Object class |
| Lock behavior | Doesn’t release the lock | Releases lock temporarily |
| Wake-up | Automatically after timeout | Must be woken up by notify() / notifyAll() |
Example:
synchronized(obj) {
obj.wait(); // Releases lock
Thread.sleep(1000); // Holds lock during sleep
}
6. What is the purpose of yield()? In what situations might it cause performance degradation or starvation?
Sometimes a thread may want to give other threads a chance to run, especially if it doesn’t have urgent work. Java provides the yield() method to signal the scheduler that the current thread is willing to pause.
- It tells the thread scheduler: “I’m ready to pause, let others run if they want.”
- The thread moves from RUNNING-> RUNNABLE, but may be scheduled again immediately.
- Behavior is platform-dependent (scheduler may ignore it).
Thread.yield(); // Voluntary pause
Issues:
- Scheduler may ignore it
- Overuse can lead to frequent context switching → poor performance
- Starvation possible if high-priority threads dominate
7. Explain thread scheduling in Java. Does thread priority guarantee execution order? How does JVM behavior vary across platforms?
Thread scheduling in Java is the process of determining which thread runs next among all runnable threads. It is influenced by thread priority, but the scheduler may ignore priorities depending on the OS and JVM implementation.
- Thread priority does not guarantee execution order.
- For predictable ordering, use join(), synchronization, or executors instead.
Example:
Runnable task = () -> System.out.println(Thread.currentThread().getName() + " running");
Thread t1 = new Thread(task, "Low-Priority");
Thread t2 = new Thread(task, "High-Priority");
t1.setPriority(Thread.MIN_PRIORITY); // 1
t2.setPriority(Thread.MAX_PRIORITY); // 10
t1.start();
t2.start();
Output:
High-Priority running
Low-Priority running
8. Demonstrate a classic race condition with a shared counter. How can you fix it using synchronized?
A race condition occurs when two or more threads read and write shared data concurrently, leading to inconsistent or incorrect results.
Example of Race Condition:
class Counter {
int count = 0;
void increment() {
count++; // Not thread-safe
}
}
public class Geeks{
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Final count: " + counter.count);
}
}
Output:
Final count: 1784 // Expected 2000
Fix Using synchronized:
class Counter {
int count = 0;
synchronized void increment() {
count++; // Thread-safe
}
}
9. How do intrinsic locks (monitor locks) work in Java? Which objects are they associated with and how do they affect concurrency?
An intrinsic lock (monitor) is a mutex associated with every Java object that ensures mutual exclusion when multiple threads access synchronized code.
How They Work:
- When a thread enters a synchronized block or method, it acquires the object's monitor lock.
- Other threads attempting to enter the same synchronized block are blocked until the lock is released.
- Static synchronized methods use the class-level lock (ClassName.class) instead of an instance lock.
Example:
class SharedResource {
synchronized void criticalSection() {
System.out.println(Thread.currentThread().getName() + " inside critical section");
}
}
public class MonitorDemo {
public static void main(String[] args) {
SharedResource resource = new SharedResource();
new Thread(() -> resource.criticalSection(), "Thread-1").start();
new Thread(() -> resource.criticalSection(), "Thread-2").start();
}
}
Output
Thread-1 inside critical section Thread-2 inside critical section
Impact:
- Only one thread can hold the lock
- Others are BLOCKED
10. How do wait(), notify(), and notifyAll() work together to implement inter-thread communication? Give a producer-consumer example.
In multithreading, sometimes a thread must pause until another thread signals it. Java provides wait(), notify(), and notifyAll() for threads to communicate and coordinate safely using intrinsic locks.
- Wait(): Releases the lock and makes the thread wait.
- notify(): Wakes up one waiting thread.
- notifyAll(): Wakes up all waiting threads.
Producer-Consumer Example:
import java.util.LinkedList;
import java.util.Queue;
class Buffer {
Queue<Integer> queue = new LinkedList<>();
int capacity = 5;
synchronized void produce(int value) throws InterruptedException {
while(queue.size() == capacity) wait();
queue.add(value);
System.out.println("Produced: " + value);
notifyAll();
}
synchronized int consume() throws InterruptedException {
while(queue.isEmpty()) wait();
int val = queue.poll();
System.out.println("Consumed: " + val);
notifyAll();
return val;
}
}
public class ProducerConsumer {
public static void main(String[] args) {
Buffer buffer = new Buffer();
Thread producer = new Thread(() -> {
try { for(int i=1;i<=5;i++) buffer.produce(i); }
catch(InterruptedException e) {}
});
Thread consumer = new Thread(() -> {
try { for(int i=1;i<=5;i++) buffer.consume(); }
catch(InterruptedException e) {}
});
producer.start();
consumer.start();
}
}
Output
Produced: 1 Produced: 2 Produced: 3 Produced: 4 Produced: 5 Consumed: 1 Consumed: 2 Consumed: 3 Consumed: 4 Consumed: 5
Explanation:
- A shared buffer (Queue) holds produced items and ensures both threads coordinate access.
- The producer adds items to the buffer and waits if the buffer is full, notifying consumers after producing.
- The consumer removes items from the buffer and waits if the buffer is empty, notifying producers after consuming.
- wait() and notifyAll() manage synchronization, preventing race conditions and ensuring proper inter-thread communication.
11. What is a visibility problem in Java multithreading? How does the volatile keyword resolve it?
A visibility problem occurs when one thread updates a variable, but other threads cannot see the latest value due to caching or CPU optimization.
Solution with volatile:
- Declaring a variable as volatile ensures all reads/writes go directly to main memory.
- This guarantees that changes by one thread are visible to all other threads.
Note: volatile does not make operations atomic (like count++).
Example:
volatile boolean running = true;
12. Can you write a deadlock scenario in Java? How would you detect and prevent it?
Deadlock is a situation in multithreading where threads cannot proceed because each is waiting for a resource held by another thread, creating a circular wait.
class Resource {
void methodA(Resource r2) {
synchronized(this) {
System.out.println(Thread.currentThread().getName() + " locked Resource1");
synchronized(r2) {
System.out.println(Thread.currentThread().getName() + " locked Resource2");
}
}
}
}
public class DeadlockDemo {
public static void main(String[] args) {
Resource r1 = new Resource();
Resource r2 = new Resource();
Thread t1 = new Thread(() -> r1.methodA(r2), "Thread-1");
Thread t2 = new Thread(() -> r2.methodA(r1), "Thread-2");
t1.start();
t2.start();
}
}
Output: Time limit exceeded.
How to Detect Deadlocks:
- Use tools like jconsole, jvisualvm, or thread dumps to identify blocked threads and circular waits.
Prevention Techniques:
- Lock Ordering: Always acquire multiple locks in the same order.
- Try-Locks (ReentrantLock): Use tryLock() with timeout to avoid waiting forever.
- Minimize Lock Scope: Hold locks only as long as necessary.
13. How is thread safety different from atomicity and visibility? Can you have atomic operations that are not thread-safe? Explain.
- Atomicity: An operation is atomic if it completes entirely or not at all, without interruption.
- Visibility: Changes made by one thread are immediately visible to other threads.
- Thread Safety: A piece of code is thread-safe if it works correctly when accessed by multiple threads simultaneously, combining atomicity, visibility, and proper synchronization.
Atomic operations may not be thread-safe if they involve multiple steps.
Example: count++ looks atomic but internally has read-> increment-> write, so multiple threads can interfere, causing lost updates.
Tricky Case:
int count = 0;
Runnable task = () -> {
for(int i=0; i<1000; i++) {
count++; // Not thread-safe
}
};Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Final count: " + count); // Often less than 2000
Fix Using AtomicInteger or Synchronization:
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet(); // Thread-safe atomic operation
14. How does the JVM handle multiple threads competing for the same lock? Describe fairness and thread starvation.
When multiple threads try to access a synchronized block or method simultaneously, the JVM uses the intrinsic lock (monitor) to control access. Only one thread can hold the lock at a time, while others wait.
- By default, JVM uses a non-fair locking strategy, so waiting threads may acquire the lock in an unpredictable order.
- Starvation can happen if high-priority threads dominate access.
- Fair locks (ReentrantLock(true)) allow threads to acquire the lock in FIFO order, reducing starvation risk.
Example Using ReentrantLock:
import java.util.concurrent.locks.ReentrantLock;
public class FairLockDemo{
static ReentrantLock lock = new ReentrantLock(true); // fair lock
public static void main(String[] args) {
Runnable task = () -> {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " acquired lock");
} finally {
lock.unlock();
}
};
new Thread(task, "Thread-1").start();
new Thread(task, "Thread-2").start();
new Thread(task, "Thread-3").start();
}
}
Output
Thread-1 acquired lock Thread-2 acquired lock Thread-3 acquired lock
15. How would you implement a custom thread-safe singleton in Java? Why does double-checked locking require volatile?
In multithreaded applications, a singleton class must ensure that only one instance is created, even when multiple threads try to access it simultaneously. Improper implementation can lead to multiple instances or race conditions.
Double-Checked Locking Implementation:
class Singleton {
private static volatile Singleton instance;
private Singleton() {} // private constructor
public static Singleton getInstance() {
if (instance == null) { // First check (no lock)
synchronized (Singleton.class) {
if (instance == null) { // Second check (with lock)
instance = new Singleton();
}
}
}
return instance;
}
}
Why volatile? Without it, another thread may see a partially constructed object due to instruction reordering.