Multithreading
Dashboard
Topic 20/30

Multithreading in Java

Multithreading is a Java feature that allows concurrent execution of two or more parts of a program for maximum utilization of CPU. Each part of such a program is called a thread. So, threads are light-weight processes within a process.

Process vs Thread

  • Process: A heavyweight, isolated execution unit with its own memory space.
  • Thread: A lightweight unit of execution that shares the memory and resources of the process that created it.

Thread Lifecycle

A thread goes through various states: New (created), Runnable (ready to run), Running (currently executing), Blocked/Waiting (waiting for resources/locks), and Terminated (finished execution).

Creating Threads

You can create a thread either by extending the Thread class or by implementing the Runnable interface (preferred).

ThreadBasicsDemo.javaJava
class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 1; i <= 3; i++) {
            System.out.println(Thread.currentThread().getName() + " - Count: " + i);
            try {
                Thread.sleep(500); // Sleep for 500ms
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class ThreadBasicsDemo {
    public static void main(String[] args) {
        Thread t1 = new Thread(new MyRunnable(), "Worker-1");
        Thread t2 = new Thread(new MyRunnable(), "Worker-2");
        
        t1.start(); // Start the thread, invokes run()
        t2.start();
        
        try {
            t1.join(); // Wait for t1 to finish
            t2.join(); // Wait for t2 to finish
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        System.out.println("Main thread finished.");
    }
}
Note: Calling run() directly instead of start() will execute the method in the current thread, not a new one.

Synchronization

When multiple threads access shared resources, data inconsistency can occur (Race Condition). The synchronized keyword ensures that only one thread can execute a block of code at a time.

SynchronizedCounter.javaJava
class Counter {
    private int count = 0;

    // synchronized method prevents race conditions
    public synchronized void increment() {
        count++;
    }

    public int getCount() { return count; }
}

public class SynchronizedCounter {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();

        Runnable task = () -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        };

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);

        t1.start(); t2.start();
        t1.join(); t2.join();

        System.out.println("Final Count: " + counter.getCount()); // Will be exactly 2000
    }
}

Inter-Thread Communication

Threads can communicate using wait(), notify(), and notifyAll(). These methods must be called from within a synchronized context.

ProducerConsumer.javaJava
class SharedBuffer {
    private int data;
    private boolean hasData = false;

    public synchronized void produce(int value) throws InterruptedException {
        while (hasData) { wait(); } // Wait if buffer is full
        data = value;
        hasData = true;
        System.out.println("Produced: " + data);
        notify(); // Notify consumer
    }

    public synchronized int consume() throws InterruptedException {
        while (!hasData) { wait(); } // Wait if buffer is empty
        hasData = false;
        System.out.println("Consumed: " + data);
        notify(); // Notify producer
        return data;
    }
}

public class ProducerConsumer {
    public static void main(String[] args) {
        SharedBuffer buffer = new SharedBuffer();
        
        new Thread(() -> {
            try { for (int i=1; i<=3; i++) buffer.produce(i); } catch (Exception e) {}
        }).start();
        
        new Thread(() -> {
            try { for (int i=1; i<=3; i++) buffer.consume(); } catch (Exception e) {}
        }).start();
    }
}

ExecutorService & Thread Pools

Creating new threads for every task is expensive. Java provides the Executor framework to manage thread pools.

ExecutorServiceDemo.javaJava
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.Callable;

public class ExecutorServiceDemo {
    public static void main(String[] args) throws Exception {
        // Create a thread pool of 2 threads
        ExecutorService executor = Executors.newFixedThreadPool(2);

        Callable<Integer> task = () -> {
            Thread.sleep(1000);
            return 42;
        };

        // Submit task to executor and get Future
        Future<Integer> future = executor.submit(task);
        
        System.out.println("Waiting for result...");
        System.out.println("Result: " + future.get()); // Blocks until result is ready
        
        executor.shutdown();
    }
}

Deadlock

Deadlock occurs when two or more threads are blocked forever, waiting for each other.

DeadlockDemo.javaJava
public class DeadlockDemo {
    public static void main(String[] args) {
        final Object resource1 = new Object();
        final Object resource2 = new Object();

        new Thread(() -> {
            synchronized (resource1) {
                System.out.println("Thread 1: Locked resource 1");
                try { Thread.sleep(50); } catch (Exception e) {}
                synchronized (resource2) {
                    System.out.println("Thread 1: Locked resource 2");
                }
            }
        }).start();

        new Thread(() -> {
            synchronized (resource2) { // Reversing the lock order avoids deadlock
                System.out.println("Thread 2: Locked resource 2");
                try { Thread.sleep(50); } catch (Exception e) {}
                synchronized (resource1) {
                    System.out.println("Thread 2: Locked resource 1");
                }
            }
        }).start();
    }
}
Note: To avoid deadlocks, ensure all threads acquire multiple locks in the same order.