Java Multithreading Scenarios in Interviews

Loading

Multithreading is a critical topic in Java interviews, as it demonstrates your ability to handle concurrent programming challenges. Below are some common multithreading scenarios that you might encounter in Java interviews, along with explanations and example solutions.


1. Producer-Consumer Problem

  • Scenario: Implement a producer-consumer scenario where one thread produces data and another consumes it.
  • Solution: Use BlockingQueue to handle synchronization.
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

public class ProducerConsumer {
    public static void main(String[] args) {
        BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(10);

        Thread producer = new Thread(() -> {
            try {
                for (int i = 0; i < 100; i++) {
                    queue.put(i);
                    System.out.println("Produced: " + i);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        Thread consumer = new Thread(() -> {
            try {
                while (true) {
                    int value = queue.take();
                    System.out.println("Consumed: " + value);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        producer.start();
        consumer.start();
    }
}

2. Thread-Safe Singleton

  • Scenario: Implement a thread-safe singleton class.
  • Solution: Use double-checked locking or enum.
public class Singleton {
    private static volatile Singleton instance;

    private Singleton() {}

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

3. Deadlock Scenario

  • Scenario: Demonstrate a deadlock scenario and how to resolve it.
  • Solution: Avoid circular wait by acquiring locks in a consistent order.
public class DeadlockExample {
    private static final Object lock1 = new Object();
    private static final Object lock2 = new Object();

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            synchronized (lock1) {
                System.out.println("Thread 1: Holding lock 1...");
                try { Thread.sleep(100); } catch (InterruptedException e) {}
                System.out.println("Thread 1: Waiting for lock 2...");
                synchronized (lock2) {
                    System.out.println("Thread 1: Holding lock 1 & 2...");
                }
            }
        });

        Thread thread2 = new Thread(() -> {
            synchronized (lock2) {
                System.out.println("Thread 2: Holding lock 2...");
                try { Thread.sleep(100); } catch (InterruptedException e) {}
                System.out.println("Thread 2: Waiting for lock 1...");
                synchronized (lock1) {
                    System.out.println("Thread 2: Holding lock 1 & 2...");
                }
            }
        });

        thread1.start();
        thread2.start();
    }
}

4. Thread Pool with ExecutorService

  • Scenario: Use a thread pool to execute multiple tasks concurrently.
  • Solution: Use ExecutorService with a fixed thread pool.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPoolExample {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(5);

        for (int i = 0; i < 10; i++) {
            Runnable task = new Task(i);
            executor.execute(task);
        }

        executor.shutdown();
    }
}

class Task implements Runnable {
    private int taskId;

    public Task(int taskId) {
        this.taskId = taskId;
    }

    @Override
    public void run() {
        System.out.println("Task " + taskId + " is running on thread " + Thread.currentThread().getName());
    }
}

5. CountDownLatch

  • Scenario: Use CountDownLatch to wait for multiple threads to complete.
  • Solution: Initialize the latch with the number of threads and count down on completion.
import java.util.concurrent.CountDownLatch;

public class CountDownLatchExample {
    public static void main(String[] args) throws InterruptedException {
        int numThreads = 5;
        CountDownLatch latch = new CountDownLatch(numThreads);

        for (int i = 0; i < numThreads; i++) {
            new Thread(new Worker(latch)).start();
        }

        latch.await();
        System.out.println("All threads have completed their tasks.");
    }
}

class Worker implements Runnable {
    private CountDownLatch latch;

    public Worker(CountDownLatch latch) {
        this.latch = latch;
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " is working.");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        latch.countDown();
    }
}

6. CyclicBarrier

  • Scenario: Use CyclicBarrier to synchronize multiple threads at a common point.
  • Solution: Initialize the barrier with the number of threads and a barrier action.
import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierExample {
    public static void main(String[] args) {
        int numThreads = 3;
        CyclicBarrier barrier = new CyclicBarrier(numThreads, () -> {
            System.out.println("All threads have reached the barrier.");
        });

        for (int i = 0; i < numThreads; i++) {
            new Thread(new Task(barrier)).start();
        }
    }
}

class Task implements Runnable {
    private CyclicBarrier barrier;

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

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " is waiting at the barrier.");
        try {
            barrier.await();
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " has crossed the barrier.");
    }
}

7. Semaphore

  • Scenario: Use Semaphore to control access to a resource.
  • Solution: Initialize the semaphore with the number of permits.
import java.util.concurrent.Semaphore;

public class SemaphoreExample {
    public static void main(String[] args) {
        int numPermits = 2;
        Semaphore semaphore = new Semaphore(numPermits);

        for (int i = 0; i < 5; i++) {
            new Thread(new Task(semaphore)).start();
        }
    }
}

class Task implements Runnable {
    private Semaphore semaphore;

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

    @Override
    public void run() {
        try {
            semaphore.acquire();
            System.out.println(Thread.currentThread().getName() + " has acquired the permit.");
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            semaphore.release();
            System.out.println(Thread.currentThread().getName() + " has released the permit.");
        }
    }
}

Best Practices

  1. Understand Thread Safety: Ensure shared resources are accessed in a thread-safe manner.
  2. Avoid Deadlocks: Acquire locks in a consistent order.
  3. Use High-Level Concurrency Utilities: Prefer java.util.concurrent over low-level synchronization.
  4. Test Thoroughly: Test multithreaded code under various conditions to ensure correctness.

Resources


Leave a Reply

Your email address will not be published. Required fields are marked *