Java Multithreading (Concurrency) Interview Questions Java Multithreading (Concurrency) Interview Questions

Page content

Comprehensive List of Java Multithreading (Concurrency) Interview Questions based on my personal interview experience over the last few years. Keep following this post link for regular updates.

Also Read Core Java Interview Questions
Also Read Java 8 Interview Questions

We can signal two threads to run alternatively using different ways in Java:

Using wait notify
  1. 1st thread print “Ping” and go to wait state.
  2. 2nd thread wakes up from wait state, print “Ping”, notify 1st thread, goes back to wait state.
  3. 1st thread wakes up from wait state, print “Pong”, notify 2nd thread, goes back to wait state.
  4. Step 2 and 3 repeats and print “Ping Pong” alternatively.
Using ReentrantLock Condition

ReentrantLock Condition provides two methods await() and signal() which works very similar to wait notify methods.

Read Ping Pong using Threads in Java for Java programs using Object’s wait-notify and ReentrantLock Condition’s await-signal.

How to run 5 threads sequentially?

Smart Answer: The whole point of having threads is to run them concurrently. If you want to run threads sequentially, better not use them.

Using Thread.join()

Let’s create a task which prints 1 to 100

class Task implements Runnable{
	int index;
	Task(int index){
		this.index = index;
	}
	@Override
	public void run() {		
		for(int i = 1; i <= 100; i++) {
			System.out.print("Thread" + index + ": " + i + " ");
		}
		System.out.println();
	}	
}

Let’s create 5 threads to run the tasks sequentially by joining the them one after the another.

Thread t1 = new Thread(new Task(1));
Thread t2 = new Thread(new Task(2));
Thread t3 = new Thread(new Task(3));
Thread t4 = new Thread(new Task(4));
Thread t5 = new Thread(new Task(5));

t1.start();
t1.join();
t2.start();
t2.join();
t3.start();
t3.join();
t4.start();
t4.join();
t5.start();
Using SingleThreadExecutor

You can create single thread executor service and submit the task 5 times to execute them sequentially.

ExecutorService service = Executors.newSingleThreadExecutor();
for(int i=1; i<= 5; i++){
  Task task = new Task(i);
  service.submit(task);
}

A Task is running in a separate thread. Stop the task if it exceeds 10 minutes.

We can use Thread.sleep() to sleep the thread for 10 minutes and Thread.interrupt() to interrupt a thread after 10 minutes.

We can interrupt a thread in three ways:-

  1. Using thread interrupt()
  2. Using Executor thread pool shutdownNow()
  3. Using Callable returned future cancel()
public class StopThreadAfterTimeout {
	
	public static void main(String[] args) throws InterruptedException, ExecutionException {
		usingThreadInterrupt();
		//usingThreadPoolShutdownNow();
		//usingFutureCancel();
	}

	private static void usingThreadInterrupt() throws InterruptedException {
		// 1. create a thread
		Thread t1 = new Thread(() -> {
			while (!Thread.currentThread().isInterrupted()) {
				// next step
			}
		});

		// 2. timeout after 10 minutes
		Thread.sleep(10 * 60 * 1000);

		// 3. stop the thread
		t1.interrupt();
	}

	private static void usingThreadPoolShutdownNow() throws InterruptedException {
		ExecutorService threadPool = Executors.newFixedThreadPool(2);
		// 1. create a thread
		threadPool.submit(() -> {
			while (!Thread.currentThread().isInterrupted()) {
				// next step
			}
		});

		// 2. timeout after 10 minutes
		Thread.sleep(10 * 60 * 1000);

		// 3. stop the thread
		threadPool.shutdownNow(); // internally call thread interrupt
	}

	private static void usingFutureCancel() throws InterruptedException, ExecutionException {
		ExecutorService service = Executors.newFixedThreadPool(2);

		// 1. submit new callable task which return future object
		Future<?> future = service.submit(() -> {
			while (!Thread.currentThread().isInterrupted()) {
				// next step
			}
		});

		// 2. wait for 10 minutes to get response
		try {
			future.get(10, TimeUnit.MINUTES);
		} catch (TimeoutException e) {
			
			// 3. stop the thread
			future.cancel(true); // internally call thread interrupt
		}
	}
}

How to decide Thread Pool Size?

