Practice
Resources
Contests
Online IDE
New
Free Mock
Events New Scaler
Practice
Improve your coding skills with our resources
Contests
Compete in popular contests with top coders
logo
Events
Attend free live masterclass hosted by top tech professionals
New
Scaler
Explore Offerings by SCALER
exit-intent-icon

Download Interview guide PDF

Before you leave, take this Multithreading Interview Questions interview guide with you.
Get a Free Personalized Career Roadmap
Answer 4 simple questions about you and get a path to a lucrative career
expand-icon Expand in New Tab
/ Interview Guides / Multithreading Interview Questions

Multithreading Interview Questions

Last Updated: May 22, 2026

Download PDF


Your requested download is ready!
Click here to download.
Certificate included
About the Speaker
What will you Learn?
Register Now

What do you mean by Multithreading? Why is it important?

Multithreading means multiple threads and is considered one of the most important features of Java. As the name suggests, it is the ability of a CPU to execute multiple threads independently at the same time but share the process resources simultaneously. Its main purpose is to provide simultaneous execution of multiple threads to utilize the CPU time as much as possible. It is a Java feature where one can subdivide the specific program into two or more threads to make the execution of the program fast and easy.

Multithreading Interview Questions in Java for Freshers

1. Explain volatile variables in Java?

A volatile variable is basically a keyword that is used to ensure and address the visibility of changes to variables in multithreaded programming. This keyword cannot be used with classes and methods, instead can be used with variables. It is simply used to achieve thread-safety. If you mark any variable as volatile, then all the threads can read its value directly from the main memory rather than CPU cache, so that each thread can get an updated value of the variable.

Create a free personalised study plan Create a FREE custom study plan
Get into your dream companies with expert guidance
Get into your dream companies with expert..
Real-Life Problems
Prep for Target Roles
Custom Plan Duration
Flexible Plans

2. What is Thread in Java?

Threads are basically the lightweight and smallest unit of processing that can be managed independently by a scheduler. Threads are referred to as parts of a process that simply let a program execute efficiently with other parts or threads of the process at the same time. Using threads, one can perform complicated tasks in the easiest way. It is considered the simplest way to take advantage of multiple CPUs available in a machine. They share the common address space and are independent of each other. 

3. What are the two ways of implementing thread in Java?

There are basically two ways of implementing thread in java as given below: 

  • Extending the Thread class

Example:

class MultithreadingDemo extends Thread 
{   
  public void run() 
 {   
     System.out.println("My thread is in running state.");    
 } 
  public static void main(String args[]) 
 {   
    MultithreadingDemo obj=new MultithreadingDemo();  
        obj.start();  
  }  
} 

Output:

My thread is in running state.
  • Implementing Runnable interface in Java

Example:  

class MultithreadingDemo implements Runnable 
{  
   public void run() 
 {  
      System.out.println("My thread is in running state.");  
  }  
    public static void main(String args[]) 
 {  
      MultithreadingDemo obj=new MultithreadingDemo();   
      Threadtobj =new Thread(obj);       tobj.start();  
 }   
} 

Output: 

My thread is in running state. 
You can download a PDF version of Multithreading Interview Questions.

Download PDF


Your requested download is ready!
Click here to download.

4. What's the difference between thread and process?

Thread: It simply refers to the smallest units of the particular process. It has the ability to execute different parts (referred to as thread) of the program at the same time.  

Process: It simply refers to a program that is in execution i.e., an active program. A process can be handled using PCB (Process Control Block). 

Thread Process
It is a subset of a subunit of a process. It is a program in execution containing multiple threads.
In this, inter-thread communication is faster, less expensive, easy and efficient because threads share the same memory address of the process they belong to.  In this, inter-process communication is slower, expensive, and complex because each process has different memory space or address.,
These are easier to create, lightweight, and have less overhead.  These are difficult to create, heavyweight, and have more overhead.
It requires less time for creation, termination, and context switching. It requires more time for creation, termination, and context switching.
Processes with multiple threads use fewer resources. Processes without threads use more resources.
Threads are parts of a process, so they are dependent on each other but each thread executes independently. Processes are independent of each other.
There is a need for synchronization in threads to avoid unexpected scenarios or problems. There is no need for synchronization in each process.
They share data and information with each other.  They do not share data with each other. 

5. What’s the difference between class lock and object lock?

Class Lock: In java, each and every class has a unique lock usually referred to as a class level lock. These locks are achieved using the keyword ‘static synchronized’ and can be used to make static data thread-safe. It is generally used when one wants to prevent multiple threads from entering a synchronized block. 

Example:  

public class ClassLevelLockExample  
{    
  public void classLevelLockMethod()  
 {       
     synchronized (ClassLevelLockExample.class)  
       {         
            //DO your stuff here       
       }    
 } 
} 

Object Lock: In java, each and every object has a unique lock usually referred to as an object-level lock. These locks are achieved using the keyword ‘synchronized’ and can be used to protect non-static data. It is generally used when one wants to synchronize a non-static method or block so that only the thread will be able to execute the code block on a given instance of the class.  

Example:  

public class ObjectLevelLockExample  
{    
  public void objectLevelLockMethod()  
 {   
     synchronized (this)  
       {     
            //DO your stuff here   
       } 
 }
} 
Explore InterviewBit’s Exclusive Live Events
Explore Exclusive Events
By
No More Events to show!
No More Events to show!
No More Events to show!
No More Events to show!
Certificate included
About the Speaker
What will you Learn?
Register Now

6. What's the difference between User thread and Daemon thread?

User and Daemon are basically two types of thread used in Java by using a ‘Thread Class’.  

User Thread (Non-Daemon Thread): In Java, user threads have a specific life cycle and its life is independent of any other thread. JVM (Java Virtual Machine) waits for any of the user threads to complete its tasks before terminating it. When user threads are finished, JVM terminates the whole program along with associated daemon threads. 

Daemon Thread: In Java, daemon threads are basically referred to as a service provider that provides services and support to user threads. There are basically two methods available in thread class for daemon thread: setDaemon() and isDaemon(). 

User Thread vs Daemon Thread

User Thread Daemon Thread 
JVM waits for user threads to finish their tasks before termination.  JVM does not wait for daemon threads to finish their tasks before termination.
These threads are normally created by the user for executing tasks concurrently.  These threads are normally created by JVM.
They are used for critical tasks or core work of an application.  They are not used for any critical tasks but to do some supporting tasks.
These threads are referred to as high-priority tasks, therefore are required for running in the foreground.  These threads are referred to as low priority threads, therefore are especially required for supporting background tasks like garbage collection, releasing memory of unused objects, etc. 

7. How can we create daemon threads?

We can create daemon threads in java using the thread class setDaemon(true). It is used to mark the current thread as daemon thread or user thread. isDaemon() method is generally used to check whether the current thread is daemon or not. If the thread is a daemon, it will return true otherwise it returns false.  
Example:   
Program to illustrate the use of setDaemon() and isDaemon() method. 

public class DaemonThread extends Thread 
{ 
   public DaemonThread(String name){ 
       super(name); 
   } 
   public void run() 
   {  
       // Checking whether the thread is Daemon or not 
       if(Thread.currentThread().isDaemon()) 
       {  
           System.out.println(getName() + " is Daemon thread");  
       }    
       else 
       {  
           System.out.println(getName() + " is User thread");  
       }  
   }   
   public static void main(String[] args) 
   {  
       DaemonThread t1 = new DaemonThread("t1"); 
       DaemonThread t2 = new DaemonThread("t2"); 
       DaemonThread t3 = new DaemonThread("t3");  
       // Setting user thread t1 to Daemon 
       t1.setDaemon(true);       
       // starting first 2 threads  
       t1.start();  
       t2.start();   
       // Setting user thread t3 to Daemon 
       t3.setDaemon(true);  
       t3.start();         
   }  
} 

Output:  

t1 is Daemon thread 
t3 is Daemon thread 
t2 is User thread 

But one can only call the setDaemon() method before start() method otherwise it will definitely throw IllegalThreadStateException as shown below:   

public class DaemonThread extends Thread 
public void run() 
   { 
       System.out.println("Thread name: " + Thread.currentThread().getName()); 
       System.out.println("Check if its DaemonThread: "  
                       + Thread.currentThread().isDaemon()); 
   } 
   public static void main(String[] args) 
   { 
       DaemonThread t1 = new DaemonThread(); 
       DaemonThread t2 = new DaemonThread(); 
       t1.start();         
       // Exception as the thread is already started 
       t1.setDaemon(true); 
       t2.start(); 
   } 
} 

