Chapter 40: Multithreading in Java (Part 3)

Chapter 40: Multithreading in Java (Part 3)

Table of contents

In this chapter, we dive deep into key aspects of multithreading in Java, focusing on thread priorities, thread synchronization using join(), and managing thread states with the sleep() method. We will explore multiple practical scenarios and edge cases that illustrate how Java handles thread management, coordination, and execution.

Key Topics Covered:

  1. Thread Priorities:

    • Understand how Java assigns priorities to threads and how the thread scheduler uses these priorities to decide which thread gets CPU time first.

    • Learn about setting thread priorities using Thread.setPriority(), and the effects of different priority levels on thread execution.

  2. The join() Method:

    • Explore how the join() method is used to make one thread wait for another to finish before proceeding.

    • We will cover several use cases, demonstrating both correct usage and problematic cases like deadlocks when threads attempt to join() each other in improper sequences.

  3. Thread Lifecycle with sleep():

    • Learn about the lifecycle of a thread when invoking the sleep() method. This allows a thread to pause its execution for a specific time, entering a sleeping state and then transitioning back to ready/runnable state after the sleep period ends or if interrupted.

    • We will look at practical examples like simulating delays in a timer, managing timed tasks, and understanding the nuances of handling InterruptedException.

  4. Real-World Scenarios:

    • Multiple examples are provided to understand how to use join(), sleep(), and thread priorities in practical scenarios, including managing tasks in parallel, preventing thread starvation, and avoiding common pitfalls in multithreading.
  5. Handling Thread Deadlocks and Interruptions:

    • Learn how improper usage of join() can result in deadlocks, where threads wait indefinitely for each other, and how to resolve such issues.

    • Understand how the sleep() method interacts with thread interruptions and how to handle InterruptedException.


By the end of this chapter, you will have a thorough understanding of how to manage thread priorities, coordinate threads with join(), and control thread execution times with sleep(). You’ll be equipped to handle various threading scenarios efficiently, avoiding issues like deadlocks and ensuring that your multithreaded applications run smoothly.

1.Priority of Threads in Java

In Java, multithreading is a crucial concept that enables multiple threads to execute concurrently. The thread priority mechanism helps determine the order in which threads are executed when CPU time is limited.

Understanding Thread Priority with Thread.currentThread().getPriority()

  • The method Thread.currentThread().getPriority() is used to retrieve the priority of the currently executing thread.

  • Java provides two key methods to manage thread priorities:

    • setPriority(int priorityNumber): Assigns a new priority to the thread.

    • getPriority(): Retrieves the priority value of the thread.

Thread Scheduler and Execution Priority

The thread scheduler, a component of the JVM, plays a pivotal role in managing thread execution. Here's the process:

  1. The OS provides CPU resources (e.g., 3 seconds) to the JVM.

  2. The JVM delegates this time to the thread scheduler.

  3. The thread scheduler decides which thread will execute based on their priority.

Decision Basis for Execution

  • Threads are executed based on their priority values.

  • If multiple threads have the same priority, the thread scheduler employs an algorithm to decide. This algorithm is vendor-dependent and is not disclosed by the vendor.

  • The priority levels range from 1 (MIN_PRIORITY) to 10 (MAX_PRIORITY), with the default being 5 (NORM_PRIORITY).

Traffic Circle Example (Priority Analogy)

To understand thread priorities, consider the example of managing traffic at a roundabout:

  • Dignitaries like a PM of another country or a CM are given higher priority.

  • A local MLA or common citizens get comparatively lower priority.

  • Similarly, in threads, higher priority threads are likely to execute first, provided resources are available.


Default Thread Priority

  • The default priority for the main thread is 5.

  • Child threads inherit their priority from the parent thread.

    • For example, if the main thread has a priority of 5, a child thread created from the main thread will also have a priority of 5.
  • JVM Behavior:

    • The main thread is automatically created by the JVM and starts with a default priority of 5.

    • This inheritance ensures consistency in priority unless explicitly changed.


Example 1: Default Priority of Threads

class MyThread extends Thread {
    public void run() {
        System.out.println("Priority of Child thread is " + Thread.currentThread().getPriority());
    }
}

public class Multithreading30 {
    public static void main(String[] args) {
        System.out.println("Priority of main thread is " + Thread.currentThread().getPriority());
        MyThread t = new MyThread();
        t.start();
    }
}

Output:

Priority of main thread is 5
Priority of Child thread is 5

Explanation:

  1. The main thread begins execution with a default priority of 5.

  2. A child thread (MyThread) is created and inherits the priority of the main thread.

  3. When both threads execute, their priorities are displayed, confirming the inheritance mechanism.


Key Points on Thread Priority

  1. Inheritance of Priority:

    • The priority of a child thread is inherited from its parent thread.

    • This ensures seamless execution without requiring explicit priority assignment unless necessary.

  2. Role of Thread Scheduler:

    • Determines the execution order based on priority.

    • Uses vendor-specific algorithms when threads have the same priority.

  3. Default Priority:

    • Main thread: 5.

    • Other threads: Inherit from parent threads unless explicitly set.

  4. Dynamic Priority Adjustment:

    • Using setPriority(int priorityNumber), developers can dynamically change thread priorities as needed.

Visual Representation of Thread Scheduling:

                        OS
                        | 3 sec
                       JVM<-----CPU
                          |
                  Thread Scheduler
                 _______|________
                |                |
           main thread        Thread-0 (child thread)

Here, the Thread Scheduler determines which thread (main or Thread-0) will execute based on priority. Both threads having the same default priority (5) leave the decision to the scheduler's internal algorithm.

2.Setting Priority of Threads in Java

Thread priority is a key feature of Java’s multithreading capabilities, allowing developers to influence the execution order of threads. Higher-priority threads are more likely to get CPU time, but execution is still subject to the thread scheduler's algorithm and the underlying operating system's support for prioritization.


How to Set Thread Priority?

Java provides the setPriority(int priorityNumber) method to explicitly assign a priority to a thread.

Code Example: Assigning a Priority

MyThread t = new MyThread();
t.setPriority(10); // Setting the child thread's priority to MAX_PRIORITY (10)
  • By default, the main thread has a priority of 5 (NORM_PRIORITY).

  • After calling t.start(), there are two active threads:

    • Main thread with priority 5.

    • Child thread (MyThread) with priority 10.

Execution Order

  • The thread with the higher priority (child thread with priority 10) gets CPU time first.

  • The thread scheduler allocates CPU time to higher-priority threads, making them execute sooner, provided the OS supports priority-based scheduling.

  • If the OS does not support scheduling or uses a different algorithm, the execution order may be unpredictable.


Thread Priority Ranges

  1. Valid Priority Range:

    • Java supports priorities from 1 (lowest priority) to 10 (highest priority).

    • Assigning a value outside this range results in an IllegalArgumentException.

  2. Predefined Constants:

    • Thread.MIN_PRIORITY = 1

    • Thread.NORM_PRIORITY = 5

    • Thread.MAX_PRIORITY = 10

Priority Constants in java.lang.Thread

You can inspect these constants by running:

javap java.lang.Thread

The output will display:

public static final int MIN_PRIORITY = 1;
public static final int NORM_PRIORITY = 5; // Normal priority
public static final int MAX_PRIORITY = 10;