It depends on the type of tasks you want to execute

  • If you are executing CPU intensive tasks such as cryptographic hash function then max thread pool size should be equal to number of cores in processor.
    For example a 4 core processor can run only 4 threads at a time so threadPoolSize = 4 is ideal for threads taking lot of CPU.
    public class IdealThreadPoolSize {
    
      public static void main(String[] args) {
        // get count of available cores
        int coreCount = Runtime.getRuntime().availableProcessors();
        ExecutorService service = Executors.newFixedThreadPool(coreCount);
    
        // submit the tasks for execution
        for (int i = 0; i < 100; i++) {
          service.execute(new CpuIntestiveTask());
        }
      }
    
      static class CpuIntestiveTask implements Runnable {
        public void run() {
          // some CPU intensive operations
        }
      }
    }
    
  • If you are executing IO intensive tasks such as Database or HTTP calls, where threads have a tendency to wait after requesting for the resources. In such case max thread pool size should be larger than the number of cores in processor.
    For example for a threadPoolSize = 20, if 16 threads are waiting for resources or blocked due to IO operations, other 4 threads can be processed concurrently by 4 core processor.
    Ideal number of threadPoolSize depends on rate of task submission and average task wait time.
    public class IdealThreadPoolSize {
    
      public static void main(String[] args) {
        // much higher count for IO tasks
        ExecutorService service = Executors.newFixedThreadPool(20);
    
        // submit the tasks for execution
        for (int i = 0; i < 100; i++) {
          service.execute(new IOTask());
        }
      }
    
      static class IOTask implements Runnable {
        public void run() {
          // some IO operations which will cause thread to block/wait
        }
      }
    }
    

ThreadPool typically place all the submitted task in blocking queue which is thread-safe. Idle threads fetch the task from the queue and process them.

What are the types of Executor Thread Pool?

  1. FixedThreadPool - Create a fixed number of threads in the beginning and place them in the pool
  2. SingleThreadExecutor - Create a pool of single thread
  3. CachedThreadPool - Create a new thread only if all the other threads are busy and place it in the pool. Ideal for lot of short lived tasks.
  4. ScheduledThreadPool - Schedule the tasks to run based on time delay (and re-trigger for fixedRate/fixedDelay). Ideal for running the tasks at regular time interval or delay.
// for fixed number of threads
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(10);

// for a pool of single thread
ExecutorService singleThreadPool = Executors.newSingleThreadExecutor();

// for lot of short lived tasks
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();

// for scheduling of tasks
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(10);

// task to run after 10 second delay
scheduledThreadPool.schedule(new Task(), 10, TimeUnit.SECONDS);

// task to run after initial delay of 15 seconds at every 10 second interval
scheduledThreadPool.scheduleAtFixedRate(new Task(), 15, 10, TimeUnit.SECONDS);

// task to run after initial delay of 15 second at every 10 second after previous task completes
scheduledThreadPool.scheduleWithFixedDelay(new Task(), 15, 10, TimeUnit.SECONDS);
Thread Pool Parameters
Thread PoolcorePoolSizemaxPoolSizekeepAliveTimeQueueType
FixedTheadPoolconstr-argsame as coreNALinkedBlockingQueue
SingleThreadExecutor11NALinkedBlockingQueue
CachedThreadPool0Integer.MAX_VALUE60sSynchronousQueue
ScheduledThreadPoolconstr-argInteger.MAX_VALUE60sDelayedWorkQueue
Custom Thread Pool
// custom thread pool (corePoolSize: 10, maxPoolSize: 100, keepAliveTime: 120s, QueueType: ArrayBlockingQueue)
ExecutorService customThreadPool = new ThreadPoolExecutor(10, 100, 120, TimeUnit.SECONDS, new ArrayBlockingQueue<>(30));

How to rename a thread in Executor Thread Pool?

You can implement the ExecutorThreadFactory to generate specific names of the threads in Executor Thread Pool

public class ExecuterTest {

	public static void main(String[] args) {
		ExecutorService service = Executors.newFixedThreadPool(5, new ExecutorThreadFactory("Test_Thread_"));
		for(int i=1; i<=5; i++){
			service.execute(new Task());
		}		
	}
}

class Task implements Runnable {

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

class ExecutorThreadFactory implements ThreadFactory {
	
	AtomicInteger counter = new AtomicInteger();
	
