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).
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."); } }
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.
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.
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.
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.
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(); } }