Multithreading: Essential Coding Questions for Interviews

·

9 min read

Multithreading is a programming concept that enables concurrent execution of multiple threads within a single process, improving performance and responsiveness. It is widely used in Java, Python, and other languages to handle tasks like parallel processing and resource sharing efficiently.

Understanding multithreading is crucial for optimizing applications and avoiding issues like race conditions and deadlocks. In interviews, Multithreading plays a crucial role so let us understand some of the programming questions on Multithreading.

  1. What is a thread in Java?

A thread is the smallest unit of execution in a process. In Java, a thread is an independent path of execution that allows multiple operations to be performed concurrently.

2. How can you create a thread in Java?

There are two ways to create a thread in Java:

  1. Extending Thread class
class MyThread extends Thread {
    public void run() {
        System.out.println("Thread is running...");
    }
}
public class Main {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        t1.start();
    }
}

2. Implementing Runnable interface

class MyRunnable implements Runnable {
    public void run() {
        System.out.println("Thread is running...");
    }
}
public class Main {
    public static void main(String[] args) {
        Thread t1 = new Thread(new MyRunnable());
        t1.start();
    }
}

3. What is the difference between start() and run() methods?

  • start(): Creates a new thread and calls run() method in a separate thread.

  • run(): Executes in the main thread if directly called instead of a separate thread.

class MyThread extends Thread {
    public void run() {
        System.out.println("Run method executed by: " + Thread.currentThread().getName());
    }
}
public class Main {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        t1.run();  // Runs in main thread (wrong way)
        t1.start(); // Runs in a new thread (correct way)
    }
}

4. What is a deadlock? How do you prevent it?

Deadlock occurs when two or more threads hold locks that the other needs, causing them to wait indefinitely.

This can be prevented by following strategies like:

  • Locking resources in a consistent order.

  • Using tryLock() with a timeout.

  • Using higher-level concurrency utilities like ExecutorService or CountDownLatch.

class DeadlockExample {
    static final Object lock1 = new Object();
    static final Object lock2 = new Object();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (lock1) {
                synchronized (lock2) {
                    System.out.println("Thread 1 acquired both locks.");
                }
            }
        });

        Thread t2 = new Thread(() -> {
            synchronized (lock2) {
                synchronized (lock1) {
                    System.out.println("Thread 2 acquired both locks.");
                }
            }
        });

        t1.start();
        t2.start();
    }
}

5. Create a simple program that simulates a deadlock situation. The program should have two threads that try to acquire two locks in a different order, leading to a deadlock.

class A {
    synchronized void methodA(B b) {
        System.out.println("Thread 1: Holding lock A...");
        try { Thread.sleep(1000); } catch (InterruptedException e) {}
        b.last();
    }
    synchronized void last() {
        System.out.println("Inside A's last method");
    }
}

class B {
    synchronized void methodB(A a) {
        System.out.println("Thread 2: Holding lock B...");
        try { Thread.sleep(1000); } catch (InterruptedException e) {}
        a.last();
    }
    synchronized void last() {
        System.out.println("Inside B's last method");
    }
}

6. Print Even and Odd Numbers Using Two Threads

Write a program to print even and odd numbers from 1 to 100 using two threads. One thread should print even numbers, and the other should print odd numbers.

public class EvenOddThreadSafe {
 public static void main(String[] args) {
  EvenThread even = new EvenThread();
  OddThread odd = new OddThread();
  even.start();
  odd.start();

  try {
   even.join();
   odd.join();
  } catch (InterruptedException e) {
   System.out.println(e);
  }
 }
}

class EvenThreadSafe extends Thread {
 private static final Object lock = new Object(); // Lock object to synchronize

 public void run() {
  for (int i = 2; i <= 100; i += 2) {
   synchronized (lock) {
    System.out.println("Even : " + i);
   }
  }
 }
}

class OddThreadSafe extends Thread {
 private static final Object lock = new Object(); // Lock object to synchronize

 public void run() {
  for (int i = 1; i < 100; i += 2) {
   synchronized (lock) {
    System.out.println(("Odd : " + i));
   }

  }
 }
}

/* 

Why are we using join in this program ?

The join() method is used in this program to ensure that 
the main thread waits for both the even and odd threads 
to complete before it exits. 

Without join(), the main thread might finish executing and exit 
before the even and odd threads are done printing their respective numbers. 
In that case, the program may exit while the threads are still running, 
causing the program to terminate before the output is complete.

*/

7. Producer-Consumer Problem

The Producer-Consumer problem is a classic example of a multi-threaded problem where two threads (producer and consumer) share a common resource, typically a buffer or queue.

The producer thread generates data (produces), while the consumer thread consumes that data. The main challenge is to synchronize the producer and consumer threads to ensure the data is produced and consumed safely and efficiently, without race conditions.


import java.util.LinkedList;
import java.util.Queue;

class Sharedqueue {
    private Queue<Integer> queue = new LinkedList<>();
    private final int MAX_CAPACITY = 5; // Max queue size

    // Producer method to add items to the queue
    public synchronized void produce(int item) throws InterruptedException {
        while (queue.size() == MAX_CAPACITY) {
            wait(); // Wait if queue is full
        }
        queue.offer(item); // Add item to the queue
        System.out.println("Produced: " + item);
        notify(); // Notify the consumer that there is data to consume
    }