	String threadNamePrefix;
	
	ExecutorThreadFactory(String prefix){
		this.threadNamePrefix = prefix;
	}

	@Override
	public Thread newThread(Runnable r) {
		Thread thread = new Thread(r, threadNamePrefix + counter.incrementAndGet());
		thread.setDaemon(false);
		return thread;
	}	
}
Output
Test_Thread_1 Test_Thread_4 Test_Thread_3 Test_Thread_2 Test_Thread_5

Runnable vs Callable Interface

Runnable and Callable interfaces are used to create a task and submit to thread pool for processing.

Runnable InterfaceCallable Interface
has run() methodhas call() method
run() do not return valuecall() return value
run() cannot throw checked exceptioncall() can throw checked exception
execute Runnable task using ExecutorService execute() methodsubmit Callable task using ExecutorService submit() method
execute() do not return anythingsubmit() return Future object
public class RunnableVsCallable {

	public static void main(String[] args) throws InterruptedException, ExecutionException {
		ExecutorService service = Executors.newSingleThreadExecutor();
		
		// execute runnable task which return nothing
		service.execute(new RunnableTask());
		
		// submit callable task which return Future object
		Future<Integer> future = service.submit(new CallableTask());
		
		// get result from future object which is blocking operation
		Integer result = future.get();
		
		// shutdown the thread gracefully
		service.shutdown();
	}

	static class RunnableTask implements Runnable {

		@Override
		public void run() {
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}

	static class CallableTask implements Callable<Integer> {
		
		@Override
		public Integer call() throws InterruptedException {
			Thread.sleep(1000);
			return new Random().nextInt();
		}
	}
}

Synchronized Block vs Reentrant Lock

  • Reentrant locks are explicit, whereas Synchronized block is implicit
  • Reentrant locks provides flexibility to lock/unlock in any scopes, whereas Synchronized block scope is limited to curly braces {}
    In below example lock() is outside and unlock() is inside the try-finally block.
    private static ReentrantLock lock = new  ReentrantLock();
    
    private static void accessResource() {
      lock.lock();
      try{
        // access the resource
      }finally{
        lock.unlock();
      }
    }
    
    public static void main(String[] args){
      Thread t1 = new Thread(() -> accessResource)); t1.start();
      Thread t2 = new Thread(() -> accessResource)); t2.start();
      Thread t3 = new Thread(() -> accessResource)); t3.start();
    }
    

    The same accessResource() method using Synchronized block:

    private static void accessResource() {
      synchronized(this) {  // equivalent to lock.lock() 
        // access the resource
      }                     // equivalent to lock.unlock() 
    }
    
  • Reentrant locks lock() can be called multiple times without calling unlock() method hence the name reentrant.
    Calling lock() two times means lock inside lock. You need to call unlock() two times as well to come out of the lock completely.
  • Reentrant locks are unfair by default. You can enable fairness by passing boolean true new ReentrantLock(true) in constructor. Fairness provides more chance to acquire lock to the threads waited for longest time in the queue. Performance is slow when fairness is turned on.
  • Reentrant locks provide ability to tryLock() and tryLock(timeout)
    private static void accessResource() {
      boolean lockAcquired = lock.tryLock();
      // boolean lockAcquired = lock.tryLock(5, TimeUnit.SECONDS);
      if(lockAcquired) {
        lock.lock();
        try{
          // access the resource
        }finally{
          lock.unlock();
        }
      } else {
        // do something else
      }   
    }
    

Reentrant Lock vs ReadWrite Lock

  • ReentrantLock consist of only one lock, only one thread can acquire at a time
  • ReentrantReadWriteLock consist of ReadLock and WriteLock. Though they are two separate instances, only one will be allowed at a time.
    Either one thread can acquire write lock OR multiple threads can acquire read lock at a time.

Both ReadLock and WriteLock use the same Queue behind the scene waiting for their turn.

public class ReadWriteLock {

	private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
	private ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
	private ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
	
	private void readResource() {
		readLock.lock();
		// read the resource
		readLock.unlock();
	}
	
	private void writeResource() {
		writeLock.lock();
		// write the resource
		writeLock.unlock();
	}
	