Important Notes:

  • Java does not have predefined constants like Thread.LOW_PRIORITY or Thread.HIGH_PRIORITY. Instead, use the predefined minimum, normal, and maximum constants.

  • Threads with equal priority depend on the thread scheduler's vendor-dependent algorithm, making execution order unpredictable in such cases.


Visual Representation of Thread Priority Allocation

                          Thread Scheduler
                          _______|________
                         |                |
         thread priority- 5                10
  • The scheduler allocates CPU time to the thread with priority 10 first.

Example 2: Setting Thread Priority

class MyThread extends Thread {
    public void run() {
        System.out.println("Priority of Child thread is " + Thread.currentThread().getPriority());
    }
}

public class Multithreading31 {
    public static void main(String[] args) {
        System.out.println("Priority of main thread is " + Thread.currentThread().getPriority());

        MyThread t = new MyThread();
        t.setPriority(10); // Setting the child thread's priority to 10
        t.start();         // Starting the child thread
    }
}

Output:

Priority of main thread is 5
Priority of Child thread is 10

Execution Flow:

  1. The main thread executes first and displays its priority as 5.

  2. The child thread is created and assigned a priority of 10.

  3. The thread scheduler allocates CPU time to the child thread first, as it has a higher priority than the main thread.


Key Observations:

  1. Impact of High Priority:

    • The child thread (priority 10) executes before the main thread (priority 5) because the scheduler prioritizes higher-priority threads.

    • However, this behavior relies on the OS supporting priority-based scheduling.

  2. Execution on Cracked or Unsupported OS:

    • On systems with improper thread scheduling support (e.g., cracked versions of Windows), the execution order might not align with the assigned priorities, leading to unexpected results.

Summary of Thread Priority Management

  1. Default Priority:

    • Main thread: 5 (NORM_PRIORITY).

    • Child threads: Inherit the priority of the parent unless explicitly changed.

  2. Range and Constants:

    • Priority range: 1 to 10.

    • Constants: MIN_PRIORITY = 1, NORM_PRIORITY = 5, MAX_PRIORITY = 10.

  3. Setting and Getting Priority:

    • Use setPriority(int) to set and getPriority() to retrieve thread priorities.
  4. Scheduling Dependencies:

    • Priority influences execution order but depends on the OS and JVM.

Example 3: Invalid Priority Value

  1. Allowed Priority Range in Java:

    • In Java, the valid range for thread priority is 1 to 10.

    • Setting a priority outside this range results in a runtime exception (IllegalArgumentException).

  2. Code Walkthrough:

     class MyThread extends Thread {
         public void run() {
             System.out.println("Priority of Child thread is " + Thread.currentThread().getPriority());
         }
     }
     public class Multithreading32 {
         public static void main(String[] args) {
             System.out.println("Priority of main thread is " + Thread.currentThread().getPriority()); 
             MyThread t = new MyThread();
             t.setPriority(100); // Invalid priority value
             t.start();
         }
     }
    
  3. Detailed Explanation of Steps:

    • Step 1: The program begins in the main thread.

      • The priority of the main thread is fetched using Thread.currentThread().getPriority() and is displayed. By default, the priority is 5.
    • Step 2: A new MyThread object t is created, which extends Thread. At this point, the run method of MyThread is not yet executed.

    • Step 3: The setPriority(100) method is called to set the priority of the thread t.

      • Since 100 is outside the valid range (1-10), Java throws an IllegalArgumentException at runtime.

      • The JVM stops execution at this point, and the t.start() line is never reached.

  4. Output Analysis:

    • The priority of the main thread (5) is printed.

    • The program terminates with an exception:

        Exception in thread "main" java.lang.IllegalArgumentException
        at java.base/java.lang.Thread.setPriority(Thread.java:1138)
        at Multithreading32.main(Multithreading32.java:185)
      
  5. Key Learnings:

    • The valid range for Thread.setPriority() is 1 (MIN_PRIORITY) to 10 (MAX_PRIORITY).

    • Any value outside this range causes a runtime exception.

    • Although setPriority() accepts integers as parameters, validation is performed during runtime.


Example 4: Setting Priority After Starting the Thread

  1. What Happens When We Set Priority After start()?:

    • Java allows setting a thread’s priority using setPriority(int priorityNumber) before starting the thread using start().

    • If setPriority() is called after the thread starts, the new priority does not affect the already executing thread, but Java does not block such a call.

  2. Code Walkthrough:

     class MyThread extends Thread {
         public void run() {
             System.out.println("Priority of Child thread is " + Thread.currentThread().getPriority());
             for (int i = 0; i < 5; i++) {
                 System.out.println("Child thread");
             }
         }
     }
     public class Multithreading32 {
         public static void main(String[] args) {
             MyThread t = new MyThread();
             t.start(); // Thread execution begins
             t.setPriority(100); // Invalid value after start
             System.out.println("Priority of main thread is " + Thread.currentThread().getPriority());
             for (int i = 0; i < 5; i++) {
                 System.out.println("Main thread");
             }
         }
     }
    
  3. Detailed Explanation of Steps:

    • Step 1: The main thread starts.

      • A MyThread object t is created, but its run() method is not yet invoked.
    • Step 2: The t.start() method is called, signaling the JVM to schedule the run() method of MyThread on a new thread.

      • At this point, the child thread (t) begins execution with its current priority, which defaults to 5.
    • Step 3: The setPriority(100) method is called after t.start().

      • Since 100 is outside the valid range, Java throws an IllegalArgumentException. However, the child thread continues executing.
    • Step 4: The main thread continues execution in parallel, printing its default priority and the "Main thread" message.

  4. Output Analysis:

    • The child thread’s priority is printed as 5, as the invalid priority change (100) does not succeed.

    • Both threads execute their respective loops, but the exception interrupts the normal flow:

        Priority of Child thread is 5
        Child thread
        Child thread
        Child thread
        Child thread
        Child thread
        Exception in thread "main" java.lang.IllegalArgumentException
        at java.base/java.lang.Thread.setPriority(Thread.java:1138)
        at Multithreading32.main(Multithreading32.java:63)
      
  5. Key Learnings:

    • Calling setPriority(int priorityNumber) after a thread has started does not alter its scheduling immediately.

    • Any invalid value for priority results in a runtime exception, regardless of when setPriority() is called.

    • Proper thread management requires setting priorities before starting the thread.

Preventing Threads from Execution Using yield(), sleep(), and join()


4. Methods to Prevent (Stop) Thread Execution

In multithreading, there are scenarios where threads need to pause or stop their execution temporarily. Java provides three primary methods to achieve this:

  1. yield()

  2. sleep()

  3. join()


Thread Lifecycle Overview

Before diving into the methods, let's understand the lifecycle of a thread:

  1. New/Born State:

    • The thread is created using Thread t = new Thread(); but not yet started.
  2. Ready/Runnable State:

    • When t.start() is called, the thread enters the Runnable state, waiting for the CPU to allocate execution time.
  3. Running State:

    • Once the CPU allocates time, the thread enters the Running state.
  4. Dead State:

    • After completing the run() method, the thread enters the Dead state.

Thread Lifecycle Diagram:

     New/Born -----------> Ready/Runnable -----------> Running -----------> Dead
                                 |                         |
                             main thread             child thread

1. yield() Method

  • Purpose:

    • The yield() method pauses the currently executing thread to give a chance to other threads of the same priority to execute.

    • It is a static native method in Java (public static native void yield()).

    • If there are no threads of the same or higher priority, the current thread continues execution.