Output:  

Thread name: Thread-0 
Check if its DaemonThread: false 
Start Your Coding Journey With Tracks Start Your Coding Journey With Tracks
Master Data Structures and Algorithms with our Learning Tracks
Master Data Structures and Algorithms
Topic Buckets
Mock Assessments
Reading Material
Earn a Certificate

8. What are the wait() and sleep() methods?

wait(): As the name suggests, it is a non-static method that causes the current thread to wait and go to sleep until some other threads call the notify () or notifyAll() method for the object’s monitor (lock). It simply releases the lock and is mostly used for inter-thread communication. It is defined in the object class, and should only be called from a synchronized context. 

Example:  

synchronized(monitor) 
{ 
monitor.wait();       Here Lock Is Released by Current Thread  
} 

sleep(): As the name suggests, it is a static method that pauses or stops the execution of the current thread for some specified period. It doesn’t release the lock while waiting and is mostly used to introduce pause on execution. It is defined in thread class, and no need to call from a synchronized context.  

Example:  

synchronized(monitor) 
{ 
Thread.sleep(1000);     Here Lock Is Held by The Current Thread 
//after 1000 milliseconds, the current thread will wake up, or after we call that is interrupt() method 
} 

9. What’s the difference between notify() and notifyAll()?

notify(): It sends a notification and wakes up only a single thread instead of multiple threads that are waiting on the object’s monitor.

notifyAll(): It sends notifications and wakes up all threads and allows them to compete for the object's monitor instead of a single thread. 

10. Why wait(), notify(), and notifyAll() methods are present in Object class?

We know that every object has a monitor that allows the thread to hold a lock on the object. But the thread class doesn't contain any monitors. Thread usually waits for the object’s monitor (lock) by calling the wait() method on an object, and notify other threads that are waiting for the same lock using notify() or notifyAll() method.  Therefore, these three methods are called on objects only and allow all threads to communicate with each that are created on that object.

11. What is Runnable and Callable Interface? Write the difference between them.

Both the interfaces are generally used to encapsulate tasks that are needed to be executed by another thread. But there are some differences between them as given below: 

Running Interface: This interface is basically available in Java right from the beginning. It is simply used to execute code on a concurrent thread.  
Callable Interface: This interface is basically a new one that was introduced as a part of the concurrency package. It addresses the limitation of runnable interfaces along with some major changes like generics, enum, static imports, variable argument method, etc. It uses generics to define the return type of object.   

public interface Runnable  
{   
  public abstract void run(); 
}  
public interface Callable<V>  
{    
V call() throws Exception;  
} 

Runnable Interface vs Callable Interface

Runnable Interface Callable Interface 
It does not return any result and therefore, cannot throw a checked exception.  It returns a result and therefore, can throw an exception.
It cannot be passed to invokeAll method.  It can be passed to invokeAll method.
It was introduced in JDK 1.0. It was introduced in JDK 5.0, so one cannot use it before Java 5. 
It simply belongs to Java.lang. It simply belongs to java.util.concurrent. 
It uses the run() method to define a task. It uses the call() method to define a task. 
To use this interface, one needs to override the run() method.  To use this interface, one needs to override the call() method.

12. What is the start() and run() method of Thread class?

start(): In simple words, the start() method is used to start or begin the execution of a newly created thread. When the start() method is called, a new thread is created and this newly created thread executes the task that is kept in the run() method. One can call the start() method only once.  

run(): In simple words, the run() method is used to start or begin the execution of the same thread. When the run() method is called, no new thread is created as in the case of the start() method. This method is executed by the current thread. One can call the run() method multiple times. 

Discover your path to a   Discover your path to a   Successful Tech Career for FREE! Successful Tech Career!
Answer 4 simple questions & get a career plan tailored for you
Answer 4 simple questions & get a career plan tailored for you
Interview Process
CTC & Designation
Projects on the Job
Referral System
Try It Out
2 Lakh+ Roadmaps Created

13. Explain thread pool?

A Thread pool is simply a collection of pre-initialized or worker threads at the start-up that can be used to execute tasks and put back in the pool when completed. It is referred to as pool threads in which a group of fixed-size threads is created.  By reducing the number of application threads and managing their lifecycle, one can mitigate the issue of performance using a thread pool. Using threads, performance can be enhanced and better system stability can occur. To create the thread pools, java.util.concurrent.Executors class usually provides factory methods.

14. What’s the purpose of the join() method?

join() method is generally used to pause the execution of a current thread unless and until the specified thread on which join is called is dead or completed. To stop a thread from running until another thread gets ended, this method can be used. It joins the start of a thread execution to the end of another thread’s execution. It is considered the final method of a thread class.

15. What do you mean by garbage collection?

Garbage collection is basically a process of managing memory automatically. It uses several GC algorithms among which the popular one includes Mark and Sweep. The process includes three phases i.e., marking, deletion, and compaction/copying. In simple words, a garbage collector finds objects that are no longer required by the program and then delete or remove these unused objects to free up the memory space.

16. Explain the meaning of the deadlock and when it can occur?

Deadlock, as the name suggests, is a situation where multiple threads are blocked forever. It generally occurs when multiple threads hold locks on different resources and are waiting for other resources to complete their task.

The above diagram shows a deadlock situation where two threads are blocked forever.  Thread 1 is holding Object 1 but needs object 2 to complete processing whereas Thread 2 is holding Object 2 but needs object 1 first. In such conditions, both of them will hold lock forever and will never complete tasks.

17. What are the benefits of using Multithreading?

There are various benefits of multithreading as given below:

  • Allow the program to run continuously even if a part of it is blocked. 
  • Improve performance as compared to traditional parallel programs that use multiple processes. 
  • Allows to write effective programs that utilize maximum CPU time
  • Improves the responsiveness of complex applications or programs. 
  • Increase use of CPU resources and reduce costs of maintenance. 
  • Saves time and parallelism tasks. 
  • If an exception occurs in a single thread, it will not affect other threads as threads are independent. 
  • Less resource-intensive than executing multiple processes at the same time. 

18. How do threads communicate with each other?

Threads can communicate using three methods i.e., wait(), notify(), and notifyAll().

19. Can two threads execute two methods (static and non-static concurrently)?

Yes, it is possible. If both the threads acquire locks on different objects, then they can execute concurrently without any problem.

20. What is the purpose of the finalize() method?

Finalize() method is basically a method of Object class specially used to perform cleanup operations on unmanaged resources just before garbage collection. It is not at all intended to be called a normal method. After the complete execution of finalize() method, the object gets destroyed automatically.

Multithreading Interview Questions in Java for Experienced

1. What is ConcurrentHashMap and Hashtable? In java, why is ConcurrentHashMap considered faster than Hashtable?

ConcurrentHashMap: It was introduced in Java 1.5 to store data using multiple buckets. As the name suggests, it allows concurrent read and writes operations to the map. It only locks a certain portion of the map while doing iteration to provide thread safety so that other readers can still have access to the map without waiting for iteration to complete.  

Hashtable: It is a thread-safe legacy class that was introduced in old versions of java to store key or value pairs using a hash table.  It does not provide any lock-free read, unlike ConcurrentHashMap. It just locks the entire map while doing iteration. 

ConcurrentHashMap and Hashtable, both are thread-safe but ConcurrentHashMap generally avoids read locks and improves performance, unlike Hashtable. ConcurrentHashMap also provides lock-free reads, unlike Hashtable. Therefore, ConcurrentHashMap is considered faster than Hashtable especially when the number of readers is more as compared to the number of writers. 

2. Explain thread priority.

Thread priority simply means that threads with the highest priority will get a chance for execution prior to low-priority threads. One can specify the priority but it's not necessary that the highest priority thread will get executed before the lower-priority thread. Thread scheduler assigns processor to thread on the basis of thread priority. The range of priority changes between 1-10 from lowest priority to highest priority. 

3. What do you mean by the ThreadLocal variable in Java?

ThreadLocal variables are special kinds of variables created and provided by the Java ThreadLocal class. These variables are only allowed to be read and written by the same thread. Two threads cannot be able to see each other’s ThreadLocal variable, so even if they will execute the same code, then there won't be any race condition and the code will be thread-safe.  

Example:  