	public static void main(String[] args) {
		ReadWriteLock obj = new ReadWriteLock();
		Thread t1 = new Thread(() -> obj.readResource()); t1.start();
		Thread t2 = new Thread(() -> obj.readResource()); t2.start();
		Thread t3 = new Thread(() -> obj.writeResource()); t3.start();
		Thread t4 = new Thread(() -> obj.writeResource()); t4.start();
		Thread t5 = new Thread(() -> obj.readResource()); t5.start();

		//t1,t2 can read at the same time  
		//t3,t4 can not write when t1,t2 are reading  
		//t4 can not write when t3 is writing  
		//t5 can not read when t3 is writing 
	}	
}

What is volatile keyword?

Each thread runs in one core of multi-core processor. Each Core has their own local cache. All the cores share one shared cache.

   thread-1       thread-2
┌―――――――――――――┐―――――――――――――┐
|   Core 1    |   Core 2    | 
|―――――――――――――|―――――――――――――|
| Local Cache | Local Cache |
|―――――――――――――┘―――――――――――――|
|       Shared Cache        |  
└―――――――――――――――――――――――――――┘

When you apply volatile keyword to a property. Any updates in that property done by thread-1 in local-cache is pushed down to shared-cache to make sure that the update is visible to thread-2

volatile boolean flag  = true;

volatile keyword doesn’t work when you do compound operations such as count++ which is read, increment and write back. In such case we can use AtomicInteger or AtomicLong

TypeUse Case
volatileFlags
AtomicInteger, AtomicLongCounters

What is ThreadLocal Object?

ThreadLocal object is used to create object per thread instances for memory efficiency and thread-safety.

In the below example, We have created a threadPool of 10 threads and submitted 1000 tasks. Here 10 ThreadLocal df objects will be created, one for each thread.

df.get() method make sure that it returns the object of the currently running thread out of those 10 df objects.

class ThreadSafeFormatter {
  public static ThreadLocal<SimpleDateFormat> df = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
}

public class UserService {
  private static ExecutorService threadPool = Executors.newFixedThreadPool(10);

  public static void main(String[] args){
    for(int i=0; i<1000; i++){
      int id = i;
      threadPool.submit(() -> new UserService().birthDate(id));
    }
  }

  public String birthDate(int userId){
    Date birthDate = birthDateFromDb(userId);
    final SimpleDateFormat df = ThreadSafeFormatter.df.get();
    return df.format(birthDate);
  }
}

What is Semaphore?

Semaphore is used when you want to limit maximum number of concurrent calls to a particular resource in multi-threaded environment.

Semaphore(3) means only 3 threads can acquire() a lock and use resources at a time. As soon as one thread out of 3 release() the lock, 4th thread can acquire() the lock and use resources.

public class SemaphoreTest {

	public static void main(String[] args) throws InterruptedException {		
		// ThreadPool of 50 threads
		ExecutorService service = Executors.newFixedThreadPool(50);
		
		// Semaphore to rum max 3 concurrent calls
		Semaphore semaphore = new Semaphore(3);
		
		// Execute 1000 tasks (initiate 1000 threads) to test
		IntStream.of(1000).forEach(i -> service.execute(new Task(semaphore)));
		
		// Shutdown the ExecutorService gracefully
		service.shutdown();
		service.awaitTermination(1, TimeUnit.MINUTES);
	}

	static class Task implements Runnable {

		private Semaphore semaphore;

		Task(Semaphore semaphore) {
			this.semaphore = semaphore;
		}

		@Override
		public void run() {		
			// Start code -> can be executed by 50 threads concurrently 
			// since thread pool size is 50

			try {
			// Only 3 threads can acquire lock at a time since semaphore count is 3
			// Other threads wait here.
			semaphore.acquire();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}

			// Slow Code -> Heavy operations like IO 
			// can be executed by only 3 threads

			// Release the lock for other threads to come in Slow code
			semaphore.release();

			// End code -> can be executed by 50 threads concurrently 
			// since thread pool size is 50
		}
	}
}

What is CountDownLatch?

CountDownLatch is used when we want our main thread to wait until all the dependent services are initialized and up.

CountDownLatch(3) means three services to be initialized before main thread. Each service do countDown() after initializing which decrement the counter. On the other hand main thread wait for all services to be initialized using await(). Once all 3 services are initialized and count reaches zero, main thread start doing its job.

public class CountDownLatchTest {