How it Works:

  1. A thread in the Running state calls yield():

    • The thread transitions back to the Ready/Runnable state.
  2. The Thread Scheduler decides when to allocate CPU time back to the yielded thread.

Behavior:

  • If there are waiting threads of the same priority, they might execute.

  • If all threads have the same priority, the thread to execute next is chosen randomly by the Thread Scheduler.

  • Note: The behavior of yield() is platform-dependent and may not guarantee fairness.


Code Example: yield() Usage

class MyThread extends Thread {
    public void run() {
        for (int i = 0; i < 5; i++) {
            Thread.yield(); // Line-1
            System.out.println("Child thread");
        }
    }
}

public class Multithreading33 {
    public static void main(String[] args) {
        MyThread t = new MyThread();
        t.start(); // Child thread enters Runnable state
        for (int i = 0; i < 5; i++) {
            System.out.println("Main thread");
        }
    }
}

Output Analysis:

  • First Execution:

      Main thread
      Main thread
      Main thread
      Main thread
      Main thread
      Child thread
      Child thread
      Child thread
      Child thread
      Child thread
    
  • Second Execution:

      Main thread
      Main thread
      Child thread
      Child thread
      Main thread
      Main thread
      Main thread
      Child thread
      Child thread
    

Explanation:

  • In the first run, the Main thread completed its execution before the Child thread.

  • In the second run, both threads alternated execution due to context switching, controlled by the Thread Scheduler.

  • If Line-1 (yield) is commented, the thread with higher priority or randomly chosen thread will dominate.


Key Points:

  1. yield() is platform-dependent, and its behavior may vary.

  2. Threads with equal priority might still not execute in a predictable order.

  3. It does not guarantee immediate suspension; it depends entirely on the Thread Scheduler.


Real-Life Analogy:

  • Imagine a telephone booth with multiple people (threads) in line.

    • Person-A (thread) is talking on the phone.

    • Person-A uses yield() to pause and let Person-B (another thread) talk if they are of equal priority.

    • If Person-C is also waiting, the owner (Thread Scheduler) decides who gets the next turn.

Thread State Transition for yield():

Running ----> Ready/Runnable ----> Running (when CPU time is allocated)

2. join() Method


Overview

The join() method allows one thread to wait for the completion of another thread. In simpler terms, if thread t1 calls t2.join(), t1 will pause its execution until thread t2 finishes.


Prototype of join()

The join() method comes in three variations:

  1. public final void join() throws InterruptedException

    • Waits indefinitely until the target thread finishes execution.
  2. public final void join(long ms) throws InterruptedException

    • Waits for a specified time in milliseconds for the target thread to complete.
  3. public final void join(long ms, int ns) throws InterruptedException

    • Waits for the specified time in milliseconds and nanoseconds for the target thread to complete.

Explanation Through a Scenario

Scenario-1: Thread Waiting Until Another Thread Completes

Two teachers, Hyder Sir (t1) and Nithin Sir (t2), are conducting sessions:

  • Hyder Sir (t1) finishes at 10:45 AM.

  • Nithin Sir (t2) extends his session until 12:00 PM.

  • Two friends (threads) need to return to their PG together.

    • Friend A (t1) waits for Friend B (t2) until Nithin Sir finishes.

    • Once t2 completes, t1 joins it, and they proceed together.

Scenario-2: Thread Interrupted During Waiting

If another person (thread) interrupts Friend A (t1) during their wait, it throws an InterruptedException. This represents an external disruption.


Key Points

  1. Waiting State:

    • When t1.join() is called, t1 enters the waiting state until t2 completes.
  2. InterruptedException:

    • If another thread interrupts t1 while it is waiting for t2, an InterruptedException is thrown.

    • This is a checked exception that must be handled.


Thread Waiting Scenarios

  1. Thread waits until the other thread completes execution:

    • t1.join() waits indefinitely for t2.
  2. Thread waits for a specified duration:

    • t1.join(30000) makes t1 wait for 30 seconds.

    • After the specified duration, t1 continues, whether or not t2 has completed.

  3. Thread waits for a specific time in milliseconds and nanoseconds:

    • t1.join(30000, 500) makes t1 wait for 30 seconds and 500 nanoseconds.

Code Example: Using join()

class MyThread extends Thread {
    public void run() {
        for (int i = 1; i <= 5; i++) {
            System.out.println("Child thread: " + i);
            try {
                Thread.sleep(1000); // Simulate some work
            } catch (InterruptedException e) {
                System.out.println("Child thread interrupted");
            }
        }
    }
}

public class MultithreadingJoinExample {
    public static void main(String[] args) {
        MyThread t = new MyThread();
        t.start(); // Start child thread

        try {
            System.out.println("Main thread waiting for child thread to complete...");
            t.join(); // Main thread waits for child thread
        } catch (InterruptedException e) {
            System.out.println("Main thread interrupted");
        }

        System.out.println("Main thread resumes after child thread completion.");
    }
}

Output

Main thread waiting for child thread to complete...
Child thread: 1
Child thread: 2
Child thread: 3
Child thread: 4
Child thread: 5
Main thread resumes after child thread completion.

Explanation

  1. The main thread starts first and then waits for the child thread to finish execution using t.join().

  2. The child thread executes its task and completes.

  3. After the child thread finishes, the main thread resumes.


Interruption Scenario

If another thread interrupts the waiting thread, an InterruptedException occurs. This is demonstrated below:

Code Example: InterruptedException

public class MultithreadingInterruptExample {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            for (int i = 1; i <= 5; i++) {
                System.out.println("Child thread: " + i);
                try {
                    Thread.sleep(1000); // Simulate some work
                } catch (InterruptedException e) {
                    System.out.println("Child thread interrupted");
                }
            }
        });

        t.start();

        try {
            System.out.println("Main thread waiting for child thread...");
            t.join();
        } catch (InterruptedException e) {
            System.out.println("Main thread interrupted");
        }

        // Simulating interruption
        if (t.isAlive()) {
            t.interrupt(); // Interrupts child thread
        }
    }
}

Output

Main thread waiting for child thread...
Child thread: 1
Child thread: 2
Child thread interrupted
Main thread interrupted

Summary

  • join() ensures thread synchronization by allowing one thread to wait for another.

  • It can handle specific durations and interruptions using overloaded methods.

  • InterruptedException must always be handled to avoid runtime errors.


Scenario-3: Marriage Planning with Dependent Actions

This scenario models dependent tasks (Venue Fixing, Wedding Card Printing, Wedding Card Distribution). Each task depends on the completion of the previous one. This represents thread dependencies and join states, which are critical in thread-based multithreading.

Key Details:

  1. Thread Execution States:

    • t1 must complete before t2 starts (t1.join()).

    • t2 must complete before t3 starts (t2.join()).

    • Each thread has a dead state after completion.

  2. InterruptedException:

    • If a thread is interrupted while waiting, it throws an InterruptedException.

    • This must be handled using a try-catch block.

  3. Thread Scheduling:

    • Main thread waits for each task to complete using join().

    • Execution is sequential, preserving dependency.

Conceptual Flow

Venue Fixing -> Wedding Card Printing -> Wedding Card Distribution
    t1              t2                     t3
    |---------------|----------------------|
       t1.join()         t2.join()

Updated Code with Commented Sections

