![]() |
VOOZH | about |
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.
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?
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:
Running in: main Running in: Thread-0
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:
- start() -> NEW -> RUNNABLE
- wait() -> WAITING
- join(timeout) -> TIMED_WAITING
- Lock contention -> BLOCKED
- run() exits -> TERMINATED
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:
Pitfalls:
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
}
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.
Thread.yield(); // Voluntary pause
Issues:
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.
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
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:
Output:
Final count: 1784 // Expected 2000
Fix Using synchronized:
class Counter {
int count = 0;
synchronized void increment() {
count++; // Thread-safe
}
}
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:
Example:
Thread-1 inside critical section Thread-2 inside critical section
Impact:
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.
Producer-Consumer Example:
Produced: 1 Produced: 2 Produced: 3 Produced: 4 Produced: 5 Consumed: 1 Consumed: 2 Consumed: 3 Consumed: 4 Consumed: 5
Explanation:
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:
Note: volatile does not make operations atomic (like count++).
Example:
volatile boolean running = true;
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.
Output: Time limit exceeded.
How to Detect Deadlocks:
Prevention Techniques:
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
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.
Example Using ReentrantLock:
Thread-1 acquired lock Thread-2 acquired lock Thread-3 acquired lock
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.