Java Multithreading

Introduction

Multithreading in Java is a feature that allows concurrent execution of two or more threads, enabling efficient utilization of the CPU. Each thread runs in parallel and can perform tasks simultaneously, improving the performance and responsiveness of applications. Java provides built-in support for multithreading through the java.lang.Thread class and the java.util.concurrent package.

Key Points:

  • Concurrent Execution: Allows multiple threads to run concurrently.
  • Efficient CPU Utilization: Enhances performance by making efficient use of CPU resources.
  • Responsive Applications: Improves the responsiveness of applications, especially in UI-based programs.
  • Built-In Support: Java provides robust support for multithreading through its standard libraries.

Table of Contents

  1. What is Multithreading?
  2. Benefits of Multithreading
  3. Creating Threads
  4. Thread Lifecycle
  5. Thread States
  6. Synchronization
  7. Inter-Thread Communication
  8. Thread Priorities
  9. Example: Multithreading in Java
  10. Best Practices
  11. Real-World Analogy
  12. Conclusion

1. What is Multithreading?

Multithreading is a programming technique where multiple threads run concurrently within a single process. Each thread represents a separate path of execution. In Java, threads are represented by the Thread class or can be created by implementing the Runnable interface.

2. Benefits of Multithreading

  • Improved Performance: Enables multiple tasks to be performed simultaneously, enhancing the performance of applications.
  • Efficient Resource Utilization: Makes better use of system resources by running multiple threads in parallel.
  • Enhanced Responsiveness: Improves the responsiveness of applications, especially in user interfaces and real-time systems.
  • Simplified Modeling: Allows complex, time-consuming tasks to be divided into simpler, concurrent tasks.

3. Creating Threads

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

  1. By extending the Thread class.
  2. By implementing the Runnable interface.

Extending the Thread Class

You can create a new thread by extending the Thread class and overriding its run() method.

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

public class ThreadExample {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        t1.start();  // Start the thread
    }
}

Implementing the Runnable Interface

You can create a new thread by implementing the Runnable interface and passing an instance of your class to a Thread object.

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

public class RunnableExample {
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread t1 = new Thread(myRunnable);
        t1.start();  // Start the thread
    }
}

4. Thread Lifecycle

A thread in Java has several states in its lifecycle:

  1. New: A thread that has been created but not yet started.
  2. Runnable: A thread that is ready to run and waiting for CPU time.
  3. Blocked: A thread that is waiting for a monitor lock to enter or re-enter a synchronized block/method.
  4. Waiting: A thread that is waiting indefinitely for another thread to perform a particular action.
  5. Timed Waiting: A thread that is waiting for another thread to perform a particular action for a specified waiting time.
  6. Terminated: A thread that has exited.

5. Thread States

Example:

public class ThreadStatesExample {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            try {
                Thread.sleep(1000);  // Timed waiting
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Thread is running...");
        });

        System.out.println("State: " + t1.getState());  // NEW
        t1.start();
        System.out.println("State: " + t1.getState());  // RUNNABLE
        try {
            Thread.sleep(500);
            System.out.println("State: " + t1.getState());  // TIMED_WAITING
            t1.join();
            System.out.println("State: " + t1.getState());  // TERMINATED
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

6. Synchronization

Synchronization in Java is used to control the access of multiple threads to shared resources. This is essential to prevent data inconsistency and ensure thread safety.

When multiple threads access shared resources, there can be data inconsistency. Synchronization ensures that only one thread can access the shared resource at a time.

Synchronized Method

A synchronized method in Java can be accessed by only one thread at a time for a particular instance.

class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

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

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

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

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Count: " + counter.getCount());  // Expected output: 2000
    }
}

Synchronized Block

A synchronized block is a block of code that is synchronized with a particular object. Compared to synchronized methods, synchronized blocks allow more fine-grained control over synchronization.

class Counter {
    private int count = 0;

    public void increment() {
        synchronized (this) {
            count++;
        }
    }

    public int getCount() {
        return count;
    }
}

7. Inter-Thread Communication