// Thread for Venue Fixing (Task 1)
class VenueFixing extends Thread {
    @Override
    public void run() {
        System.out.println("Venue Fixing: Task started...");
        try {
            Thread.sleep(2000); // Simulate time taken for venue fixing
        } catch (InterruptedException e) {
            System.out.println("Venue Fixing was interrupted!");
        }
        System.out.println("Venue Fixing: Task completed.");
    }
}

// Thread for Wedding Card Printing (Task 2)
class WeddingCardPrinting extends Thread {
    @Override
    public void run() {
        System.out.println("Wedding Card Printing: Task started...");
        try {
            Thread.sleep(3000); // Simulate time taken for card printing
        } catch (InterruptedException e) {
            System.out.println("Wedding Card Printing was interrupted!");
        }
        System.out.println("Wedding Card Printing: Task completed.");
    }
}

// Thread for Wedding Card Distribution (Task 3)
class WeddingCardDistribution extends Thread {
    @Override
    public void run() {
        System.out.println("Wedding Card Distribution: Task started...");
        try {
            Thread.sleep(1000); // Simulate time taken for distribution
        } catch (InterruptedException e) {
            System.out.println("Wedding Card Distribution was interrupted!");
        }
        System.out.println("Wedding Card Distribution: Task completed.");
    }
}

// Main class demonstrating thread dependencies
public class MarriagePlanning {
    public static void main(String[] args) throws InterruptedException {
        // Step 1: Initialize threads
        VenueFixing t1 = new VenueFixing();
        WeddingCardPrinting t2 = new WeddingCardPrinting();
        WeddingCardDistribution t3 = new WeddingCardDistribution();

        // Step 2: Start and synchronize threads using join()
        System.out.println("Wedding Preparation Begins...");

        t1.start(); // Start Venue Fixing
        t1.join();  // Main thread waits for t1 to complete
        System.out.println("Venue Fixing is complete. Proceeding to card printing...");

        t2.start(); // Start Wedding Card Printing
        t2.join();  // Main thread waits for t2 to complete
        System.out.println("Card Printing is complete. Proceeding to card distribution...");

        t3.start(); // Start Wedding Card Distribution
        t3.join();  // Main thread waits for t3 to complete
        System.out.println("All tasks completed. Wedding preparation is done!");
    }
}

Output

Wedding Preparation Begins...
Venue Fixing: Task started...
Venue Fixing: Task completed.
Venue Fixing is complete. Proceeding to card printing...
Wedding Card Printing: Task started...
Wedding Card Printing: Task completed.
Card Printing is complete. Proceeding to card distribution...
Wedding Card Distribution: Task started...
Wedding Card Distribution: Task completed.
All tasks completed. Wedding preparation is done!

Critical Notes Addressed

  1. Task Dependencies:
    The use of t1.join() and t2.join() ensures that the tasks execute in a specific sequence.

  2. Dead States:
    Each thread transitions to a dead state after completing its task and the main thread resumes its operation.

  3. InterruptedException Handling:
    Each Thread.sleep() call is wrapped in a try-catch block to handle interruptions gracefully.

  4. Sequential Flow with Messages:
    Console output clearly indicates the progress of each task and its dependency.

  5. Enhanced Realism:
    By simulating delays using Thread.sleep(), the scenario mirrors real-world multithreading behavior.


Visualization

Thread States

t1.start() -> t1 (Running) -> t1.join() -> t1 (Dead)
t2.start() -> t2 (Running) -> t2.join() -> t2 (Dead)
t3.start() -> t3 (Running) -> t3.join() -> t3 (Dead)

Key Learnings

  1. Sequential Execution with join():

    • Ensures task dependencies are respected.
  2. Checked Exception Handling:

    • The join() method throws InterruptedException, a checked exception that must be handled in code.
  3. Dead State:

    • After a thread completes its task, it transitions to the dead state.

Case-2 Explanation: Using public final void join(long ms) Method

In Case-2, we will use the join(long ms) method where the parent thread (main thread) will wait for the child thread to finish for a specified time (in this case, 1000 milliseconds).

The key observation here is that the parent thread does not completely wait for the child thread to finish. Instead, it will wait for the specified time (1 second) before resuming its execution.


Concept Explanation

  1. join(long ms) Method:

    • The join(long ms) method causes the parent thread to wait for a maximum of ms milliseconds for the child thread to finish.

    • If the child thread completes before ms milliseconds, the parent thread will resume execution immediately.

    • If the child thread takes longer than the specified time, the parent thread will continue after the specified timeout.

  2. Parent and Child Thread Execution:

    • The parent thread starts, then the child thread begins execution.

    • The parent thread calls t.join(1000), which makes it wait for 1 second (1000 milliseconds).

    • After the 1-second wait, the parent thread continues its execution, and the child thread may still be running (if it hasn’t completed within 1 second).


Code with Explanation and Numbering

// Step-1: Defining the Child Thread
class MyThread extends Thread {
    @Override
    public void run() {
        // Step-2: Child thread prints "Child thread" 5 times with a 2-second delay between each print
        for (int i = 0; i < 5; i++) {
            System.out.println("Child thread");
            try {
                // Step-3: Pausing the execution for 2 seconds (2000 milliseconds)
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                // Step-4: Handle InterruptedException if thrown
                System.out.println("Child thread interrupted!");
            }
        }
    }
}

// Step-5: Main Class with Main Method
public class MultithreadingJoinExample {
    public static void main(String[] args) throws InterruptedException {
        // Step-6: Create and start the child thread
        MyThread t = new MyThread();
        t.start();

        // Step-7: Main thread calls join(1000), causing it to wait for 1 second before continuing
        t.join(1000);  // Line-1: Parent thread waits for 1 second

        // Step-8: Main thread continues executing after 1 second
        for (int i = 0; i < 5; i++) {
            System.out.println("Parent thread");
        }
    }
}

Explanation of Execution

  1. Step-6 (Child Thread Creation and Start):

    • A new thread t is created from the MyThread class and started using t.start(). This triggers the run() method in the MyThread class.

    • Inside the run() method, the child thread will print "Child thread" 5 times, with a 2-second pause between each print.

  2. Step-7 (Parent Thread Waits for 1 Second):

    • After starting the child thread, the main (parent) thread calls t.join(1000). This tells the main thread to wait for the child thread to complete for 1000 milliseconds (1 second).

    • The main thread will not completely block, but it will pause for 1 second while the child thread is still running.

  3. Step-8 (Parent Thread Continues Execution):

    • After 1 second, the parent thread resumes execution and prints "Parent thread" 5 times, irrespective of whether the child thread has completed its execution or not.

    • The child thread, if it hasn’t finished its loop, will continue executing independently of the main thread.


Output:

Here’s the output when you run this code:

Child thread
Parent thread
Parent thread
Parent thread
Parent thread
Parent thread
Child thread
Child thread
Child thread
Child thread
Child thread

Key Observations

  1. Parent Thread Executes Before Child:

    • The parent thread prints "Parent thread" after 1 second, even though the child thread might not have finished its execution.
  2. Concurrency:

    • After 1 second, the parent thread continues execution independently from the child thread.
  3. Timing Difference:

    • Notice that the parent thread starts printing "Parent thread" immediately after waiting for 1 second, while the child thread prints "Child thread" based on its own sleep cycle (which is 2 seconds).
  4. Thread Scheduling:

    • Since thread scheduling is dependent on the operating system and JVM, you may see slight variations in the output order depending on how the threads are scheduled.

Behavior Comparison