	public static void main(String[] args) throws InterruptedException{
		CountDownLatch latch = new CountDownLatch(3);

		Thread cacheService = new Thread(new Service("Cache Service", latch));
		Thread alertService = new Thread(new Service("Alert Service", latch));
		Thread validationService = new Thread(new Service("Validation Service", latch));
		
		cacheService.start();
		alertService.start();
		validationService.start();
		
		latch.await(); //wait here till latch count reaches 0 then only process next line of code.
		System.out.println("All Services are up and running. Main Thread started processing...");	
	}
	
	static class Service implements Runnable{
		
		private String name;
		private CountDownLatch latch;
		
		Service(String name, CountDownLatch latch){
			this.name = name;
			this.latch = latch;
		}

		@Override
		public void run() {
			// startup task
			System.out.println(name + " is up");
			latch.countDown();
			// continue w/ other tasks
		}		
	}
}
Output
Cache Service is up Validation Service is up Alert Service is up All Services are up and running. Main Thread started processing...

How to start 5 threads at the same time?

Well, you can have max number of parallel threads run at the same time is equal to number of cores in CPU. If you have a 4 cores CPU, only 4 threads can start at the exact same time.

Though the idea of the question is if you have any Java concurrency feature which you can use to trigger them at the same time. Answer is CountDownLatch.

Idea is to make all the threads wait until countdown reaches zero, all the threads start at the same time from that point.

public class StartThreadsAtSameTime {

	public static void main(String[] args) throws InterruptedException {
		// Initialize Countdown = 5
		CountDownLatch latch = new CountDownLatch(5);

		// Initialize and start 5 threads passing the same latch object
		for (int i = 0; i < 5; i++) {
			Thread t = new Thread(new Task(latch));
			t.start();
		}

		// All threads wait until Countdown = 0

		// Decrement the Countdown 5 times
		for (int i = 0; i < 5; i++) {
			latch.countDown();
		}

		// Countdown = 0 at this point
		// All threads start printing at this point
	}

	static class Task implements Runnable {

		private CountDownLatch latch;
		Task(CountDownLatch latch) { this.latch = latch;}

		@Override
		public void run() {
			try {
				// thread wait for Countdown = 0
				latch.await();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println(Thread.currentThread().getName() + " is running");
		}
	}
}

What is CyclicBarrier?

CyclicBarrier is used when two or more threads are required to reach at certain barrier point.

  1. They wait for each other at barrier point using await().
  2. Once all reach at barrier point, they start from there again.
  3. Since it is a CyclicBarrier, at this point, cycle is reset.
  4. All three threads wait again at next barrier point using await()
  5. Once all reach at next barrier point, they start from there again.
  6. This cycle goes on…
public class CyclicBarrierTest {

	public static void main(String[] args) {
		CyclicBarrier barrier = new CyclicBarrier(3);
		Thread t1 = new Thread(new Task(barrier));
		Thread t2 = new Thread(new Task(barrier));
		Thread t3 = new Thread(new Task(barrier));
		t1.start();
		t2.start();
		t3.start();
	}

	static class Task implements Runnable {

		private CyclicBarrier barrier;

		Task(CyclicBarrier barrier) { this.barrier = barrier;}

		@Override
		public void run() {
			while(true) {
				String threadName = Thread.currentThread().getName();
				System.out.println(threadName + " running towards barrier");
				
				try {
					barrier.await();  // wait for other threads to reach at barrier point
				} catch (InterruptedException | BrokenBarrierException e) {
					e.printStackTrace();
				}
        
				// barrier cycle is reset at this point

				System.out.println(threadName + " crossed barrier");				
				try {
					Thread.sleep(1000); // just sleep for a while to verify in console
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}		
		}
	}
}
Output
Thread-0 running towards barrier Thread-2 running towards barrier Thread-1 running towards barrier Thread-1 crossed barrier Thread-0 crossed barrier Thread-2 crossed barrier Thread-1 running towards barrier Thread-2 running towards barrier Thread-0 running towards barrier Thread-0 crossed barrier Thread-1 crossed barrier Thread-2 crossed barrier ... ... ...

Retrieve Price from N Sources Asynchronously

We can implement a Scatter Gather Pattern using CompletableFuture where we run three tasks synchronously to fetch price of a product from 3 websites.

Note that when you call CompletableFuture.runAsync(Runnable runnable) then Java runs this in a separate thread of ForkJoinPool.commonPool() thread pool by default. You do not need to create any separate thread pool.

Also note that CompletableFuture.get() method is blocking operations. We do not want our pricing result to wait forever if any website is down thatswhy we have given a timeout of 5 seconds.

We wait for them for max 5 seconds and return the prices. Two things can happen:-

