Multithreading

Understand the concepts of multithreading and concurrency in Java. Learn how to create and manage threads to perform tasks concurrently.


Creating and Managing Threads in Java

Introduction to Threads

Threads are a fundamental concept in concurrent programming. In Java, threads allow you to execute multiple parts of a program concurrently. This is especially useful for improving the performance of applications that perform multiple independent tasks. A thread is a lightweight subprocess within a process. A single process can contain multiple threads.

Creating Threads in Java

Java provides two primary ways to create threads:

1. Extending the Thread Class

You can create a thread by defining a new class that extends the Thread class and overriding the run() method. The run() method contains the code that will be executed by the thread.

Example:

 class MyThread extends Thread {
                    @Override
                    public void run() {
                        System.out.println("Thread running: " + Thread.currentThread().getName());
                        for (int i = 0; i < 5; i++) {
                            System.out.println("MyThread: " + i);
                            try {
                                Thread.sleep(500); // Simulate some work
                            } catch (InterruptedException e) {
                                System.out.println("Thread interrupted!");
                            }
                        }
                    }
                }

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

                        MyThread thread2 = new MyThread();
                        thread2.start(); // Start another thread
                    }
                } 

Explanation:

  • MyThread extends the Thread class.
  • The run() method is overridden to define the thread's logic.
  • thread1.start() creates a new thread and begins its execution, calling the run() method. The same applies for thread2.
  • Thread.sleep() pauses the thread's execution for a specified amount of time (in milliseconds). This can be useful to simulate a time-consuming operation.
  • Thread.currentThread().getName() gets the name of currently running thread.

2. Implementing the Runnable Interface

You can also create a thread by implementing the Runnable interface. This approach is generally preferred because it allows your class to extend another class if needed (Java does not allow multiple inheritance of classes). The Runnable interface requires you to implement the run() method. To execute the Runnable task, you need to create a Thread object, passing your Runnable implementation as the argument to the Thread constructor, and then call start() on the Thread object.

Example:

 class MyRunnable implements Runnable {
                    @Override
                    public void run() {
                        System.out.println("Thread running: " + Thread.currentThread().getName());
                        for (int i = 0; i < 5; i++) {
                            System.out.println("MyRunnable: " + i);
                            try {
                                Thread.sleep(500);
                            } catch (InterruptedException e) {
                                System.out.println("Thread interrupted!");
                            }
                        }
                    }
                }

                public class RunnableExample {
                    public static void main(String[] args) {
                        MyRunnable myRunnable = new MyRunnable();
                        Thread thread1 = new Thread(myRunnable);
                        thread1.start();

                        Thread thread2 = new Thread(new MyRunnable()); // Another way to create a thread
                        thread2.start();
                    }
                } 

Explanation:

  • MyRunnable implements the Runnable interface.
  • The run() method is overridden to define the thread's logic.
  • A Thread object is created, passing the MyRunnable instance to its constructor.
  • thread1.start() starts the thread, executing the run() method of the MyRunnable instance.
  • The second thread is create by directly instantiating MyRunnable in the Thread constructor.

When to use which approach:

  • If your class needs to inherit behavior from another class, implement the Runnable interface.
  • If your class doesn't need to inherit from another class and you want a simpler approach, extend the Thread class.
  • Implementing Runnable is generally preferred because it promotes better code organization and avoids the limitations of single inheritance in Java.

Thread Lifecycle

A thread's lifecycle consists of several states:

  • New: The thread has been created but has not yet started.
  • Runnable: The thread is ready to run and is waiting for its turn to be executed by the CPU. This state includes both "ready" and "running".
  • Blocked/Waiting: The thread is waiting for a resource or event (e.g., I/O completion, lock acquisition, notification from another thread).
  • Timed Waiting: The thread is waiting for a resource or event for a specified amount of time (e.g., calling Thread.sleep(), wait(long timeout)).
  • Terminated (Dead): The thread has completed its execution or has been terminated due to an exception.

You can influence a thread's state using methods like:

  • start(): Moves the thread from the "New" state to the "Runnable" state.
  • sleep(long millis): Moves the thread to the "Timed Waiting" state for the specified duration.
  • join(): Makes the current thread wait until the thread on which join() is called terminates.
  • interrupt(): Interrupts a thread that is blocked or waiting. This will usually cause an InterruptedException to be thrown in the target thread, if the thread is blocked on a method that supports interruption.
  • wait(): Causes the current thread to wait until another thread invokes the notify() or notifyAll() methods for this object.
  • notify(): Wakes up a single thread that is waiting on this object's monitor.
  • notifyAll(): Wakes up all threads that are waiting on this object's monitor.

Managing Thread States

Thread.sleep()

The Thread.sleep(long millis) method pauses the execution of the current thread for a specified duration in milliseconds. This is useful for introducing delays or simulating time-consuming operations.

Example:

 public class SleepExample {
                    public static void main(String[] args) {
                        System.out.println("Starting...");
                        try {
                            Thread.sleep(2000); // Pause for 2 seconds
                        } catch (InterruptedException e) {
                            System.out.println("Thread interrupted!");
                        }
                        System.out.println("...Finished");
                    }
                } 

Thread.join()

The Thread.join() method allows one thread to wait for the completion of another thread. When a thread calls join() on another thread, the calling thread is blocked until the target thread finishes its execution.

Example:

 public class JoinExample {
                    public static void main(String[] args) {
                        Thread t1 = new Thread(() -> {
                            try {
                                Thread.sleep(3000); // Simulate some work
                                System.out.println("Thread 1 finished.");
                            } catch (InterruptedException e) {
                                System.out.println("Thread 1 interrupted!");
                            }
                        });

                        Thread t2 = new Thread(() -> {
                            System.out.println("Thread 2 starting...");
                            try {
                                t1.join(); // Wait for thread 1 to finish
                                System.out.println("Thread 1 is done, Thread 2 continuing...");
                            } catch (InterruptedException e) {
                                System.out.println("Thread 2 interrupted!");
                            }
                        });

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

Thread.interrupt()

The Thread.interrupt() method interrupts a thread. If the target thread is blocked (e.g., waiting on I/O, sleeping, or waiting on a lock), an InterruptedException will be thrown. If the thread is not blocked, the interrupt flag is set, which the thread can check using Thread.interrupted() or Thread.isInterrupted().

Example:

 public class InterruptExample {
                    public static void main(String[] args) {
                        Thread t = new Thread(() -> {
                            try {
                                System.out.println("Thread starting...");
                                Thread.sleep(5000); // Long sleep
                                System.out.println("Thread finished normally.");
                            } catch (InterruptedException e) {
                                System.out.println("Thread interrupted!");
                            }
                        });

                        t.start();

                        try {
                            Thread.sleep(1000); // Wait for a short time
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }

                        System.out.println("Interrupting thread...");
                        t.interrupt();
                    }
                } 

Conclusion

Understanding how to create and manage threads is essential for building concurrent applications in Java. By choosing the appropriate approach (extending Thread or implementing Runnable) and utilizing methods to manage thread states, you can effectively improve application performance and responsiveness. Proper thread management is crucial to avoid issues like race conditions, deadlocks, and resource starvation.