MethodBehavior
t.join()The parent thread waits until the child thread completes all iterations.
t.join(1000)The parent thread waits for 1 second and then resumes independently.
No join (t.join())The output is unpredictable due to thread scheduler behavior.

Key Notes

  1. Predictability:

    • With t.join(1000), the parent thread executes after a predictable 1-second delay.

    • Without any join, the behavior depends on the thread scheduler.

  2. Concurrency:

    • This demonstrates a non-blocking join where the parent thread and child thread execute concurrently after the timeout.
  3. InterruptedException:

    • The InterruptedException must be handled as it’s a checked exception.
  4. Thread Scheduling:

    • Thread scheduling is determined by the operating system, which may cause slight variations in execution order.
  5. Unpredictable Behavior: If we change the join timeout or omit it, the thread scheduling may behave unpredictably, but the general pattern will remain consistent.

  6. InterruptedException Handling: Always handle InterruptedException when using Thread.sleep() or join() methods.


Conclusion

  • The join(long ms) method causes the parent thread to wait for up to ms milliseconds for the child thread to finish.

  • In this case, the parent thread waits for 1 second (join(1000)), then continues executing independently, while the child thread may still be running.

  • If t.join() (without a timeout) were used, the parent thread would wait for the child thread to complete completely before continuing execution.

Case-3 Explanation: Using public final void join(long ms, int ns) Method

In Case-3, we are using the join(long ms, int ns) method, where the parent thread is waiting for the child thread to finish its execution for a more precise amount of time: milliseconds and nanoseconds.

This method gives the parent thread more control over the time it waits for the child thread. It allows for specifying both milliseconds and nanoseconds, which can provide finer-grained control over waiting times.


Concept Explanation

  1. join(long ms, int ns) Method:

    • The join(long ms, int ns) method causes the current thread (parent thread) to wait for at most ms milliseconds and ns nanoseconds for the child thread to finish its execution.

    • This method is available in the java.lang.Thread class and is useful when you want to wait for a combination of milliseconds and nanoseconds before the parent thread proceeds.

  2. Parent and Child Thread Execution:

    • The parent thread starts, then the child thread begins execution.

    • The parent thread calls t.join(100, 10), which means it will wait for 100 milliseconds and 10 nanoseconds for the child thread to complete before it resumes execution.

    • After the specified time, the parent thread will continue, whether or not the child thread has finished its execution.


Code with Explanation and Numbering

// Step-1: Defining the Child Thread
class MyThread extends Thread {
    @Override
    public void run() {
        // Step-2: Child thread prints "Child thread" 5 times with a 2-second delay between each print
        for (int i = 0; i < 5; i++) {
            System.out.println("Child thread");
            try {
                // Step-3: Pausing the execution for 2 seconds (2000 milliseconds)
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                // Step-4: Handle InterruptedException if thrown
                System.out.println("Child thread interrupted!");
            }
        }
    }
}

// Step-5: Main Class with Main Method
public class MultithreadingJoinExample {
    public static void main(String[] args) throws InterruptedException {
        // Step-6: Create and start the child thread
        MyThread t = new MyThread();
        t.start();

        // Step-7: Main thread calls join(100, 10), causing it to wait for 100 milliseconds and 10 nanoseconds before continuing
        t.join(100, 10);  // Line-1: Parent thread waits for 100 milliseconds and 10 nanoseconds

        // Step-8: Main thread continues executing after waiting for the child thread
        for (int i = 0; i < 5; i++) {
            System.out.println("Parent thread");
        }
    }
}

Explanation of Execution

  1. Step-6 (Child Thread Creation and Start):

    • A new thread t is created from the MyThread class and started using t.start(). This triggers the run() method in the MyThread class.

    • Inside the run() method, the child thread will print "Child thread" 5 times, with a 2-second pause between each print.

  2. Step-7 (Parent Thread Waits for 100 ms and 10 ns):

    • After starting the child thread, the main (parent) thread calls t.join(100, 10). This tells the main thread to wait for 100 milliseconds and 10 nanoseconds for the child thread to complete.

    • The parent thread will wait for this exact time, after which it will resume its execution, regardless of whether the child thread is finished or not.

  3. Step-8 (Parent Thread Continues Execution):

    • After the 100 milliseconds and 10 nanoseconds, the parent thread resumes and prints "Parent thread" 5 times.

    • Meanwhile, the child thread may still be running, and its output will be printed afterward.


Output:

Here’s the output when you run this code:

Child thread
Parent thread
Parent thread
Parent thread
Parent thread
Parent thread
Child thread
Child thread
Child thread
Child thread

Key Observations

  1. Parent Thread Executes After Waiting for the Specified Time:

    • The parent thread will start printing "Parent thread" after waiting for exactly 100 milliseconds and 10 nanoseconds.

    • While the parent thread waits, the child thread continues its execution, printing "Child thread" 5 times.

  2. Child Thread's Longer Execution:

    • The child thread prints "Child thread" 5 times, but it executes for longer than the parent thread’s wait time (because of the Thread.sleep(2000)).

    • This means that the parent thread may finish printing "Parent thread" before the child thread completes its loop.

  3. Precise Waiting Time:

    • The use of milliseconds and nanoseconds in join(long ms, int ns) provides more precise control over the waiting time compared to just using milliseconds.
  4. Thread Scheduling:

    • The exact sequence of printed statements can depend on thread scheduling by the JVM and operating system, so you might see slight variations in the exact order of "Child thread" and "Parent thread" outputs.

Conclusion

  • The join(long ms, int ns) method gives the parent thread more precise control over the wait time by allowing it to specify both milliseconds and nanoseconds.

  • After waiting for the specified time, the parent thread continues executing, while the child thread may still be running or may have completed its execution.

  • This method is useful when you need to wait for a very specific time before the parent thread proceeds, such as when working with tasks that require fine-grained synchronization.


Key Notes:

  1. Unpredictable Behavior: As always, thread scheduling is dependent on the JVM and OS, so the exact order of outputs can vary slightly across different executions.

  2. Interrupt Handling: Make sure to handle InterruptedException when using Thread.sleep() or join() methods.


Case-4 Explanation: Child Thread Waiting for Parent Thread

In Case-4, we explore a scenario where the child thread waits for the parent (main) thread to complete its execution. This is achieved using the join() method in the child thread, where the child thread waits for the parent thread to finish before it starts or continues its execution.

Here’s a step-by-step explanation:


Concept Explanation

  1. Child Thread Waiting for Parent Thread:

    • In this case, the child thread is not executed immediately; instead, it waits for the parent (main) thread to complete.

    • The parent thread starts first and the child thread waits for the parent to finish by calling join() on the parent thread’s reference.

  2. Using the join() Method in the Child Thread:

    • The join() method is called from the child thread’s run() method to ensure that the child thread waits for the parent thread to complete before proceeding.

    • This means that the child thread will only execute after the parent thread has finished its task, ensuring the parent thread executes first.

  3. Thread Synchronization:

    • This approach is a form of thread synchronization where one thread (the child) waits for another thread (the parent) to finish before proceeding.

Code Explanation with Numbering

// Step-1: Defining the Child Thread Class
class MyThread extends Thread {
    static Thread mt;  // Step-2: Static reference to the Parent thread