    // Consumer method to consume items from the queue
    public synchronized int consume() throws InterruptedException {
        while (queue.isEmpty()) {
            wait(); // Wait if queue is empty
        }
        int item = queue.poll(); // Remove item from the queue
        System.out.println("Consumed: " + item);
        notify(); // Notify the producer that there is space to produce more items
        return item;
    }
}

class Producer implements Runnable {
    private Sharedqueue queue;

    public Producer(Sharedqueue queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        try {
            int item = 1;
            while (true) {
                queue.produce(item++);
                Thread.sleep(1000); // Simulate time taken to produce an item
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

class Consumer implements Runnable {
    private Sharedqueue queue;

    public Consumer(Sharedqueue queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        try {
            while (true) {
                queue.consume();
                Thread.sleep(1500); // Simulate time taken to consume an item
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

public class ProducerConsumerExample {
    public static void main(String[] args) {
        Sharedqueue queue = new Sharedqueue();

        Thread producerThread = new Thread(new Producer(queue));
        Thread consumerThread = new Thread(new Consumer(queue));

        producerThread.start();
        consumerThread.start();
    }
}

8. Implement a Simple Thread Pool

Write a program that implements a simple thread pool (using ExecutorService) to execute multiple tasks concurrently.

public class SimpleThreadPool {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(3);

        for (int i = 0; i < 10; i++) {
            final int taskId = i;
            executorService.submit(() -> {
                System.out.println("Executing task: " + taskId + " in thread " + Thread.currentThread().getName());
            });
        }
        executorService.shutdown();
    }
}

9. Thread-safe Singleton Design Pattern

Problem: Implement the Singleton design pattern with thread safety. Ensure that only one instance of the class is created even when multiple threads are trying to access it.

public class Singleton {
    private static volatile Singleton instance;

    private Singleton() {
        // Private constructor to prevent instantiation
    }

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

/*
Why are we using volatile keyword in singleton? 

The volatile keyword in the Singleton class is used to ensure that 
the instance of the Singleton class is properly visible across all threads.

when one thread creates the Singleton instance (inside the getInstance() 
method), other threads might not see the updated value of instance immediately.

This is because, in Java, the value of a variable can be cached by 
individual threads, and it might not be immediately visible to other threads, 
leading to potential issues where multiple threads could create separate 
instances of the Singleton. 

The volatile keyword solves this problem by ensuring visibility of the 
instance variable across all threads.

*/

10. Print Fibonacci Sequence Using Two Threads

Problem: Write a program to print the Fibonacci sequence up to a specified limit using two threads. One thread should print the Fibonacci numbers, and the other thread should print their indices.

class Fibonacci {
    private int currentIndex = 0;
    private int prev1 = 0, prev2 = 1;

    // Synchronized method to print Fibonacci numbers
    public synchronized void printFibonacci() {
        // Calculate Fibonacci numbers and print them
        while (currentIndex < 10) {  // Limit to 10 for this example
            int fibonacciNumber;
            if (currentIndex == 0) {
                fibonacciNumber = 0;
            } else if (currentIndex == 1) {
                fibonacciNumber = 1;
            } else {
                fibonacciNumber = prev1 + prev2;
                prev1 = prev2;
                prev2 = fibonacciNumber;
            }
            System.out.println("Fibonacci Number: " + fibonacciNumber);
            currentIndex++;
            notify();  // Notify the other thread to print the index
            try {
                if (currentIndex < 10) {
                    wait();  // Wait for the other thread to print the index
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }

    // Synchronized method to print the index of the Fibonacci number
    public synchronized void printIndex() {
        while (currentIndex < 10) {  // Limit to 10 for this example
            System.out.println("Index: " + currentIndex);
            notify();  // Notify the other thread to print the Fibonacci number
            try {
                if (currentIndex < 10) {
                    wait();  // Wait for the other thread to print the Fibonacci number
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }
}

public class FibonacciThreads {
    public static void main(String[] args) {
        Fibonacci fibonacci = new Fibonacci();

        // Thread 1: Prints Fibonacci numbers
        Thread fibonacciThread = new Thread(() -> fibonacci.printFibonacci());

        // Thread 2: Prints indices of Fibonacci numbers
        Thread indexThread = new Thread(() -> fibonacci.printIndex());

        // Start both threads
        fibonacciThread.start();
        indexThread.start();

        // Wait for both threads to complete
        try {
            fibonacciThread.join();
            indexThread.join();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

11. What is the difference between Thread.sleep() and Object.wait()?

  • Thread.sleep() pauses the current thread for a specified period without releasing the lock, whereas

  • Object.wait() releases the lock and puts the current thread in a waiting state until it is notified.

12. Explain the concept of thread safety and how you can ensure thread safety in Java.

  • Thread safety refers to ensuring that shared resources or variables are accessed by only one thread at a time, preventing data corruption.

  • Thread safety can be achieved by using synchronization (e.g., synchronized keyword), volatile variables, or using concurrent collections like ConcurrentHashMap.

13. What is the difference between wait(), notify(), and notifyAll() in Java?

  • wait(): Causes the current thread to release the lock and wait until it is notified by another thread.

  • notify(): Wakes up one thread that is waiting on the object's monitor.

  • notifyAll(): Wakes up all threads that are waiting on the object's monitor.

I hope you found this article insightful and valuable. Your support means a lot to me — feel free to share your thoughts, feedback, or suggestions in the comments.

If you enjoyed reading, don’t forget to clap, comment and share it with others who might benefit. Your encouragement inspires me to create more such content. Thank you