Multithreading in Java


Table of Contents

  1. Introduction to Multithreading
  2. Creating Threads in Java
  3. Thread Lifecycle
  4. Thread States
  5. Thread Synchronization
  6. Inter-thread Communication
  7. Thread Pooling
  8. Executor Framework
  9. Fork/Join Framework
  10. Conclusion

1. Introduction to Multithreading

Multithreading in Java is a fundamental feature that allows the concurrent execution of two or more threads. A thread is the smallest unit of execution within a process, and multiple threads can share the same resources, such as memory space, but execute independently. This allows programs to perform multiple tasks at once, improving the program’s efficiency, especially on multi-core processors.

Multithreading is essential for creating high-performance applications and is particularly useful in scenarios where tasks can be executed independently, like server applications, GUIs, and parallel data processing.

In Java, multithreading is built into the core language, making it relatively easy to implement. Java’s Thread class and Runnable interface are the primary tools for creating and managing threads.


2. Creating Threads in Java

There are two primary ways to create a thread in Java:

  1. By Extending the Thread Class
  2. By Implementing the Runnable Interface

1. By Extending the Thread Class

Java provides a Thread class, which can be extended to create a custom thread. This class has a run() method, which contains the code that will be executed by the thread. To run the thread, you need to call the start() method.

Example:

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

public static void main(String[] args) {
MyThread t = new MyThread();
t.start(); // Starts the thread
}
}

In this example, the run() method contains the task that the thread will perform.

2. By Implementing the Runnable Interface

Alternatively, you can implement the Runnable interface, which requires you to define the run() method. This approach is more flexible as it allows you to create threads by passing a Runnable object to a Thread object.

Example:

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

public static void main(String[] args) {
MyRunnable task = new MyRunnable();
Thread t = new Thread(task);
t.start(); // Starts the thread
}
}

In this example, the run() method in the Runnable interface is implemented, and the Runnable object is passed to the Thread constructor.


3. Thread Lifecycle

In Java, every thread goes through various states during its execution. The lifecycle of a thread is managed by the Thread Scheduler. The main states of a thread include:

  1. New (Born): A thread is in the “new” state when it is created but has not yet started.
  2. Runnable: A thread is in the “runnable” state after the start() method is called. A thread may remain in this state until it gets the CPU time to run.
  3. Blocked: A thread enters the “blocked” state when it is waiting for a resource that another thread holds.
  4. Waiting: A thread enters the “waiting” state when it is waiting indefinitely for another thread to perform a particular action (e.g., calling join()).
  5. Timed Waiting: A thread enters the “timed waiting” state when it is waiting for a specified period (e.g., Thread.sleep()).
  6. Terminated: A thread enters the “terminated” state when its run() method completes or it is otherwise terminated.

4. Thread States

The state of a thread at any given time can be one of the following:

  1. NEW: When a thread is created but not yet started.
  2. RUNNABLE: A thread is ready to run or is currently executing.
  3. BLOCKED: A thread is blocked and cannot proceed due to lack of resources.
  4. WAITING: A thread is waiting indefinitely for another thread to finish a task.
  5. TIMED_WAITING: A thread is waiting for a specified time.
  6. TERMINATED: The thread has finished executing.

The thread scheduler handles the transitions between these states.


5. Thread Synchronization

Thread synchronization is crucial when multiple threads access shared resources to prevent data inconsistency. Without synchronization, two threads could simultaneously access and modify shared data, leading to unpredictable results.

Example of Synchronization:

class Counter {
private int count = 0;

// Synchronized method to ensure thread safety
synchronized void increment() {
count++;
}

public int getCount() {
return count;
}
}

public class SyncExample {
public static void main(String[] args) {
Counter counter = new Counter();
Thread t1 = new Thread(() -> counter.increment());
Thread t2 = new Thread(() -> counter.increment());

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

In this example, the increment() method is synchronized, meaning only one thread can access it at a time.


6. Inter-thread Communication

Java provides a way for threads to communicate with each other using the wait(), notify(), and notifyAll() methods. These methods allow threads to cooperate and synchronize their execution.

  • wait(): Causes the current thread to release the lock and enter the waiting state.
  • notify(): Wakes up a thread that is waiting on the object’s lock.
  • notifyAll(): Wakes up all threads waiting on the object’s lock.

Example of Inter-thread Communication:

class Producer implements Runnable {
private final Object lock;

public Producer(Object lock) {
this.lock = lock;
}

public void run() {
synchronized (lock) {
System.out.println("Producing item");
lock.notify(); // Notify the consumer
}
}
}

class Consumer implements Runnable {
private final Object lock;

public Consumer(Object lock) {
this.lock = lock;
}

public void run() {
synchronized (lock) {
try {
lock.wait(); // Wait for producer to notify
System.out.println("Consuming item");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

public class CommunicationExample {
public static void main(String[] args) {
Object lock = new Object();
Thread producer = new Thread(new Producer(lock));
Thread consumer = new Thread(new Consumer(lock));

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

7. Thread Pooling

Creating and managing threads can be expensive in terms of performance. Thread pools help reduce this overhead by reusing a fixed number of threads for multiple tasks. Instead of creating new threads each time, tasks are queued and processed by available threads from the pool.

The ExecutorService interface and the Executors utility class are used to manage thread pools.

Example:

import java.util.concurrent.*;

public class ThreadPoolExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.submit(() -> System.out.println("Task 1"));
executor.submit(() -> System.out.println("Task 2"));
executor.shutdown();
}
}

8. Executor Framework

The Executor Framework provides a higher-level replacement for the traditional thread management approach. It abstracts thread management and provides better performance for handling a large number of tasks. The framework includes:

  • Executor: The core interface that executes tasks.
  • ExecutorService: Extends Executor and provides methods for managing lifecycle and task execution.
  • ScheduledExecutorService: Extends ExecutorService and provides methods for scheduling tasks.

9. Fork/Join Framework

The Fork/Join Framework is designed for tasks that can be recursively divided into smaller subtasks, which can then be executed in parallel. This is particularly useful for divide-and-conquer algorithms.

Example:

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

public class ForkJoinExample {
static class FibonacciTask extends RecursiveTask<Integer> {
private final int n;

public FibonacciTask(int n) {
this.n = n;
}

@Override
protected Integer compute() {
if (n <= 1) {
return n;
}
FibonacciTask task1 = new FibonacciTask(n - 1);
FibonacciTask task2 = new FibonacciTask(n - 2);
task1.fork();
task2.fork();
return task1.join() + task2.join();
}
}

public static void main(String[] args) {
ForkJoinPool pool = new ForkJoinPool();
FibonacciTask task = new FibonacciTask(10);
System.out.println(pool.invoke(task));
}
}

10. Conclusion

Multithreading in Java allows developers to write programs that can perform multiple tasks concurrently, leading to more efficient and responsive applications. Java provides several tools and frameworks for managing threads, including the Thread class, Runnable interface, synchronization mechanisms, the Executor Framework, and the Fork/Join framework. Understanding multithreading concepts and implementing them effectively can greatly improve the performance of applications, especially in data processing, server-side applications, and real-time systems.

By following best practices in multithreading (such as avoiding race conditions, using thread pooling, and managing synchronization carefully), you can build scalable and efficient Java applications.