    @Override
    public void run() {
        try {
            // Step-3: Child thread waits for the Parent thread to finish
            mt.join();  // Child thread waits for Parent thread
        } catch (InterruptedException e) {
            // Step-4: Handle InterruptedException if it occurs
            e.printStackTrace();
        }

        // Step-5: Child thread prints "child thread" 5 times after waiting for Parent thread
        for (int i = 1; i <= 5; i++) {
            System.out.println("child thread");
        }
    }
}

// Step-6: Main Class to Run the Threads
public class Multithreading38 {
    public static void main(String[] args) throws InterruptedException {
        // Step-7: The reference of the Parent thread is set
        MyThread.mt = Thread.currentThread();  // Parent thread (main thread) reference

        // Step-8: Create and start the Child thread
        MyThread t = new MyThread();
        t.start();  // Child thread starts

        // Step-9: Parent thread sleeps for 2 seconds, allowing the Child thread to execute after waiting
        for (int i = 0; i < 5; i++) {
            Thread.sleep(2000);  // Parent thread sleeps for 2 seconds
            System.out.println("Parent thread");  // Parent thread prints
        }
    }
}

Explanation of Execution Flow

  1. Step-7 (Setting Parent Thread Reference):

    • In the main method, the reference of the parent thread (main thread) is set to the static field mt of the MyThread class. This allows the child thread to access the parent thread reference later.
  2. Step-8 (Creating and Starting the Child Thread):

    • A new MyThread object (t) is created, and the start() method is called to begin the execution of the child thread.
  3. Step-9 (Parent Thread Sleeps):

    • The parent thread (main thread) enters a loop and prints "Parent thread" five times, with a 2-second delay between each print (Thread.sleep(2000)).
  4. Step-3 (Child Thread Waits for Parent Thread):

    • Inside the child thread's run() method, the mt.join() method is called. Since mt refers to the parent thread (the main thread), the child thread waits for the parent thread to finish before it proceeds.
  5. Step-5 (Child Thread Prints):

    • After the parent thread finishes its loop and printing, the child thread resumes and prints "child thread" five times.

Output

Here is the expected output when running this code:

Parent thread
Parent thread
Parent thread
Parent thread
Parent thread
child thread
child thread
child thread
child thread
child thread

Key Observations

  1. Parent Thread Executes First:

    • The parent thread prints "Parent thread" five times, with a 2-second interval between each print. This happens before the child thread starts executing.
  2. Child Thread Waits for Parent Thread:

    • The child thread waits for the parent thread to finish before it starts printing "child thread". The call to mt.join() ensures that the parent thread completes its task first.
  3. Synchronization:

    • This case demonstrates thread synchronization, where the child thread waits for the parent thread to finish using the join() method. This is useful when the execution order of threads is important.

Conclusion

In Case-4, we’ve created a situation where the child thread waits for the parent thread to finish using the join() method. The child thread does not proceed until the parent thread completes its execution. This ensures a specific execution order, where the parent thread executes first and the child thread executes afterward.


Important Notes:

  1. Thread Synchronization: This approach guarantees that the parent thread completes its task before the child thread starts, which can be crucial in certain scenarios where the child depends on the completion of the parent.

  2. Handling InterruptedException: Always handle InterruptedException when working with thread-related methods like join() and sleep() to prevent unexpected behavior when the thread is interrupted.

Case-5: Deadlock Caused by join() Method

In Case-5, we explore a deadlock scenario caused by the improper use of the join() method. Deadlock occurs when two threads are each waiting for the other to finish, causing both threads to be stuck in an infinite wait state.


Concept Explanation

  1. Deadlock in Multithreading:

    • A deadlock happens when a thread waits indefinitely for a condition that cannot be met because it’s also waiting on that thread. In this case, if a thread waits for itself to finish, it will never proceed, leading to a deadlock.

    • Deadlock occurs in our example when a thread calls join() on itself. This results in the thread waiting for itself to finish, which is impossible, and causes an infinite wait state.

  2. Join on the Same Thread:

    • When a thread calls join() on itself, like Thread.currentThread().join(), the thread waits for itself to complete, which creates a situation where the thread is stuck indefinitely, causing infinite waiting.

Code Explanation with Numbering

// Example-1: Deadlock due to Thread joining with itself

// Step-1: Child thread class extends Thread
class MyThread extends Thread {
    static Thread mt; // Static reference to parent (main) thread

    @Override
    public void run() {
        try {
            // Step-2: Child thread waits for parent thread to finish using join
            mt.join();  // Deadlock happens here: Parent thread joins with itself
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // Step-3: Print "child thread" 5 times after waiting for parent thread
        for (int i = 1; i <= 5; i++) {
            System.out.println("child thread");
        }
    }
}

// Step-4: Main class
public class Multithreading39 {
    public static void main(String[] args) throws InterruptedException {
        // Step-5: Main thread attempts to join with itself, causing a deadlock
        Thread.currentThread().join();  // Main thread joins with itself
    }
}

Explanation of Execution Flow

  1. Step-5 (Main Thread Joins with Itself):

    • The main thread calls Thread.currentThread().join(), which means the main thread is waiting for itself to finish. This is an infinite wait because a thread cannot complete while waiting for itself.
  2. Deadlock Situation:

    • Since the main thread is stuck waiting for itself, it will never finish, and the program will not produce any output. This is a deadlock, where the main thread is waiting indefinitely, causing the program to freeze.
  3. No Output:

    • The program does not proceed to any further execution (e.g., printing anything) because the main thread is stuck in the join() method, waiting for itself, which never happens.

Output for Example-1

(no output, program hangs indefinitely)

Example-2: Deadlock Between Parent and Child Threads

In this example, we explore a case where a parent (main) thread and child thread are both trying to wait for each other to finish. This results in a deadlock because both threads are stuck waiting for the other to finish, and neither can proceed.


Code Explanation for Example-2

// Example-2: Deadlock between main thread and child thread

// Step-1: Child thread class extends Thread
class MyThread extends Thread {
    static Thread mt; // Static reference to parent (main) thread

    @Override
    public void run() {
        try {
            // Step-2: Child thread waits for the main thread to finish using join
            mt.join();  // Deadlock happens here: Child thread joins with main thread
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // Step-3: Child thread prints "child thread" 10 times
        for (int i = 1; i <= 10; i++) {
            System.out.println("child thread");
        }
    }
}

// Step-4: Main class
public class Multithreading39 {
    public static void main(String... args) throws InterruptedException {
        // Step-5: Parent (main) thread sets its reference
        MyThread.mt = Thread.currentThread();  // Main thread reference

        // Step-6: Create and start the child thread
        MyThread t = new MyThread();
        t.start();

        // Step-7: Main thread waits for the child thread to finish using join
        t.join();  // Main thread waits for child thread

        // Step-8: Main thread prints "main thread" 10 times
        for (int i = 1; i <= 10; i++) {
            System.out.println("main thread");
            Thread.sleep(2000);  // Main thread sleeps for 2 seconds
        }
    }
}

Explanation of Execution Flow for Example-2

  1. Step-5 (Parent (Main) Thread Sets Its Reference):

    • The main thread sets its reference in MyThread.mt, which allows the child thread to access it.
  2. Step-6 (Creating and Starting the Child Thread):

    • A child thread is created and started using t.start().
  3. Step-7 (Main Thread Joins with the Child Thread):

    • The main thread calls t.join(), which causes it to wait for the child thread to finish.
  4. Step-2 (Child Thread Joins with the Main Thread):