Inter-thread communication in Java is facilitated through methods like wait(), notify(), and notifyAll().

Example:

class SharedResource {
    private int value = 0;
    private boolean available = false;

    public synchronized void produce(int value) throws InterruptedException {
        while (available) {
            wait();
        }
        this.value = value;
        available = true;
        notify();
    }

    public synchronized int consume() throws InterruptedException {
        while (!available) {
            wait();
        }
        available = false;
        notify();
        return value;
    }
}

public class InterThreadCommunicationExample {
    public static void main(String[] args) {
        SharedResource sharedResource = new SharedResource();

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

        Thread consumer = new Thread(() -> {
            try {
                for (int i = 0; i < 5; i++) {
                    int value = sharedResource.consume();
                    System.out.println("Consumed: " + value);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

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

8. Thread Priorities

Java allows setting priorities for threads using the setPriority() method. Priorities are integers ranging from MIN_PRIORITY (1) to MAX_PRIORITY (10), with NORM_PRIORITY (5) as the default.

Example:

class MyThread extends Thread {
    public void run() {
        System.out.println("Thread priority: " + Thread.currentThread().getPriority());
    }

    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();

        t1.setPriority(Thread.MIN_PRIORITY);
        t2.setPriority(Thread.MAX_PRIORITY);

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

Output:

Thread priority: 1
Thread priority: 10

9. Example: Multithreading in Java

Example: Calculating Sum of Array Elements

class SumTask implements Runnable {
    private int[] arr;
    private int start;
    private int end;
    private int result;

    public SumTask(int[] arr, int start, int end) {
        this.arr = arr;
        this.start = start;
        this.end = end;
    }

    public int getResult() {
        return result;
    }

    @Override
    public void run() {
        result = 0;
        for (int i = start; i < end; i++) {
            result += arr[i];
        }
    }
}

public class MultiThreadingExample {
    public static void main(String[] args) {
        int[] arr = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
        int mid = arr.length / 2;

        SumTask task1 = new SumTask(arr, 0, mid);
        SumTask task2 = new SumTask(arr, mid, arr.length);

        Thread t1 = new Thread(task1);
        Thread t2 = new Thread(task2);

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

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        int totalSum = task1.getResult() + task2.getResult();
        System.out.println("Total Sum: " + totalSum);
    }
}

Output:

Total Sum: 55

10. Best Practices

  1. Avoid Race Conditions: Use synchronization to avoid race conditions when multiple threads access shared resources.
  2. Minimize Synchronization Overhead: Use synchronized blocks instead of synchronized methods to minimize the scope of synchronization and reduce overhead.
  3. Use Thread Pools: Use thread pools to manage and reuse threads efficiently, especially for handling a large number of short-lived tasks.
  4. Handle InterruptedException: Always handle InterruptedException appropriately to ensure threads are interrupted gracefully.
  5. Use Atomic Variables: Use atomic variables (e.g., AtomicInteger, AtomicBoolean) for simple thread-safe operations instead of synchronization.
  6. Avoid Deadlocks: Be cautious of potential deadlocks and design your synchronization strategy to avoid them.
  7. Leverage High-Level Concurrency Utilities: Use high-level concurrency utilities from the java.util.concurrent package for complex synchronization and concurrency tasks.

11. Real-World Analogy

Consider a restaurant kitchen where multiple chefs (threads) work simultaneously to prepare different dishes (tasks):

  • Adding Ingredients: Each chef adds ingredients to their assigned dishes.
  • Cooking: Chefs cook their dishes simultaneously.
  • Serving: Dishes are served to customers as soon as they are ready.
  • Shared Resources: Chefs need to coordinate when using shared resources like ovens or cooking utensils to avoid conflicts.

12. Conclusion

Multithreading in Java is a powerful feature that allows concurrent execution of tasks, improving the performance and responsiveness of applications. By understanding how to create, manage, and synchronize threads, you can effectively utilize multithreading in your Java applications. Following best practices will help you write efficient, thread-safe, and maintainable code.

Leave a Comment

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

Scroll to Top