  1. If all three tasks completed (run method executed) within 4 seconds, prices from all websites are returned at 4th second.
  2. If only two tasks are completed within 5 seconds, prices from only two websites are returned at 5th second.

public class ScatterGatherPattern {

	public static void main(String[] args) throws InterruptedException, ExecutionException, TimeoutException {
		ScatterGatherPattern scatterGather = new ScatterGatherPattern();
		scatterGather.getPrices(1);
	}

	private Set<Double> getPrices(int productId) throws InterruptedException, ExecutionException, TimeoutException {
		Set<Double> prices = Collections.synchronizedSet(new HashSet<>());
		
		CompletableFuture<Void> task1 = CompletableFuture.runAsync(new Task("amazon.com", productId, prices));
		CompletableFuture<Void> task2 = CompletableFuture.runAsync(new Task("ebay.com", productId, prices));
		CompletableFuture<Void> task3 = CompletableFuture.runAsync(new Task("wallmart.com", productId, prices));
		CompletableFuture<Void> allTasks = CompletableFuture.allOf(task1, task2, task3);

		// wait for all the taks to complete, but max for 5 seconds
		allTasks.get(5, TimeUnit.SECONDS); //blocking operation

		return prices;
	}

	private static class Task implements Runnable {
		private String url;
		private int productId;
		private Set<Double> set;

		Task(String url, int productId, Set<Double> set) {
			this.url = url;
			this.productId = productId;
			this.set = set;
		}

		@Override
		public void run() {
			double price = 0;
			// make http call (url, productId) to get price
			set.add(price);
		}
	}
}

What is DeadLock. How to Detect and Avoid them?

Deadlock is a situation where:-

  1. thread_1 acquires lock_A and waiting to acquire lock_B
  2. thread_2 acquires lock_B and waiting to acquire lock_A

Now both thread_1 and thread_2 will wait for each other indefinitely and result into deadlock situation.

public class DeadlockTest {
	
	private Lock lockA = new ReentrantLock();
	private Lock lockB = new ReentrantLock();

	public static void main(String[] args){
		DeadlockTest test = new DeadlockTest();
		test.execute();
	}
	
	private void execute() {
		new Thread(this::processThis).start();
		new Thread(this::processThat).start();
	}
	
	private void processThis() {
		lockA.lock();
		System.out.println(Thread.currentThread().getName() + " processing resource A");
		
		lockB.lock();
		System.out.println(Thread.currentThread().getName() + " processing resource B");
			
		lockA.unlock();
		lockB.unlock();
	}
	
	private void processThat() {
		lockB.lock();
		System.out.println(Thread.currentThread().getName() + " processing resource B");
		
		lockA.lock();
		System.out.println(Thread.currentThread().getName() + " processing resource A");
				
		lockA.unlock();
		lockB.unlock();
	}
}

How to detect a DeadLock?

Deadlock can be detected at runtime by creating Thead dump of the application which represent the state of the application at that point of time. We can use any of the following commands to get thread dumps or can use any thirdparty tool:-

kill -3 <process_id>

jstack <process_id> > ./out.txt

Alternatively we can run this program constantly in the background to check for deadlocks:-

private static void detectedDeadLock() {
	ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();
	long[] threadIds = threadBean.findDeadlockedThreads();
	boolean deadLock = threadIds != null && threadIds.length > 0;
	System.out.println("DeadLock found: " + deadLock);
}

How to avoid a DeadLock?
  1. Use timeouts while acquiring locks if possible, so that thread do not wait for indefinitely
    Lock lock = new ReentrantLock();
    lock.tryLock(2, TimeUnit.SECONDS);
    
  2. If same locks are used in different methods, try to acquire the locks in same order.
    private void processThis() {
        lockA.lock();
        lockB.lock();
        // ...
    }
    
    private void processThat() {
        lockA.lock();
        lockB.lock();
        // ...
    }