    • Inside the child thread’s run() method, the child calls mt.join(), which is the main thread, causing the child to wait for the main thread to finish.
  5. Deadlock Situation:

    • The main thread waits for the child thread to finish, but the child thread is waiting for the main thread to finish. As a result, both threads are stuck in an infinite wait, causing a deadlock.
  6. No Output:

    • Since both threads are stuck waiting for each other, the program does not proceed to print anything, resulting in a blank output.

Output for Example-2

(blank output, program hangs indefinitely)

Key Observations

  1. Infinite Wait (Deadlock):

    • In both examples, the program results in deadlock because a thread waits for itself (Example 1) or both threads wait for each other (Example 2), leading to infinite waiting.
  2. Avoiding Deadlock:

    • When using join(), make sure that a thread is not joining itself and that threads do not create circular dependencies by waiting for each other. In multithreading, always ensure that the waiting time is well-defined and avoid situations where threads can end up in infinite loops.
  3. No Output Due to Deadlock:

    • Both examples demonstrate that when deadlock occurs, no output is produced because the threads are stuck in the waiting state.

Conclusion

Deadlock is a common issue when working with multithreading. It can occur when threads wait indefinitely for each other to complete. In both examples above, we saw how joining a thread with itself or having two threads wait for each other results in an infinite wait, leading to deadlock. Always ensure proper thread synchronization to avoid such situations in multithreading applications.

Life of a Thread When join() Method is Called

When the join() method is called on a thread, it changes the state of the calling thread, typically causing it to enter a waiting state until the thread that it called join() on completes its execution. The thread can move between different states based on certain conditions like completion, timeout, or interruption. Below, we'll break down how the thread moves through different states and what possibilities exist when using the join() method.


Thread Lifecycle with join()

  1. Thread States:

    • Ready/Runnable State: The thread is ready to run or is actively running.

    • Waiting State: The thread is not running but is waiting for some condition (like the completion of another thread) to become runnable again.

    • Running State: The thread is currently executing instructions.

When a thread calls the join() method, it enters the waiting state and will remain there until the thread it is waiting on either finishes execution or the wait time expires (if a time-bound join() is used).


Possibilities of Thread Entering Waiting State with join()

  1. t.join():

    • This makes the calling thread wait until the thread t has finished executing.

    • The thread will stay in the waiting state until thread t completes its run.

  2. t.join(100):

    • This causes the calling thread to wait for a maximum of 100 milliseconds.

    • If thread t finishes execution before the timeout, the calling thread will move to the runnable state. If the timeout of 100 milliseconds elapses, the calling thread will also move to the runnable state.

  3. t.join(100, 10):

    • This specifies a timeout of 100 milliseconds and 10 nanoseconds.

    • After the timeout (100ms and 10ns), the calling thread will return to the runnable state, whether or not the thread t has completed.


Thread Movement Between States

When calling the join() method, the thread goes through a few state transitions:

  1. Ready/Runnable State → Waiting State (after calling join())

    • Initially, the calling thread is in the ready/runnable state.

    • After calling join(), it enters the waiting state until the thread it is waiting on finishes its execution.

  2. Waiting State → Ready/Runnable State

    • The calling thread will return to the runnable state in the following cases:

      • Thread t finishes execution: The calling thread will be notified that thread t has completed, and it can now resume.

      • Timeout expires: If the timeout passed to join() (in milliseconds or nanoseconds) elapses, the calling thread moves to the runnable state.

      • Interruption occurs: If the calling thread is interrupted while waiting, it will move back to the runnable state. The interruption causes the thread to exit the waiting state prematurely.


Example of join() in Action

class MyThread extends Thread {
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("Child thread executing");
            try {
                Thread.sleep(1000);  // Simulate work
            } catch (InterruptedException e) {
                System.out.println("Thread interrupted");
            }
        }
    }
}

public class ThreadJoinExample {
    public static void main(String[] args) throws InterruptedException {
        MyThread t = new MyThread(); // Creating child thread
        t.start(); // Start the thread

        System.out.println("Main thread before join");

        // Main thread waits for child thread to finish using join
        t.join(); // Main thread enters the waiting state

        // Main thread resumes after child thread finishes execution
        System.out.println("Main thread after join");
    }
}

Explanation of the Example:

  1. Thread Creation:

    • A new thread t is created by extending Thread and overriding the run() method.
  2. Thread Start:

    • The child thread (t) starts execution using t.start().
  3. Main Thread Calling join():

    • The main thread calls t.join(), which causes the main thread to enter the waiting state until thread t completes.
  4. Thread Resumption:

    • After the child thread finishes, the main thread resumes execution and prints "Main thread after join".

Key Scenarios When Thread Moves from Waiting to Runnable

  1. When t finishes execution:

    • The calling thread will move from the waiting state back to the runnable state when the thread it is waiting for finishes.
  2. When the specified timeout (in join(long millis) or join(long millis, int nanos)) expires:

    • The calling thread will be notified after the timeout and can resume execution, moving to the runnable state.
  3. When the thread is interrupted:

    • If the thread is interrupted while waiting, it will exit the waiting state and move to the runnable state. An InterruptedException will be thrown.

Sleep Example with join()

The sleep() method is often used in conjunction with join() to simulate time delays. The calling thread can be interrupted during sleep or while waiting for another thread to finish, leading to state transitions.

class MyThread extends Thread {
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("Child thread executing");
            try {
                Thread.sleep(1000);  // Simulate work
            } catch (InterruptedException e) {
                System.out.println("Thread interrupted");
            }
        }
    }
}

public class ThreadSleepJoinExample {
    public static void main(String[] args) throws InterruptedException {
        MyThread t = new MyThread();
        t.start(); // Start child thread

        t.join(3000); // Main thread waits for 3 seconds or until child finishes

        System.out.println("Main thread resumed after 3 seconds or child completion");
    }
}

In this example:

  • The main thread calls t.join(3000) to wait for at most 3 seconds.

  • The child thread is sleeping for 1 second at a time in its run() method.

  • The main thread will wait for the child thread for up to 3 seconds, but will resume after that if the child thread hasn't completed.


Conclusion

The join() method is used to ensure that one thread waits for another thread to finish before continuing its execution. Depending on the variant of join() used (with or without time limits), the calling thread can enter a waiting state for a specified period or indefinitely. Upon completion of the target thread, timeout, or interruption, the calling thread will move back to the runnable state.

  • Without timeout (join()): The calling thread waits indefinitely until the target thread finishes.

  • With timeout (join(long millis)): The calling thread waits for the specified time or until the target thread finishes.

  • With nanosecond precision (join(long millis, int nanos)): The calling thread waits for the specified time and nanoseconds.

Understanding how threads move between these states is crucial for writing efficient and bug-free multithreaded applications.

Understanding the sleep() Method in Java

The sleep() method in Java is used to pause the execution of the current thread for a specified period of time. It is part of the Thread class, and it allows you to control the timing and delay of thread execution.


When to Use sleep()

  • Purpose: If a thread does not need to perform any operation for a particular amount of time, you can use the sleep() method to pause its execution.

  • Example Use Case: One common use case is a timer (like in PowerPoint presentations), where after showing a slide, the thread pauses for a few seconds before displaying the next one.


Signatures of the sleep() Method

  1. public static void sleep(long ms) throws InterruptedException:

    • This method puts the current thread to sleep for the specified number of milliseconds.

    • It throws an InterruptedException if another thread interrupts the sleeping thread.

  2. public static void sleep(long ms, int ns) throws InterruptedException:

    • This method puts the current thread to sleep for the specified milliseconds and nanoseconds.

    • It also throws InterruptedException if interrupted.


