
Here’s a structured list of Java multithreading interview questions, starting from basic concepts and moving up to advanced topics. This collection will help you quickly revise and strengthen your multithreading knowledge for interviews.
1. What is multithreading in Java?
Multithreading in Java allows a program to execute multiple tasks simultaneously using threads. It improves performance by utilizing CPU cores efficiently and helps in creating responsive applications. For example, in a web server, each request can be handled by a separate thread instead of processing them sequentially. We can implement multithreading using Thread class or Runnable interface.
2. What is thread?
A thread is a lightweight process that runs independently within a program. It allows multiple tasks to execute concurrently, improving performance and responsiveness. In Java, we can create threads using the Thread
class or Runnable
interface
3. How is multithreading different from multiprocessing?
Multithreading lets us run multiple threads within the same process, sharing memory and resources. It’s lightweight and great for tasks like handling multiple user requests in a web server. Multiprocessing, on the other hand, runs multiple processes separately, each with its own memory. It’s heavier but avoids thread-safety issues, making it useful for CPU-intensive tasks like video rendering.
So, if we need efficient, shared execution, we go with multithreading. If we need true parallelism, multiprocessing is the way to go.
4. What are the benefits of multithreading?
Multithreading helps us make better use of CPU resources by allowing multiple tasks to run in parallel. This makes applications more responsive since background tasks don’t block the main execution. It also speeds processing large computations happens faster when tasks run simultaneously.
Plus, since threads share memory, it’s more efficient than creating separate processes. So, if we want speed, responsiveness, and efficient resource usage, multithreading is a great choice.
5. How do you create a thread in Java?
In Java, we create a thread in three ways by extending Thread
, implementing Runnable
, or using a lambda.
When we extend Thread
, we override the run()
method and start the thread using start()
. But since Java doesn’t support multiple inheritance, this approach limits flexibility.
Another one is by implementing Runnable
, where we also override run()
, but instead of creating a thread directly, we pass its instance to a Thread
object. This allows us to extend other classes while keeping the code reusable.
The simplest one is using a lambda. Since Runnable
is a functional interface, we can pass the logic directly inside Thread
, reducing extra code. So, if we need flexibility, we go with Runnable
, and if we want cleaner code, we use a lambda.
6. What is the difference between extending Thread
and implementing Runnable
?
The key difference is that when we extend Thread
, our class directly inherits from it and overrides the run()
method. But since Java doesn’t support multiple inheritance, we can’t extend any other class.
On the other hand, when we implement Runnable
, we still override run()
, but instead of creating a thread directly, we pass an instance of our class to a Thread
object. This way, we keep our class more flexible and free to extend another class if needed.
So, if we don’t need inheritance, Thread
works fine, but for better flexibility and reusability, Runnable
is the better choice.
7. What are the states of a thread in Java?
A thread in Java goes through multiple states. It starts as NEW when created, then moves to RUNNABLE after calling start(), meaning it’s ready but waiting for CPU time.
Once scheduled, it enters RUNNING and executes its task. From there, it might get BLOCKED (waiting for a lock), WAITING (paused indefinitely), or TIMED WAITING (paused for a set time). After any of these, it always goes back to RUNNABLE state before running again. Finally, when the task is done, it reaches TERMINATED and stops
1 2 3 | NEW → RUNNABLE → RUNNING → TERMINATED ↑ ↓ (BLOCKED / WAITING / TIMED WAITING) |
8. How do you start and stop a thread?
To start a thread in Java, we create a Thread
object and call start()
, which begins its execution.
To stop a thread safely, we don’t use stop()
(since it’s deprecated). Instead, we generally use a flag variable. The thread keeps running while the flag is true
, and when we set it to false
, it exits run()
gracefully.
Another way is using interrupt()
. If the thread is sleeping or waiting, calling interrupt()
wakes it up by throwing InterruptedException
, allowing us to handle the stop inside catch
. We also check Thread.interrupted()
inside the loop to stop it cleanly.
9. What is the difference between start()
and run()
in threads?
Calling start()
creates a new thread and runs run()
inside it. This means the task executes in parallel with the main thread.
On the other hand, calling run()
directly doesn’t start a new thread. It just runs like a normal method call in the same thread. So, no concurrency happens.
10. What is the sleep()
method in Java, and how does it work?
The sleep()
method in Java pauses the current thread for a specified time. It’s a static method from Thread
class, meaning it always affects the thread that calls it.
While sleeping, the thread doesn’t release locks it holds – it just waits. After the time is up, it moves back to the runnable state, waiting for CPU time. If another thread calls interrupt()
while it’s sleeping, it throws InterruptedException
and wakes up early.
11. What is the volatile
keyword, and how is it different from synchronized
?
The volatile
keyword ensures that changes to a variable are immediately visible to all threads. It prevents CPU caching but doesn’t provide atomicity.
On the other hand, synchronized
ensures both visibility and atomicity by allowing only one thread to access the critical section at a time.
So, if multiple threads only read and write a variable without complex operations, volatile
is enough. But if we need to ensure atomic updates (like incrementing a counter), we use synchronized
.
12. When should one prefer volatile
over synchronized
?
We prefer volatile
when multiple threads only need to read a variable, and a single thread is writing to it. In this case, atomicity isn’t a concern because we’re sure that only one thread updates it. volatile
ensures visibility, so all threads always see the latest value immediately. Plus, it removes the overhead of synchronization, making it more efficient. For example, using volatile
with a signal variable.
But if we need both visibility and atomicity, like modifying shared data safely, we go with synchronized
, since it ensures only one thread can change the value at a time.
13. How can you make a thread wait for another thread to complete?
We can make a thread wait for another thread to complete using the join()
method. When we call thread.join()
, the current thread pauses and waits until the specified thread finishes execution.
For example, if Thread A
calls B.join()
, A
will wait until B
completes before continuing. We can also use join(time)
to wait for a specific duration instead of indefinitely.
14. What is a race condition? How do you handle it?
A race condition happens when multiple threads try to modify shared data at the same time, leading to inconsistent results. This usually occurs when thread execution order isn’t controlled, causing unexpected behavior.
To handle it, we use synchronization techniques like synchronized
blocks and methods, ReentrantLock
, or Atomic
variables. So, these ensure that only one thread modifies the shared resource at a time, preventing inconsistent data updates.
15. How does Java manage multiple threads on a multi-core processor?
Java relies on the OS thread scheduler to assign JVM threads to CPU cores, enabling true parallelism on multiple cores and context switching when threads exceed number of core count.
Suppose we have an octa-core CPU and create 8 threads, each thread will run truly parallel on separate cores. But, if you create more than 8 threads, the OS will perform context switching to make it appear as if all threads are running simultaneously.
16. Why must synchronization in Java use the same lock monitor instead of multiple locks for different threads? What issues arise if different locks are used?
We use the same lock monitor in Java synchronization to make sure only one thread can access the critical section at a time. If different locks were used, multiple threads could enter the synchronized block simultaneously, completely breaking synchronization. This would lead to race conditions and inconsistent data.
17. Why is using synchronized(this) not ideal when an instance has multiple critical sections?
If we use synchronized(this)
, we’re locking the entire object’s monitor. That means even if two threads are working on separate synchronized methods or blocks within the same instance, one has to wait for the other to release the lock. This reduces concurrency.
18. How does using separate lock objects improve concurrency in a multithreaded environment?
By using separate lock objects for different critical sections in the same instance, we let multiple threads work at the same time. This way, one thread using lockA
won’t block another using lockB
within that instance, helping things run faster and smoother.
19. Can exceptions in one thread propagate to another thread automatically?
No, exceptions in one thread don’t automatically propagate to another thread. Each thread runs independently, so if a thread throws an exception, it won’t affect other threads unless we explicitly handle it, like using shared data or mechanisms like ExecutorService.awaitTermination()
to check for failures.
20. Why do we need to handle InterruptedException inside the thread itself?
We need to handle InterruptedException
inside the thread because it’s a signal that the thread should stop or perform cleanup. If we ignore it, the thread might keep running when it was supposed to stop. Instead of ignoring the exception, we should either propagate it or gracefully exit by checking if the current thread is interrupted, like using Thread.currentThread().isInterrupted().
21. What are the core different methods available in the Thread lifecycle and their uses?
In Java, multiple methods are called during a thread’s life cycle.
First, to start a thread, we use start()
, which moves it from NEW to RUNNABLE. If we mistakenly call run()
, it simply runs in the current thread instead of starting a new one.
To pause execution, sleep(ms)
temporarily puts the thread in TIMED_WAITING, while join()
makes the current thread wait until another thread finishes. Similarly, wait()
puts a thread on hold until another thread calls notify()
or notifyAll()
to resume execution. These are key for thread synchronization.
When stopping a thread, interrupt()
signals it to stop, but the thread must handle it properly. Finally, isAlive()
checks if a thread is still running or has finished execution.
22. What is the difference between wait(), sleep(), and yield()?
When a thread calls wait()
, it pauses execution, releases the lock, and stays in the waiting state until another thread calls notify()
.
sleep(ms)
, on the other hand, pauses execution for a specified time but retains the lock, preventing other threads from proceeding. yield()
simply hints to the scheduler that the thread is willing to give up CPU time, but it does not guarantee a context switch.
23. What is Context Switching?
Context switching is the process where the CPU saves the current state of a thread and switches to another thread for multitasking. It happens when the scheduler suspends a running thread and assigns CPU time to another, ensuring that multiple tasks make progress without one thread blocking others. This helps keep the CPU actively working on different tasks instead of idling while waiting for one thread to finish.
24. What is the difference between notify() and notifyAll()?
notify()
wakes up one waiting thread, chosen randomly, while notifyAll()
wakes up all waiting threads on the same monitor. If multiple threads are waiting, notify()
selects one at random, whereas notifyAll()
ensures all threads have a chance to compete for execution. Therefore, we use notify()
when only one thread needs to proceed, and notifyAll()
when multiple threads should be considered.
25. What is the difference between wait(), sleep(), and yield()?
We use wait()
when we want a thread to pause execution until another thread notifies it. It releases the lock, so other threads can proceed. On the other hand, sleep(ms)
is used when we just want to pause the thread for a specific time, but it doesn’t release the lock. yield()
is more of a hint to the scheduler that the thread is willing to give up CPU time so other threads of the same priority can run, but it doesn’t guarantee a switch and doesn’t release the lock.
26. Why should wait(), notify(), and notifyAll() be called inside synchronized blocks?
Because these methods operate on the object’s monitor lock, and calling them outside a synchronized block will throw IllegalMonitorStateException
.
27. What is deadlock, and how can you prevent it?
A deadlock happens when two or more threads are stuck waiting for each other to release resources, but none of them can proceed, leading to an indefinite halt. It usually occurs when multiple threads acquire locks in an inconsistent order, causing a circular wait.
To prevent deadlocks, we must ensure that lock ordering is consistent. Additionally, we can use ReentrantLock
with tryLock()
and a timeout to avoid indefinite waiting. We can also minimize nested locks and implement deadlock detection mechanisms to catch issues before they cause problems.
28. Explain common deadlock detection mechanisms in brief.
There are a few common ways to detect deadlocks. One is using a Wait-for Graph, where we look for cycles between threads and resources. Another is Thread Monitoring, which checks if threads are stuck in WAITING or BLOCKED states for too long. Lastly, Timeouts, such as using tryLock()
from ReentrantLock
with a timeout, help avoid indefinite waiting and can detect potential deadlocks early.
29. What is a livelock, and how is it different from a deadlock?
A livelock occurs when two or more threads keep changing their states in response to each other but without making any real progress. Unlike a deadlock, where threads are completely stuck waiting for resources, livelocked threads remain active but continuously retry the same operations without succeeding.
For example, if two threads keep trying to acquire a lock but release it immediately when they fail, only to retry again, they fall into a livelock.
The only key difference is that in a deadlock, threads are stuck indefinitely, whereas in a livelock, threads are still running but unable to proceed with their tasks.
30. What is thread starvation, and how can it be prevented?
Thread starvation is a situation where a thread is unable to gain regular access to the resources it needs to execute because other threads are constantly being given priority. To overcome this, we can use fair scheduling mechanisms provided by ReentrantLock with fairness enabled to ensure all threads get a fair chance to execute based on FIFO order.
31. What is the Fork/Join framework in Java?
The Fork/Join framework in Java is used to efficiently handle parallelism by forking tasks into smaller sub-tasks and then joining their results. It is part of the java.util.concurrent
package that is introduced back in Java 7.
32. What are the main components of the Fork/Join framework in Java?
This Fork/Join framework consists of several key components, some of them are –
- RecursiveAction – Used when we want a task needs to be performed without returning a result.
- ForkJoinPool – A specialized thread pool used for managing parallel task execution.
- ForkJoinTask – A base class for tasks that can be divided into smaller tasks.
- RecursiveTask<V> – Used when we want a result needs to be returned after computation.
33. What is a ThreadLocal variable?
A ThreadLocal variable in Java is a special type of variable that is local to a specific thread. Each thread that accesses a ThreadLocal variable gets its own isolated copy, that means changes made by one thread do not affect others.
We use it commonly for storing thread-specific data, such as user sessions, transaction contexts, generally where sharing data between threads is not required.
34. What is the difference between fair and non-fair locks in Java?
In threading, a fair lock grants access in the order threads request it, preventing starvation but adding overhead. A non-fair lock allows any thread to acquire it immediately if available, improving performance but risking starvation. By default, ReentrantLock is non-fair, but we can enable fairness with setting reentrant lock flag as true. e.g., new ReentrantLock(true)
.
35. What are atomic variables, and why are they useful?
Atomic variables are special variables in Java that support atomic operations without needing synchronization. They are part of the java.util.concurrent.atomic
package and provide methods like get()
, set()
, incrementAndGet()
, and compareAndSet()
.
Some common atomic variables that are generally used are –
- AtomicInteger – For atomic operations on integers.
- AtomicLong – For atomic operations on long values.
- AtomicBoolean – For atomic operations on boolean values.
- AtomicReference<T> – For atomic operations on object references.
Also, we can use the Integer class with AtomicReference, e.g., AtomicReference<Integer>, but it is not recommended because we already have integer specific variable AtomicInteger that operates directly on the primitive data type int.
36. What is the difference between synchronized and ReentrantLock?
synchronized is implicit in nature, that means it handles locking and unlocking automatically, while ReentrantLock is manual, requiring explicit calls to lock() and unlock(). It also supports features like tryLock(), fairness, and condition variables for advanced thread coordination. Use synchronized for simple cases, but if more flexibility is needed, ReentrantLock is the better choice.
37. What is the purpose of using a thread pool in Java?
We use thread pools to manage and reuse a fixed number of threads instead of creating new threads every time a task needs to be executed. This helps in controlling resource usage, reducing overhead of thread creation, and improving application performance by efficiently handling multiple tasks without exhausting system resources.
38. How can you implement a thread pool in Java?
There are two common ways to create a thread pool in Java. The first is by using the factory methods provided by the Executors class, such as newFixedThreadPool() or newSingleThreadExecutor(). These methods offer a quick and simple way to set up a thread pool without worrying about detailed configuration, making them ideal for straightforward use cases where the default settings are sufficient.
However, if we need more fine-grained control over the thread pool’s behavior, the second approach is to directly use the ThreadPoolExecutor class. This allows us to explicitly define parameters like core pool size, maximum pool size, queue capacity, rejection policies, and so on.
39. What is the difference between core pool size and maximum pool size in ThreadPoolExecutor
?
The core pool size defines the minimum number of threads that the thread pool will keep alive, even if those threads are idle and not executing any tasks. These threads stay ready to handle incoming tasks immediately. On the other hand, the maximum pool size specifies the upper limit on how many threads can exist in the pool at any given time. When the number of tasks exceeds the capacity of the core threads and the task queue is full, the pool can create additional threads up to this maximum limit to handle the extra load.
For more details on Java Concurrency, you can visit the Official Java Concurrency Documentation.
Leave a Reply