DEV Community

DevCorner
DevCorner

Posted on

Multithreading & Concurrency in Java: One-Stop Solution for Interviews

Introduction

Multithreading and concurrency are fundamental concepts in Java, crucial for building high-performance and scalable applications. This guide covers everything you need to master Java concurrency for interviews, including essential concepts, code snippets, and best practices.


1. Thread vs. Runnable

Thread Class

  • Java provides the Thread class, which represents a thread of execution.
  • To create a thread, extend the Thread class and override the run() method.

Example:

class MyThread extends Thread {
    public void run() {
        System.out.println("Thread is running");
    }
}

public class ThreadExample {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        t1.start(); // Starts a new thread
    }
}
Enter fullscreen mode Exit fullscreen mode

Runnable Interface

  • Java provides the Runnable interface, which represents a task that can be executed by a thread.
  • Runnable is preferred as Java does not support multiple inheritance.

Example:

class MyRunnable implements Runnable {
    public void run() {
        System.out.println("Runnable is running");
    }
}

public class RunnableExample {
    public static void main(String[] args) {
        Thread t1 = new Thread(new MyRunnable());
        t1.start(); // Starts a new thread
    }
}
Enter fullscreen mode Exit fullscreen mode

Key Differences

Feature Thread Class Runnable Interface
Inheritance Extends Thread Implements Runnable
Flexibility Less flexible (restricts inheritance) More flexible (allows extending other classes)
Resource Usage More memory overhead Less memory overhead
Best Practice Not recommended Recommended

2. synchronized Keyword, Locks, and Reentrant Locks

synchronized Keyword

  • Used to achieve mutual exclusion.
  • Ensures only one thread can execute a synchronized block/method at a time.

Example:

class SharedResource {
    synchronized void printNumbers() {
        for (int i = 1; i <= 5; i++) {
            System.out.println(i);
            try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); }
        }
    }
}

class MyThread extends Thread {
    SharedResource resource;
    MyThread(SharedResource res) { this.resource = res; }
    public void run() { resource.printNumbers(); }
}

public class SynchronizedExample {
    public static void main(String[] args) {
        SharedResource obj = new SharedResource();
        MyThread t1 = new MyThread(obj);
        MyThread t2 = new MyThread(obj);
        t1.start();
        t2.start();
    }
}
Enter fullscreen mode Exit fullscreen mode

Locks and ReentrantLock

  • ReentrantLock allows more control than synchronized, including fairness policies.
  • Supports try-lock and timed locking mechanisms.

Example:

import java.util.concurrent.locks.ReentrantLock;

class SharedResource {
    private final ReentrantLock lock = new ReentrantLock();

    void printNumbers() {
        lock.lock();
        try {
            for (int i = 1; i <= 5; i++) {
                System.out.println(i);
                Thread.sleep(500);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

3. volatile, Atomic Variables, and ThreadLocal

volatile Keyword

  • Ensures visibility of changes to variables across threads.
  • Prevents instruction reordering.

Example:

class SharedData {
    volatile boolean flag = false;
}
Enter fullscreen mode Exit fullscreen mode

Atomic Variables

  • Provided by java.util.concurrent.atomic package.
  • Ensures atomic updates without explicit synchronization.

Example:

import java.util.concurrent.atomic.AtomicInteger;

class Counter {
    private AtomicInteger count = new AtomicInteger(0);
    void increment() { count.incrementAndGet(); }
}
Enter fullscreen mode Exit fullscreen mode

ThreadLocal

  • Provides thread-local variables, ensuring thread safety without synchronization.

Example:

class ThreadLocalExample {
    private static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
}
Enter fullscreen mode Exit fullscreen mode

4. Thread Starvation, Deadlock, and Livelock

Thread Starvation

Occurs when low-priority threads never get CPU time due to higher-priority threads monopolizing resources.

Deadlock

Occurs when two or more threads wait indefinitely for each other to release a lock.

Example:

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 locks");
                }
            }
        });
        Thread t2 = new Thread(() -> {
            synchronized (lock2) {
                synchronized (lock1) {
                    System.out.println("Thread 2 acquired locks");
                }
            }
        });
        t1.start();
        t2.start();
    }
}
Enter fullscreen mode Exit fullscreen mode

Livelock

Occurs when threads keep changing state to avoid deadlock but never make progress.


5. Fork/Join Framework

  • Used for parallel processing, dividing tasks into smaller subtasks.
  • Uses ForkJoinPool and RecursiveTask.

Example:

import java.util.concurrent.RecursiveTask;
import java.util.concurrent.ForkJoinPool;

class SumTask extends RecursiveTask<Integer> {
    private int start, end;
    SumTask(int start, int end) { this.start = start; this.end = end; }
    protected Integer compute() {
        if (end - start <= 10) {
            int sum = 0;
            for (int i = start; i <= end; i++) sum += i;
            return sum;
        }
        int mid = (start + end) / 2;
        SumTask left = new SumTask(start, mid);
        SumTask right = new SumTask(mid + 1, end);
        left.fork();
        return right.compute() + left.join();
    }
}

public class ForkJoinExample {
    public static void main(String[] args) {
        ForkJoinPool pool = new ForkJoinPool();
        int result = pool.invoke(new SumTask(1, 100));
        System.out.println("Sum: " + result);
    }
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

Mastering multithreading and concurrency in Java is essential for building efficient applications. Understanding synchronized, locks, volatile, atomic variables, deadlocks, and the Fork/Join framework will help you excel in interviews and real-world scenarios.

Top comments (0)