public class ThreadLocalExp   
{   
     public static class MyRunnable implements Runnable    
   {   
       private ThreadLocal<Integer> threadLocal =   
              new ThreadLocal<Integer>();   
      @Override   
       public void run() {   
           threadLocal.set( (int) (Math.random() * 50D) );   
           try    
           {   
               Thread.sleep(1000);   
           } catch (InterruptedException e) {   
           }   
           System.out.println(threadLocal.get());   
       }   
   }   
   public static void main(String[] args)    
   {   
       MyRunnable runnableInstance = new MyRunnable();    
       Thread t1 = new Thread(runnableInstance);   
       Thread t2 = new Thread(runnableInstance);   
      // this will call run() method    
       t1.start();   
       t2.start();   
   }   
} 

Output

10 
33 
10 33 

4. What is semaphore?

Semaphore is regarded as a thread synchronization construct that is usually required to control and manage the access to the shared resource using counters. It simply sets the limit of the thread. The semaphore class is defined within the package java.util.concurrent and can be used to send signals between threads to avoid missed signals or to guard critical sections. It can also be used to implement resource pools or bounded collection.

5. Explain Thread Group. Why should we not use it?

ThreadGroup is a class that is used to create multiple groups of threads in a single object. This group of threads is present in the form of three structures in which every thread group has a parent except the initial thread. Thread groups can contain other thread groups also. A thread is only allowed to have access to information about its own thread group, not other thread groups. 

Previously in the old version of Java, the only functionality that did not work without a thread group was uncaughtException( Thread t, Throwable e). But now in Java 5 versions, there is Thread.setUncaughtExceptionHandler(UncaughtExceptionHandler). So now even that works without thread groups and therefore, there is no need to use thread groups.  