Important Points to Note About sleep()

  • Checked Exception: Both versions of sleep() throw an InterruptedException, which is a checked exception. Therefore, it must either be caught using a try-catch block or declared using the throws keyword.

  • Thread States:

    • New (Born) State: The thread is created but not yet started.

    • Ready/Runnable State: The thread is ready to run and waiting for CPU allocation.

    • Running State: The thread is currently executing.

    • Sleeping State: The thread is in the sleeping state and is not performing any operations during the specified sleep time.

    • Dead State: The thread has finished execution and can no longer be started.


Thread Flow with sleep()

  • When a thread calls sleep(), it enters the sleeping state for the specified duration.

  • After the time expires or if the thread is interrupted, the thread returns to the ready/runnable state and waits for CPU allocation to continue execution.


Example 1: Using sleep() in a Sequence

public class Multithreading41 {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("R");
        Thread.sleep(3000); // Sleep for 3 seconds
        System.out.println("C");
        Thread.sleep(3000); // Sleep for 3 seconds
        System.out.println("B");
        Thread.sleep(3000); // Sleep for 3 seconds

        System.out.println("Kohli");
    }
}

Output:

R
C
B
Kohli

Explanation:

  • The program prints "R", then sleeps for 3 seconds. After that, it prints "C", sleeps again, and so on.

  • Each Thread.sleep(3000) pauses the execution for 3 seconds, resulting in a delay between the printed statements.


Example 2: Using sleep() in a Loop

public class Multithreading41 {
    public static void main(String[] args) throws InterruptedException {
        for (int i = 1; i <= 10; i++) {
            System.out.println("Slide: " + i);
            Thread.sleep(2000); // Sleep for 2 seconds
        }
    }
}

Output:

Slide: 1
Slide: 2
Slide: 3
Slide: 4
Slide: 5
Slide: 6
Slide: 7
Slide: 8
Slide: 9
Slide: 10

Explanation:

  • The program prints each slide number with a 2-second delay between each print.

  • The sleep(2000) method pauses the program for 2 seconds after printing each slide number, giving a delay between iterations.


Thread Lifecycle with sleep()

  1. Initial State: The thread starts in the ready/runnable state after being created and started.

  2. Sleeping State: When the thread invokes sleep(), it enters the sleeping state.

  3. Returning to Ready/Runnable: After the sleep time expires (or if interrupted), the thread returns to the ready/runnable state.

  4. Completion: Once the thread finishes execution, it enters the dead state.


Conclusion

The sleep() method in Java is a useful tool for controlling thread execution, pausing it for a specified amount of time. It is important to handle the InterruptedException when using sleep() to avoid compile-time errors. Understanding the thread lifecycle and state transitions (from running to sleeping and back to ready/runnable) is essential for creating efficient and controlled multi-threaded applications.

Life Cycle of a Thread When sleep() Method is Called

The sleep() method in Java affects a thread's life cycle by causing it to pause execution for a specified time. Understanding how the thread transitions between states when using sleep() is crucial for handling multi-threading correctly.


1. Transition to Sleeping State

When a thread invokes the sleep() method, it moves into the sleeping state. This happens in the following situations:

  • Using join(): When you call join() on a thread, the calling thread (the one that invokes join()) will wait until the other thread completes its execution. If a specific time limit is provided (e.g., thread.join(1000)), the calling thread sleeps for that amount of time.

    • Example:

        thread.join(1000); // The thread waits for 1 second
      
  • Using sleep(): The thread explicitly sleeps for a specified amount of time using the sleep() method.

    • Example:

        Thread.sleep(1000); // The thread sleeps for 1000 milliseconds (1 second)
      

2. Signatures of sleep() Method

Java provides two variations of the sleep() method to control how long the thread should pause:

  1. public static void sleep(long ms) throws InterruptedException

    • This version makes the current thread sleep for a specified number of milliseconds.

    • It throws InterruptedException if the thread is interrupted while sleeping.

  2. public static void sleep(long ms, int ns) throws InterruptedException

    • This version makes the current thread sleep for a specified milliseconds (ms) and nanoseconds (ns).

    • Again, it throws InterruptedException if interrupted.


3. Transition from Sleeping State to Ready/Runnable State

A thread can exit the sleeping state in the following situations:

  1. Time Expiration: The thread wakes up automatically once the specified sleep time has passed.

    • Example: If a thread sleeps for 1000 milliseconds (1 second), it will return to the ready/runnable state after that duration, unless interrupted.
  2. Thread Interruption: If another thread interrupts the sleeping thread, it will immediately transition back to the ready/runnable state, even if the sleep time hasn't fully elapsed.

    • Example:

        Thread t = new Thread(() -> {
            try {
                Thread.sleep(5000); // Sleep for 5 seconds
            } catch (InterruptedException e) {
                System.out.println("Thread was interrupted");
            }
        });
        t.start();
        t.interrupt();  // Interrupting the thread before it finishes sleeping
      

Thread Lifecycle with sleep()

  • Ready/Runnable State: The thread starts in the ready/runnable state after being created and started.

  • Running State: Once the CPU scheduler picks the thread, it enters the running state and starts executing.

  • Sleeping State: When sleep() is called, the thread transitions to the sleeping state.

  • Returning to Ready/Runnable: After the sleep time expires or the thread is interrupted, it moves back to the ready/runnable state, waiting for the CPU to allocate time to continue execution.

  • Dead State: Once the thread completes execution, it moves to the dead state and can no longer be started.


Visual Representation

Ready/Runnable state  <--->  Sleeping state  <--->  Running state
    |                           |                        |
    |---> sleep()                |---> time expires       |
    |                           |---> interrupted         |
    |---> join() with timeout    |
  1. The thread starts in the ready/runnable state and begins execution in the running state.

  2. When sleep() or join() with a timeout is called, the thread transitions to the sleeping state.

  3. After the sleep time expires, or if the thread is interrupted, it returns to the ready/runnable state.


Conclusion

The sleep() method is essential for pausing the execution of a thread for a specific period. It allows threads to pause and then return to the ready/runnable state once the time expires or if they are interrupted. Understanding how this method interacts with the thread lifecycle helps in creating efficient multi-threaded applications and prevents issues like unnecessary CPU usage or unresponsiveness.

Conclusion for Chapter 40: Multithreading in Java (Part 3)

In this chapter, we've gained a deeper understanding of thread management in Java, focusing on thread priorities, synchronization with the join() method, and managing thread execution with the sleep() method. We explored how thread priorities influence scheduling, how to control thread execution order using join(), and how to pause threads with sleep() while ensuring efficient multitasking. Through practical examples, we've learned to avoid issues like deadlocks and interruptions, ensuring smoother multithreaded applications. Mastering these concepts equips you with the tools to handle complex concurrency challenges in Java programming.

Other Series:


Connect with Me
Stay updated with my latest posts and projects by following me on social media:

  • LinkedIn: Connect with me for professional updates and insights.

  • GitHub: Explore my repository and contributions to various projects.

  • LeetCode: Check out my coding practice and challenges.

Your feedback and engagement are invaluable. Feel free to reach out with questions, comments, or suggestions. Happy coding!


Rohit Gawande
Full Stack Java Developer | Blogger | Coding Enthusiast