t1.setUncaughtExceptionHandler(new UncaughtExceptionHandler() 
{ 
@Override  
public void uncaughtException(Thread t, Throwable e)  
{  
System.out.println("exception occured:"+e.getMessage()); 
}  
}; 

6. What is the ExecutorService interface?

ExecutorService interface is basically a sub-interface of Executor interface with some additional methods or features that help in managing and controlling the execution of threads. It enables us to execute tasks asynchronously on threads.

Example: 

import java.util.concurrent.ExecutorService;   
import java.util.concurrent.Executors;   
import java.util.concurrent.TimeUnit;   
  
public class TestThread {   
                                   public static void main(final String[] arguments) throws InterruptedException {   
ExecutorService e = Executors.newSingleThreadExecutor();   
 
     try {   
       e.submit(new Thread());   
        System.out.println("Shutdown executor");   
        e.shutdown();   
        e.awaitTermination(5, TimeUnit.SECONDS);   
  } catch (InterruptedException ex) {   
       System.err.println("tasks interrupted");   
  } finally {   
  
        if (!e.isTerminated()) {   
           System.err.println("cancel non-finished tasks");   
     }   
        e.shutdownNow();   
        System.out.println("shutdown finished");   
  }   
  }   
  
  static class Task implements Runnable {   
        
     public void run() {   
          
        try {   
        Long duration = (long) (Math.random() * 20);   
           System.out.println("Running Task!");   
           TimeUnit.SECONDS.sleep(duration);   
     } catch (InterruptedException ex) {   
           ex.printStackTrace();   
     }   
  }   
 }          
}   

Output:

Shutdown executor 
shutdown finished

7. What will happen if we don’t override the thread class run() method?

Nothing will happen as such if we don’t override the run() method. The compiler will not show any error. It will execute the run() method of thread class and we will just don’t get any output because the run() method is with an empty implementation. 

Example:  

class MyThread extends Thread { 
  //don't override run() method 
} 
public class DontOverrideRun { 
  public static void main(String[] args) { 
         System.out.println("Started Main."); 
         MyThread thread1=new MyThread(); 
      thread1.start(); 
         System.out.println("Ended Main."); 
  } 
} 

Output: 

Started Main. 
Ended Main.  

8. What is the lock interface? Why is it better to use a lock interface rather than a synchronized block.?

Lock interface was introduced in Java 1.5 and is generally used as a synchronization mechanism to provide important operations for blocking.  

Advantages of using Lock interface over Synchronization block: 

  • Methods of Lock interface i.e., Lock() and Unlock() can be called in different methods. It is the main advantage of a lock interface over a synchronized block because the synchronized block is fully contained in a single method.  
  • Lock interface is more flexible and makes sure that the longest waiting thread gets a fair chance for execution, unlike the synchronization block.

9. Is it possible to call the run() method directly to start a new thread?

No, it's not possible at all. You need to call the start method to create a new thread otherwise run method won't create a new thread. Instead, it will execute in the current thread.

10. Is it possible that each thread can have its stack in multithreaded programming?

Of course, it is possible. In multithreaded programming, each thread maintains its own separate stack area in memory because of which every thread is independent of each other rather than dependent.

11. What is busy spinning?

Busy Spinning, also known as Busy-waiting, is a technique in which one thread waits for some condition to happen, without calling wait or sleep methods and releasing the CPU. In this condition, one can pause a thread by making it run an empty loop for a certain time period, and it does not even give CPY control. Therefore, it is used to preserve CPU caches and avoid the cost of rebuilding cache.

12. What is the synchronization process? Why use it?

Synchronization is basically a process in java that enables a simple strategy for avoiding thread interference and memory consistency errors. This process makes sure that resource will be only used one thread at a time when one thread tries to access a shared resource. It can be achieved in three different ways as given below: 

  • By the synchronized method
  • By synchronized block
  • By static synchronization

Syntax:  

synchronized (object) 
{        
   //statement to be synchronized 
} 

13. What is synchronized method and synchronized block? Which one should be preferred?

Synchronized Method: In this method, the thread acquires a lock on the object when they enter the synchronized method and releases the lock either normally or by throwing an exception when they leave the method.  No other thread can use the whole method unless and until the current thread finishes its execution and release the lock. It can be used when one wants to lock on the entire functionality of a particular method. 

Synchronized Block: In this method, the thread acquires a lock on the object between parentheses after the synchronized keyword, and releases the lock when they leave the block. No other thread can acquire a lock on the locked object unless and until the synchronized block exists. It can be used when one wants to keep other parts of the programs accessible to other threads.
 
Synchronized blocks should be preferred more as it boosts the performance of a particular program. It only locks a certain part of the program (critical section) rather than the entire method and therefore leads to less contention.

14. What is thread starvation?

Thread starvation is basically a situation or condition where a thread won’t be able to have regular access to shared resources and therefore is unable to proceed or make progress. This is because other threads have high priority and occupy the resources for too long. This usually happens with low-priority threads that do not get CPU for its execution to carry on. 

15. What is Livelock? What happens when it occurs?

Similar to deadlock, livelock is also another concurrency problem. In this case, the state of threads changes between one another without making any progress. Threads are not blocked but their execution is stopped due to the unavailability of resources.

16. What is BlockingQueue?

BlockingQueue basically represents a queue that is thread-safe. Producer thread inserts resource/element into the queue using put() method unless it gets full and consumer thread takes resources from the queue using take() method until it gets empty. But if a thread tries to dequeue from an empty queue, then a particular thread will be blocked until some other thread inserts an item into the queue, or if a thread tries to insert an item into a queue that is already full, then a particular thread will be blocked until some threads take away an item from the queue. 

Example

package org.arpit.java2blog; 
 
import java.util.concurrent.ArrayBlockingQueue; 
import java.util.concurrent.BlockingQueue; 
 
public class BlockingQueuePCExample { 
 
   public static void main(String[] args) { 
 
       BlockingQueue<String> queue=new ArrayBlockingQueue<>(5); 
       Producer producer=new Producer(queue); 
       Consumer consumer=new Consumer(queue); 
       Thread producerThread = new Thread(producer); 
       Thread consumerThread = new Thread(consumer); 
 
       producerThread.start(); 
       consumerThread.start(); 
 
   } 
 
   static class Producer implements Runnable { 
 
       BlockingQueue<String> queue=null; 
 
       public Producer(BlockingQueue queue) { 
           super(); 
           this.queue = queue; 
       } 
 
       @Override 
       public void run() { 
 
               try { 
                   System.out.println("Producing element 1"); 
                   queue.put("Element 1"); 
                   Thread.sleep(1000); 
                   System.out.println("Producing element 2"); 
                   queue.put("Element 2"); 
                   Thread.sleep(1000); 
                   System.out.println("Producing element 3"); 
                   queue.put("Element 3"); 
               } catch (InterruptedException e) { 
 
                   e.printStackTrace(); 
               } 
       } 
   } 
 
   static class Consumer implements Runnable { 
 
       BlockingQueue<String> queue=null; 
 
       public Consumer(BlockingQueue queue) { 
           super(); 
           this.queue = queue; 
       } 
 
       @Override 
       public void run() { 
 
           while(true) 
           { 
               try { 
                   System.out.println("Consumed "+queue.take()); 
               } catch (InterruptedException e) { 
                   e.printStackTrace(); 
               } 
           } 
       } 
 
   } 
} 

Output

Producing element 1 
Consumed Element 1 
Producing element 2 
Consumed Element 2 
Producing element 3 
Consumed Element 3

17. Can you start a thread twice?

No, it's not at all possible to restart a thread once a thread gets started and completes its execution. Thread only runs once and if you try to run it for a second time, then it will throw a runtime exception i.e., java.lang.IllegalThreadStateException. 

Example

public class TestThreadTwice1 extends Thread{   
public void run(){   
System.out.println(" thread is executing now........");   
}   
public static void main(String args[]){   
TestThreadTwice1 t1=new TestThreadTwice1();   
t1.start();   
t1.start();   
}   
}   

Output:

thread is executing now........ 
Exception in thread "main" java.lang.IllegalThreadStateException  

18. Explain context switching.

Context switching is basically an important feature of multithreading. It is referred to as switching of CPU from one thread or process to another one. It allows multiple processes to share the same CPU. In context switching, the state of thread or process is stored so that the execution of the thread can be resumed later if required. 

19. What is CyclicBarrier and CountDownLatch?

CyclicBarrier and CountDownLatch, both are required for managing multithreaded programming. But there is some difference between them as given below: 

CyclicBarrier: It is a tool to synchronize threads processing using some algorithm. It enables a set of threads to wait for each other till they reach a common execution point or common barrier points, and then let them further continue execution. One can reuse the same CyclicBarrier even if the barrier is broken by setting it. 

CountDownLatch: It is a tool that enables main threads to wait until mandatory operations are performed and completed by other threads. In simple words, it makes sure that a thread waits until the execution in another thread completes before it starts its execution. One cannot reuse the same CountDownLatch once the count reaches 0. 

20. What do you mean by inter-thread communication?

Inter-thread communication, as the name suggests, is a process or mechanism using which multiple threads can communicate with each other. It is especially used to avoid thread polling in java and can be obtained using wait(), notify(), and notifyAll() methods. 

21. What is Thread Scheduler and Time Slicing?

Thread Scheduler: It is a component of JVM that is used to decide which thread will execute next if multiple threads are waiting to get the chance of execution. By looking at the priority assigned to each thread that is READY, the thread scheduler selects the next run to execute. To schedule the threads, it mainly uses two mechanisms: Preemptive Scheduling and Time slicing scheduling.  

Time Slicing: It is especially used to divide CPU time and allocate them to active threads. In this, each thread will get a predefined slice of time to execute. When the time expires, a particular thread has to wait till other threads get their chances to use their time in a round-robin fashion. Every running thread will get executed for a fixed time period. 

22. What is a shutdown hook?

A shutdown hook is simply a thread that is invoked implicitly before JVM shuts down. It is one of the most important features of JVM because it provides the capacity to do resource cleanup or save application state JVM shuts down.  By calling the halt(int) method of the Runtime class, the shutdown hook can be stopped. Using the following method, one can add a shutdown hook. 

public void addShutdownHook(Thread hook){}     
Runtime r=Runtime.getRuntime();   
r.addShutdownHook(new MyThread());

Conclusion

1. Conclusion

Overall, multithreading is a very essential part of Java and modern software development. It is very helpful in making the program more efficient and also reduces the usage of storage resources. In this article, we have discussed important interview questions related to multithreading along with answers that were asked mostly in the Interviews and will help you to crack your interviews.

Recommended Tutorials:
Practice
Java Developer Skills

C++ Multithreading Interview Questions

1. How do you create, join, and detach threads in C++11? What is std::jthread (C++20)?

std::thread is the standard way to create a thread in C++11. A thread starts executing immediately when the object is constructed, taking a callable and any arguments it needs:

std::thread t(processRequest, requestId);

Once a thread is running, the creating thread has two options for managing its lifetime. join() blocks the calling thread until the spawned thread finishes, making it the safe default when the completion of the thread matters:

t.join();

detach() separates the thread from its std::thread object, allowing it to run independently in the background. Once detached, the thread cannot be joined and its lifetime is no longer managed by the object:

t.detach();

The most dangerous pitfall with std::thread is destroying the thread object without calling either join() or detach(). If the destructor runs on a joinable thread, the standard mandates that std::terminate() is called, crashing the program immediately. This can happen silently when an exception is thrown between thread creation and the join call, which is why wrapping joins in RAII wrappers is important.

std::jthread, introduced in C++20, addresses this directly. It automatically calls join() in its destructor, making it impossible to accidentally terminate the program by forgetting to join. Beyond RAII safety, std::jthread supports cooperative cancellation through std::stop_token. The thread function receives a stop token and can periodically check whether a stop has been requested, allowing clean early termination without shared flags or condition variables:

std::jthread t([](std::stop_token token) {
    while (!token.stop_requested()) {
        doWork();
    }
});
t.request_stop();

The thread checks stop_requested() on each iteration and exits cleanly when a stop is signaled. For any new C++20 code, std::jthread is the safer default. std::thread remains relevant for codebases where C++20 is not available.

2. What is std::mutex? How does std::lock_guard differ from std::unique_lock in C++?

std::mutex provides mutual exclusion for shared data, ensuring only one thread can execute a protected section at a time. Calling lock() acquires the mutex and unlock() releases it. Manually managing these calls is error-prone since any exception between them leaves the mutex permanently locked, which is why both lock_guard and unique_lock exist as RAII wrappers.

Here is the difference between them.

lock_guard unique_lock
Locks the mutex immediately when the object is created Also locks immediately by default, but supports deferred locking
Cannot unlock manually before the scope ends Supports manual unlock and relock inside the same scope
Lightweight and meant for simple scoped locking More flexible, but slightly heavier internally
Cannot be used with condition_variable Works directly with condition_variable
Best when a lock only needs basic RAII protection Better for complex synchronization flows

lock_guard acquires the mutex on construction and releases it when it goes out of scope. It cannot be manually unlocked, transferred, or used with a condition variable. For the majority of cases where a lock just needs to protect a scope, it is the right choice:

std::mutex mtx;
void updateBalance(int amount) {
    std::lock_guard<std::mutex> lock(mtx);
    balance += amount;
}

unique_lock offers the same RAII guarantee but adds the flexibility needed in more complex scenarios. It supports deferred locking, manual unlock and relock mid-scope, timed acquisition with try_lock_for(), and is required by std::condition_variable:

std::unique_lock<std::mutex> lock(mtx, std::defer_lock);
lock.lock();
balance += amount;
lock.unlock();

Manually unlocking in the middle of a method can help when there’s a long non-critical operation between two critical sections, so the lock is not held longer than it needs to be.

For scenarios where the same thread must acquire the same mutex multiple times within a call chain, std::recursive_mutex allows re-acquisition without deadlocking. Each lock() call must be matched by an unlock(), and the mutex is only fully released when the lock count returns to zero.

3. What is std::condition_variable in C++? Explain the wait/notify pattern and spurious wakeups.

std::condition_variable allows one thread to block and wait until another thread signals that a condition has been met. It always works alongside a std::mutex because the act of checking a condition and waiting on it must be atomic to avoid missing a notification.

A producer-consumer queue is a common example where one thread adds work while another thread waits to process it. The consumer waits until data is available and the producer notifies after adding work to the queue:

std::mutex mtx;
std::condition_variable cv;
std::queue<Task> taskQueue;

void producer(Task t) {
    std::lock_guard<std::mutex> lock(mtx);
    taskQueue.push(t);
    cv.notify_one();
}

void consumer() {
    std::unique_lock<std::mutex> lock(mtx);
    cv.wait(lock, [] { return !taskQueue.empty(); });
    Task t = taskQueue.front();
    taskQueue.pop();
}

When wait() is called, it atomically releases the mutex and suspends the thread. When notified, the thread reacquires the mutex and resumes. This atomic release-and-suspend is what prevents the race condition where a notification arrives after the condition check but before the thread actually sleeps.

Spurious wakeups

The operating system can wake a waiting thread for reasons unrelated to any notify_one() or notify_all() call. This is called a spurious wakeup and is an intentional allowance in the POSIX threading standard. A thread that wakes spuriously and proceeds without rechecking the condition will operate on stale or missing data.

The two-argument form of wait() handles this automatically by re-evaluating the predicate every time the thread wakes and going back to sleep if the condition is still false. Using the single-argument form without a predicate requires a manual while loop to achieve the same protection, which is more error-prone and should be avoided.

notify_one() wakes a single waiting thread, which is sufficient when only one thread should handle the available work. notify_all() wakes every waiting thread, which is appropriate when a state change is relevant to all waiters, such as a shutdown signal.

std::condition_variable_any works with any lockable type rather than requiring std::unique_lock specifically. It is more flexible but carries slightly higher overhead, making std::condition_variable the better default when unique_lock is already in use.

4. What is std::atomic in C++? How does it achieve thread safety without locks?

std::atomic wraps a type and guarantees that operations on it are indivisible, meaning no thread can observe the value in a partially modified state. For types that map to native CPU instructions, this is achieved without any mutex, making atomic operations significantly faster than lock-protected alternatives under high contention.

The mechanism behind this is Compare-And-Swap, or CAS. Before writing a new value, the CPU compares the current value in memory against an expected value. If they match, the write proceeds. If they do not match, another thread has modified the value since it was last read, and the operation retries. This entire sequence happens as a single uninterruptible hardware instruction:

std::atomic<int> counter{0};
counter.fetch_add(1, std::memory_order_relaxed);

std::atomic<bool> ready{false};
ready.store(true, std::memory_order_release);

std::atomic_flag is the most primitive atomic type, guaranteed lock-free on every platform. It supports only test_and_set() and clear(), making it the building block for simple spinlocks when a full mutex is too heavy.

Memory ordering

The memory order argument controls how the compiler and CPU are allowed to reorder operations around the atomic access, which is where most of the nuance lies.

memory_order_relaxed applies no ordering constraints beyond atomicity itself. It is suitable for independent counters where the order of operations relative to other variables does not matter. memory_order_acquire and memory_order_release work as a pair to synchronize a producer and consumer. A store with release makes all preceding writes visible to a thread that performs a load with acquire on the same variable. memory_order_seq_cst is the default and enforces a single total ordering of all atomic operations across all threads, which is the easiest to reason about but the most expensive on architectures that require explicit memory fence instructions.

Atomics usually perform better when the work is limited to updating a single shared variable, especially under high contention, because the operation can often be handled directly by CPU-level atomic instructions. But once multiple values need to be updated together in a consistent way, a mutex is still the safer and more appropriate choice since atomics do not coordinate grouped operations on their own.

5. What is std::future and std::promise in C++? How does std::async work?

std::promise and std::future form a one-time communication channel between two threads. The promise is the producer side that sets a value or exception into a shared state, and the future is the consumer side that retrieves it. Once a value is set, it cannot be changed, and once retrieved, the channel is consumed.

Instead of blocking the main thread immediately, the work can run separately and return a result once it completes:

std::promise<int> promise;
std::future<int> future = promise.get_future();

std::thread worker([&promise] {
    int result = computeHeavyWork();
    promise.set_value(result);
});

int result = future.get();
worker.join();

future.get() blocks until the value is available and returns it. If the worker threw an exception instead of setting a value, get() rethrows that exception in the calling thread, making error propagation across threads clean and explicit.

std::async simplifies this pattern by launching a function asynchronously and returning a future automatically without manually creating a promise or thread:

std::future<int> result = std::async(std::launch::async, computeHeavyWork);
int value = result.get();

The launch policy controls how the function is executed. std::launch::async guarantees the function runs on a new thread immediately. std::launch::deferred delays execution until get() or wait() is called, running the function lazily on the calling thread rather than spawning a new one. Passing both policies together lets the runtime decide, which is convenient but unpredictable in latency-sensitive code.

std::packaged_task sits between promise and async. It wraps a callable and gives it a future without launching it immediately, allowing the task definition to be separated from when and where it is scheduled:

std::packaged_task<int()> task(computeHeavyWork);
std::future<int> future = task.get_future();

std::thread t(std::move(task));
t.join();

int result = future.get();

In these cases, tasks are created first and then queued, transferred between threads, or executed later through a custom executor instead of running right away.

6. How do you implement a thread-safe producer-consumer queue in C++?

std::queue provides no thread safety on its own. Concurrent pushes and pops without synchronization corrupt the internal structure silently, producing wrong results without any crash or error. Making it thread-safe requires a mutex for mutual exclusion and a condition variable to coordinate between producers and consumers efficiently without busy-waiting.

The complete implementation wraps the queue, mutex, and condition variable together:

template<typename T>
class ThreadSafeQueue {
    std::queue<T> queue;
    std::mutex mtx;
    std::condition_variable cv;
    size_t maxSize;
public:
    ThreadSafeQueue(size_t max) : maxSize(max) {}

    void push(T item) {
        std::unique_lock<std::mutex> lock(mtx);
        cv.wait(lock, [this] { return queue.size() < maxSize; });
        queue.push(std::move(item));
        cv.notify_all();
    }

    T pop() {
        std::unique_lock<std::mutex> lock(mtx);
        cv.wait(lock, [this] { return !queue.empty(); });
        T item = std::move(queue.front());
        queue.pop();
        cv.notify_all();
        return item;
    }
};

The producer acquires the lock, checks whether the queue has space, pushes the item, and notifies waiting consumers. The consumer acquires the lock, checks whether the queue has items, pops one, and notifies waiting producers. Both sides use the two-argument form of wait() with a predicate, which handles spurious wakeups automatically by rechecking the condition every time the thread wakes before deciding whether to proceed.

notify_all() is used here rather than notify_one() because both producers and consumers are waiting on the same condition variable. If only notify_one() were used after a pop, there is a risk of waking another consumer rather than an unblocked producer, leaving the producer waiting unnecessarily. Using notify_all() ensures both sides are re-evaluated on every state change.

This bounded queue pattern is the foundation of most thread pool implementations. Worker threads sit in a loop calling pop(), blocking when the queue is empty and waking when a task arrives. The submitting thread calls push(), blocking when the pool's task queue is full and waking when a worker frees up space. Bounding the queue is important in production because an unbounded queue under a slow consumer allows memory to grow without limit until the process is killed.

Java Multithreading Interview Questions

1. What is a ReentrantLock in Java? When should you prefer it over synchronized?

ReentrantLock is an explicit locking mechanism from java.util.concurrent.locks that provides the same mutual exclusion guarantee as synchronized but with significantly more control over how and when locks are acquired. Like synchronized, it is reentrant, meaning a thread that already holds the lock can acquire it again without deadlocking itself.

Since the lock is not released automatically, the unlock call is usually placed inside finally:

ReentrantLock lock = new ReentrantLock();

lock.lock();
try {
    // critical section
} finally {
    lock.unlock();
}

The main advantage of ReentrantLock is the extra control it gives around lock handling.tryLock() attempts to acquire the lock without blocking and returns immediately with a boolean result, which allows a thread to back off and do something else rather than waiting indefinitely:

if (lock.tryLock(500, TimeUnit.MILLISECONDS)) {
    try {
        // acquired
    } finally {
        lock.unlock();
    }
} else {
    // handle timeout
}

lockInterruptibly() allows a thread waiting for a lock to be interrupted, which is impossible with synchronized. This is valuable in systems where threads need to be cancelled cleanly while blocked on lock acquisition.

Fair mode, enabled by passing true to the constructor, ensures threads acquire the lock in the order they requested it, preventing thread starvation in high-contention scenarios. synchronized makes no ordering guarantee.

For read-heavy workloads where multiple threads can safely read simultaneously, ReentrantReadWriteLock allows concurrent reads while still serializing writes, which improves throughput significantly over a standard exclusive lock.

For most normal cases, you can just stick with synchronized since it keeps things straightforward and handles locking automatically. ReentrantLock can be used when you know you need extra features like timeout-based locking, interrupt handling, or finer control over thread access.

2. What is the Java Thread lifecycle? Explain all thread states with transitions.

Java threads move between multiple execution states, like RUNNABLE, WAITING, and BLOCKED, during their lifecycle. These states are exposed through the Thread.State enum.

A thread starts in the NEW state the moment it is created, but before start() is called. Calling start() moves it to RUNNABLE, which covers both threads that are actively executing on a CPU core and threads that are ready to run but waiting for the scheduler to assign them one. From the JVM's perspective, both situations look the same.

From RUNNABLE, a thread can move into one of three waiting states depending on what causes it to pause. BLOCKED means the thread is waiting to acquire a monitor lock that another thread currently holds. It is sitting at the entrance of a synchronized block or method and will move back to RUNNABLE the moment the lock is released. WAITING means the thread has voluntarily paused indefinitely by calling Object.wait(), Thread.join(), or LockSupport.park(). It will stay there until another thread explicitly wakes it up. TIMED_WAITING is the same concept but with a defined timeout, entered through Thread.sleep(n), Object.wait(n), or Thread.join(n).

BLOCKED and WAITING threads may both look inactive in a thread dump, but they usually point to very different problems. A BLOCKED thread is waiting to get access to a lock that another thread is already using. A WAITING thread has paused on purpose and will stay there until some other thread signals it to continue. It is important to understand the difference while debugging because one usually points to lock contention, while the other is part of normal thread coordination.

Once execution completes or an unhandled exception occurs, the thread moves to TERMINATED. The current state of any thread can be inspected at runtime using thread.getState(), which returns the Thread.State enum value directly.

3. What is the difference between synchronized method and synchronized block in Java?

Both synchronized methods and synchronized blocks use the same underlying monitor locking mechanism. Still, they differ in scope and the object they lock on, which has direct performance implications in high-concurrency code.

A synchronized method locks the entire method body. For instance methods, the lock is acquired on this, the current object instance. For static methods, the lock is acquired on the Class object itself. Every thread that wants to enter any synchronized method on the same instance must acquire the same lock, meaning unrelated operations inside the class block each other unnecessarily:

public synchronized void updateBalance(int amount) {
    this.balance += amount;
}

A synchronized block locks only the section of code that actually needs protection, and the lock object can be anything, not just this. This allows different parts of a class to synchronize on different objects independently, reducing contention significantly:

public void updateBalance(int amount) {
    synchronized (this.balanceLock) {
        this.balance += amount;
    }
}

Here, only the critical section is locked, and other threads can execute non-synchronized parts of the same method concurrently. In classes with multiple independent shared resources, separate lock objects per resource allow truly parallel access rather than serializing everything through a single monitor.

Java's synchronization is reentrant, meaning a thread that already holds a monitor lock can re-enter any block synchronized on the same object without deadlocking itself. This matters when a synchronized method calls another synchronized method on the same instance.

A common concurrency issue that most developers run into is double-checked locking during lazy initialization. Without declaring the field volatile, the compiler and CPU can reorder the write to the field relative to the constructor call, causing another thread to observe a partially constructed object even after the null check passes. Declaring the field volatile prevents this reordering and makes the pattern safe.

4. What is ExecutorService in Java? How does it differ from creating new Thread() manually?

Creating a new thread with a new Thread() for every task is straightforward but expensive in production. Thread creation involves OS-level resource allocation, stack memory assignment, and scheduler registration. Once the application starts handling traffic, constantly creating and destroying threads for every task can become more expensive than the task execution itself.

ExecutorService solves this by maintaining a pool of reusable threads. Tasks are submitted to the pool and executed by an available thread, which returns to the pool when the task completes rather than being destroyed. This eliminates per-task thread creation overhead and gives the application predictable, bounded concurrency.

The four most commonly used pool types serve different workload patterns:

ExecutorService fixed    = Executors.newFixedThreadPool(4);
ExecutorService cached   = Executors.newCachedThreadPool();
ExecutorService single   = Executors.newSingleThreadExecutor();
ScheduledExecutorService scheduled = Executors.newScheduledThreadPool(2);

newFixedThreadPool creates a fixed number of threads and queues tasks when all threads are busy, making it suitable for CPU-bound workloads where you want to match thread count to available cores. newCachedThreadPool creates threads on demand and reuses idle ones, which works well for short-lived I/O tasks but can create unbounded threads under sustained load. newSingleThreadExecutor guarantees sequential execution across all submitted tasks. newScheduledThreadPool supports delayed and recurring task execution.

Submitting tasks uses either execute() or submit() depending on whether a return value is needed:

executor.execute(() -> doWork());
Future<String> future = executor.submit(() -> computeResult());
String result = future.get();

execute() accepts a Runnable and returns nothing. submit() accepts either a Runnable or Callable and returns a Future that can be used to retrieve the result or catch exceptions from the task.

Shutting down an executor correctly is important and frequently missed. Calling shutdown() stops accepting new tasks but waits for submitted tasks to complete. awaitTermination() blocks until all tasks finish or a timeout expires:

executor.shutdown();
executor.awaitTermination(30, TimeUnit.SECONDS);

CompletableFuture works with ExecutorService and is commonly used when async tasks become difficult to manage with plain Future objects. It supports chaining tasks together, handling results without blocking the main thread, and combining multiple async operations without repeatedly calling Future.get().

5. What is the difference between volatile and atomic variables in Java?

volatile and atomic variables both address thread safety but they solve different problems, and confusing them is a common source of subtle concurrency bugs.

volatile makes updates written by one thread visible to other threads immediately. When one thread writes to a volatile field, that write is immediately visible to all other threads rather than being cached in a CPU register or local thread cache. Without volatile, threads can read stale values from their own cache without ever seeing updates made by other threads. One thread can update a shared shutdown flag while other threads keep checking it:

private volatile boolean running = true;

public void stop() {
    running = false;
}

This works correctly because there is a single writer and multiple readers, and no thread needs to read the current value before writing a new one. However volatile provides no atomicity guarantee. A read-modify-write operation like counter++ is actually three steps: read the value, increment it, write it back. Two threads executing this simultaneously on a volatile field can both read the same value, both increment it, and both write back the same result, losing one increment entirely.

This is exactly what AtomicInteger solves. Atomic classes use Compare-And-Swap operations at the CPU level to perform read-modify-write sequences as a single uninterruptible operation:

AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet();

If two threads attempt incrementAndGet simultaneously, one will succeed and the other will retry automatically until its CAS succeeds. No lock is needed and no increment is lost.

If threads only need to see the latest value, volatile is often enough. Once the operation involves updating the current value, atomic classes are usually the better option.

6. What is ConcurrentHashMap in Java? How does it differ from HashMap and Hashtable?

ConcurrentHashMap is the standard thread-safe map implementation in Java and understanding why it exists requires knowing what breaks with the alternatives under concurrent access.

HashMap is not thread-safe. When multiple threads write to it simultaneously, the internal structure can become permanently corrupted. Concurrent writes during a resize operation can create circular references in a bucket, causing threads to spin in an infinite loop. This is not a race condition that occasionally produces wrong results, it permanently breaks the map.

Hashtable is thread-safe but synchronizes every method on the same lock. Every read and every write blocks all other operations on the entire map regardless of which keys are involved, making it a bottleneck under any meaningful concurrency.

ConcurrentHashMap takes a fundamentally different approach. Reads require no locking and complete without blocking any writer. Writes use CAS operations for uncontested inserts and node-level synchronization only when bucket-level contention actually occurs. Two threads writing to different keys never block each other, and readers never block writers.

Null keys and null values are not permitted. Since get() returning null is indistinguishable from a missing key in a concurrent context, allowing nulls would make atomic conditional operations unreliable.

The atomic compound operations are where ConcurrentHashMap provides the most practical value in production code:

map.computeIfAbsent(key, k -> new ArrayList<>()).add(value);
map.merge(key, 1, Integer::sum);

computeIfAbsent initializes a value only if the key is absent without requiring an external null check. merge reads the existing value and combines it with a new one atomically, which is the correct way to implement concurrent counters without a separate AtomicInteger.

Collections.synchronizedMap() wraps any map with a single lock on every operation, making it equivalent to Hashtable in concurrency behavior and suitable only when the specific map implementation matters more than concurrent performance.

7. What are CountDownLatch and CyclicBarrier in Java? Provide a real use case for each.

Both CountDownLatch and CyclicBarrier coordinate multiple threads at a synchronization point, but they differ in who is waiting, who is signaling, and whether the barrier can be reused.

1. CountDownLatch

A CountDownLatch is initialized with a count and provides two operations. Threads that need to wait call await() and block until the count reaches zero. Other threads call countDown() to decrement the count as they complete their work. The latch is one-directional and cannot be reset once it reaches zero.

A practical use case is an integration test that needs several services to be ready before assertions run:

CountDownLatch latch = new CountDownLatch(3);
startService("database", latch);
startService("cache", latch);
startService("messaging", latch);
latch.await();
runTests();

Each service calls countDown() once it finishes initializing. The test thread blocks on await() until all three services are ready, then proceeds. The waiting thread and the signaling threads are entirely separate.

2. CyclicBarrier

A CyclicBarrier makes all participating threads wait for each other at a defined point before any of them can continue. Every thread calls await() and blocks until all parties have arrived, at which point all of them are released simultaneously. Unlike CountDownLatch, a CyclicBarrier resets automatically after each cycle and can be reused.

Imagine splitting work across multiple threads where nobody can start phase two until all threads finish phase one:

CyclicBarrier barrier = new CyclicBarrier(4);
for (int i = 0; i < 4; i++) {
    executor.submit(() -> {
        computePhaseOne(partition);
        barrier.await();
        computePhaseTwo(partition);
    });
}

All four threads process their partition of data in phase one, meet at the barrier, and only begin phase two once every thread has finished phase one. The barrier resets automatically for the next phase.

CountDownLatch is usually used when one set of threads needs to wait until some other threads finish their work, and once the count reaches zero, it cannot be reused. CyclicBarrier works differently because all participating threads wait for each other at the same point, and the barrier can be reused across multiple execution cycles.

8. What is ThreadLocal in Java? What are the memory leak risks in thread pools?

ThreadLocal provides each thread with its own isolated copy of a variable. Rather than sharing a single instance across threads, every thread that accesses a ThreadLocal gets its own independent value that no other thread can see or modify. This makes it useful for carrying per-request context through a call stack without passing it explicitly through every method signature.

One thread handling a request may need access to the current user across multiple service layers without passing it around everywhere:

public class RequestContext {
    private static final ThreadLocal<User> currentUser =
        ThreadLocal.withInitial(() -> null);
    public static void set(User user) { currentUser.set(user); }
    public static User get() { return currentUser.get(); }
    public static void clear() { currentUser.remove(); }
}

At the start of each request, the authenticated user is stored in the ThreadLocal. Any service or repository in the call chain can retrieve it without any additional wiring.

In a simple application where threads are created per request and discarded afterward, ThreadLocal values are cleaned up naturally when the thread terminates. Thread pools break this assumption entirely. A thread in a pool is never destroyed between requests, it simply picks up the next task. If the previous request stored a value in a ThreadLocal and never cleaned it up, the next request handled by the same thread will see that stale value from a completely different user or session.

This is not just a correctness problem. If the stored value holds a reference to a large object graph, that memory can never be reclaimed because the thread remains alive indefinitely holding the reference through its ThreadLocal map.

The fix is always calling remove() after the request completes, placed in a finally block or a framework-level after-request hook to guarantee it runs regardless of exceptions:

try {
    RequestContext.set(authenticatedUser);
    handleRequest();
} finally {
    RequestContext.clear();
}

Multithreading Scenario-Based Interview Questions

1. You suspect a deadlock in your production Java service. How do you diagnose and resolve it?

Deadlock in a production service typically surfaces as threads that stop making progress without any exception or error in the logs. Request timeouts start accumulating, thread pool queues fill up, and the service becomes unresponsive while the JVM itself stays alive. The first step is confirming the suspicion with a thread dump.

Step 1 - Taking a thread dump

You can select from these three options depending on what is available in the environment:

jstack <pid>
jcmd <pid> Thread.print

jstack and jcmd both produce a snapshot of every thread's current state and stack trace. VisualVM provides the same output through a GUI and highlights deadlocked threads automatically.

Step 2 - Reading the thread dump

A deadlock shows up as a circular chain of BLOCKED threads where each thread is waiting for a lock held by the next thread in the chain. The output makes this explicit:

Thread-1: BLOCKED waiting to lock <0x000000076b3a2f40> held by Thread-2
Thread-2: BLOCKED waiting to lock <0x000000076b3a1e80> held by Thread-1

Thread-1 holds one lock and wants another. Thread-2 holds that other lock and wants the first one back. Neither can proceed. The JVM also prints a deadlock summary at the bottom of the dump that identifies the involved threads directly.

Step 3 - Fixing the root cause

The most common cause is acquiring multiple locks in inconsistent order across different code paths. Thread-1 acquires lock A then lock B while Thread-2 acquires lock B then lock A. Enforcing a global lock acquisition order, always acquiring A before B everywhere in the codebase, eliminates this entirely.

Sometimes consistent lock ordering is hard to maintain across a large codebase. In those cases, tryLock() with a timeout is often safer than waiting forever on a lock. A thread that cannot acquire the second lock within the timeout releases the first and retries after a backoff period, breaking the circular wait:

if (lockA.tryLock(100, TimeUnit.MILLISECONDS)) {
    try {
        if (lockB.tryLock(100, TimeUnit.MILLISECONDS)) {
            try {
                // critical section
            } finally { lockB.unlock(); }
        }
    } finally { lockA.unlock(); }
}

Redesigning with concurrent data structures like ConcurrentHashMap often removes the need for explicit locking entirely, which is the cleanest resolution when the data access pattern allows it.

Step 4 - Prevention

Static analysis tools like FindBugs and its successor SpotBugs can detect potential lock ordering violations at compile time before they reach production. It is also worth distinguishing deadlock from two related conditions. Livelock occurs when threads keep responding to each other without making progress, similar to two people repeatedly stepping aside for each other in a corridor. Starvation occurs when a thread is perpetually denied lock access because higher-priority threads keep acquiring it first. Both produce symptoms similar to deadlock but require different fixes.

2. Implement the Producer-Consumer problem in Java using BlockingQueue. Why is this preferred?

The producer-consumer problem requires a producer to add items to a shared buffer and a consumer to take them, with both sides blocking when the buffer is full or empty respectively. Implementing this correctly with wait() and notify() requires careful handling of spurious wakeups, lock ownership, and notification timing. BlockingQueue handles all of this internally, reducing the implementation to two method calls.

BlockingQueue<Task> queue = new ArrayBlockingQueue<>(100);

class Producer implements Runnable {
    public void run() {
        while (true) {
            Task task = generateTask();
            queue.put(task);
        }
    }
}

class Consumer implements Runnable {
    public void run() {
        while (true) {
            Task task = queue.take();
            process(task);
        }
    }
}

put() blocks the producer when the queue is at capacity and resumes automatically when a consumer takes an item. take() blocks the consumer when the queue is empty and resumes when a producer adds one. The internal synchronization, condition checking, and spurious wakeup handling are all managed by the queue implementation.

ArrayBlockingQueue is bounded, meaning its capacity is fixed at construction time. This creates natural backpressure on the producer, preventing it from outpacing the consumer and consuming unbounded memory. LinkedBlockingQueue can be constructed with or without a capacity bound. Without a bound it accepts items indefinitely, which risks out-of-memory errors if the producer consistently outpaces the consumer.

Graceful shutdown

The cleanest shutdown pattern is the poison pill, where the producer puts a sentinel value into the queue that the consumer recognizes as a signal to stop:

static final Task POISON_PILL = new Task();
queue.put(POISON_PILL);

Task task = queue.take();
if (task == POISON_PILL) return;

For multi-consumer setups, one poison pill per consumer thread is needed since each take() only removes one item. Alternatively, managing producers and consumers through an ExecutorService and calling shutdown() followed by awaitTermination() handles lifecycle without a sentinel value.

3. A multithreaded application produces intermittently incorrect results without crashing. What is the root cause and how do you fix it?

Intermittent incorrect results without a crash are the signature of a race condition. The application is not broken in a way that throws exceptions or corrupts memory visibly, it is broken in a way that only manifests when thread scheduling produces a specific interleaving of operations. Because the bug depends on timing, it disappears under a debugger, passes in low-load tests, and surfaces unpredictably in production.

Understanding the root cause

The underlying cause is shared mutable state being accessed by multiple threads without synchronization. A read-modify-write sequence like reading a counter, incrementing it, and writing it back appears atomic in source code but compiles into multiple instructions. Two threads executing this sequence simultaneously can both read the same value, both compute the same incremented result, and both write it back, losing one update entirely. The program continues running with a silently wrong value.

Investigation process

The first step is identifying which shared variables are involved. Adding thread-ID logging around every access to shared state reveals whether multiple threads are interleaving reads and writes on the same data:

System.out.println(Thread.currentThread().getId() + " reading: " + sharedValue);

In Java, running with async-profiler or reviewing all fields that lack volatile, synchronized, or atomic wrappers systematically surfaces the candidates. In C++, ThreadSanitizer detects data races at runtime by instrumenting memory accesses and flagging unsynchronized concurrent reads and writes:

g++ -fsanitize=thread -g -o app app.cpp
./app

ThreadSanitizer reports the exact lines involved in the race, the thread IDs, and the access types, which eliminates the guesswork entirely.

Why these bugs are hard to reproduce

Race conditions are timing-sensitive. The window where two threads can interleave incorrectly is often measured in nanoseconds. Running under a debugger serializes execution enough to close that window, making the bug disappear. Higher load, a different CPU core count, or a different OS scheduler can open or close the window unpredictably, which is why the bug appears intermittently rather than consistently.

The happens-before relationship in the Java memory model defines when a write by one thread is guaranteed to be visible to a read by another. Without a synchronization action establishing happens-before between the write and the read, the JVM and CPU are free to reorder and cache operations in ways that make a write invisible to another thread even after it has completed.

Fixes

The right fix depends on what the shared state needs to do. For simple counters and flags, atomic variables eliminate the race without any locking:

AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet();

For broader critical sections involving multiple variables that must be updated together consistently, synchronization is the correct tool. For state that does not actually need to be shared between threads, ThreadLocal eliminates sharing entirely by giving each thread its own independent copy. Making shared objects immutable after construction is the strongest guarantee since immutable objects require no synchronization at all.

4. How would you design a thread-safe rate limiter supporting 1,000 RPS per user?

A rate limiter that supports 1,000 requests per second per user needs to track request counts per user, enforce the limit atomically under concurrent access, and reset or replenish allowances at a defined rate. The token bucket algorithm is the most practical starting point for this.

Token bucket approach

Each user has a bucket that holds up to 1,000 tokens. Every request consumes one token. A background process refills the bucket at a fixed rate. If the bucket is empty, the request is rejected. The bucket cap prevents burst accumulation beyond the defined limit.

The data structure that makes this thread-safe without explicit locking is a ConcurrentHashMap of AtomicLong values, one per user:

ConcurrentHashMap<String, AtomicLong> buckets = new ConcurrentHashMap<>();

public boolean allowRequest(String userId) {
    AtomicLong tokens = buckets.computeIfAbsent(userId, 
        k -> new AtomicLong(1000));
    
    long current = tokens.get();
    if (current <= 0) return false;
    return tokens.compareAndSet(current, current - 1);
}

computeIfAbsent initializes the bucket atomically the first time a user is seen. The CAS operation inside allowRequest reads the current token count and decrements it only if the value has not changed since the read. If another thread consumed a token between the read and the write, the CAS fails and the caller retries or rejects the request. This avoids synchronized blocks entirely while remaining correct under high concurrency.

Refilling is handled by a ScheduledExecutorService that resets each bucket to its maximum capacity on a fixed interval:

ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
scheduler.scheduleAtFixedRate(() -> {
    buckets.forEach((userId, tokens) -> tokens.set(1000));
}, 0, 1, TimeUnit.SECONDS);

Sliding window alternative

For stricter per-second enforcement, a sliding window tracks the timestamps of recent requests in a ConcurrentLinkedQueue. When a new request arrives, timestamps older than one second are removed and the queue size determines whether the request is within the limit. This is more accurate than a fixed refill cycle but consumes more memory per user.

Distributed rate limiting

A single-node implementation breaks down in a multi-instance deployment since each instance maintains its own in-memory state and users can exceed the limit by routing requests across instances. The standard solution is moving the token count to Redis and using a Lua script to perform the check-and-decrement atomically on the Redis side. Lua scripts in Redis execute as a single atomic operation, providing the same CAS guarantee as AtomicLong but across all instances simultaneously:

local tokens = redis.call('GET', KEYS[1])
if tonumber(tokens) > 0 then
    redis.call('DECR', KEYS[1])
    return 1
end
return 0

The tradeoff is network latency on every request check, which is why the local AtomicLong approach remains preferable for single-node deployments where the round-trip cost to Redis would add more latency than the rate limiting saves.

5. Explain a real scenario where a ReadWriteLock is better than a regular mutex.

A regular mutex serializes all access to shared data, meaning reads block other reads even though concurrent reads are entirely safe. In read-heavy workloads this becomes a significant bottleneck since threads spend most of their time waiting for locks held by other threads that are only reading, not modifying anything.

A ReadWriteLock separates these two access patterns. Multiple threads can hold the read lock simultaneously because concurrent reads do not conflict. The write lock is exclusive, blocking all readers and other writers until the modification is complete.

A configuration store is a practical example where this difference matters. An application that serves thousands of requests per second reads the configuration on every request but updates it only when an operator pushes a change, perhaps a few times per day. With a regular mutex, every read blocks every other read, serializing thousands of concurrent threads through a single lock for no safety benefit. With a ReadWriteLock, all reads proceed in parallel and only the rare write causes any blocking.

In Java, ReentrantReadWriteLock provides the two locks through separate accessors:

java

ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();

public Config getConfig(String key) {
    rwLock.readLock().lock();
    try {
        return configMap.get(key);
    } finally {
        rwLock.readLock().unlock();
    }
}

public void updateConfig(String key, Config value) {
    rwLock.writeLock().lock();
    try {
        configMap.put(key, value);
    } finally {
        rwLock.writeLock().unlock();
    }
}

In C++17, std::shared_mutex provides the same pattern using std::shared_lock for reads and std::unique_lock for writes.

One important limitation in Java is that a read lock cannot be upgraded to a write lock atomically. A thread holding the read lock that needs to perform a write must release the read lock first, then acquire the write lock. Another thread can acquire the write lock in that window, meaning the data may have changed by the time the original thread gets the write lock. The correct approach is to acquire the write lock directly when a modification is anticipated rather than starting with a read lock and attempting to upgrade it.

MCQs on Multithreading in Java

1.

Multithreading is defined as...

2.

Name the method that is used to register a thread in a thread scheduler.

3.

When the thread is considered dead?

4.

How can a thread be created in java multi-threading?

5.

Which of the following is not a valid constructor of the Thread class?

6.

Predict the output of the following code:

class MyThread extends Thread  
{ 
  public void run()  
   {         
       System.out.println("Running");   
   } 
} 
class ThreadTest {
   public static void main(String args[]) throws InterruptedException  
        {         
             Runnable r = new MyThread(); // #1         
               Thread myThread = new Thread(r); // #2         
               myThread.start();     
       } 
}
7.

What is thread priority in Java?

8.

Which of the following method needs to be defined by a class that is implementing java.lang.Runnable interface?

9.

Name the method of the thread that is called before the run() method and carries out initialization.

10.

In the following java program, what is the name of the thread?

class multithreaded_programing 
 { 
   public static void main(String args[]) 
   { 
         Thread t = Thread.currentThread(); 
         System.out.println(t);         
   } 
  }
Excel at your interview with Masterclasses Know More
Certificate included
What will you Learn?
Free Mock Assessment
Fill up the details for personalised experience.
Phone Number *
OTP will be sent to this number for verification
+91 *
+91
Change Number
Graduation Year *
Graduation Year *
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
*Enter the expected year of graduation if you're student
Current Employer
Company Name
College you graduated from
College/University Name
Job Title
Job Title
Engineering Leadership
Software Development Engineer (Backend)
Software Development Engineer (Frontend)
Software Development Engineer (Full Stack)
Data Scientist
Android Engineer
iOS Engineer
Devops Engineer
Support Engineer
Research Engineer
Engineering Intern
QA Engineer
Co-founder
SDET
Product Manager
Product Designer
Backend Architect
Program Manager
Release Engineer
Security Leadership
Database Administrator
Data Analyst
Data Engineer
Non Coder
Other
Please verify your phone number
Edit
Resend OTP
By clicking on Start Test, I agree to be contacted by Scaler in the future.
Already have an account? Log in
Free Mock Assessment
Instructions from Interviewbit
Start Test