Joining Threads in Java

In Java, the join() method serves the purpose of pausing the execution of one thread until another thread has finished its work. This is valuable when you need to ensure a particular sequence of tasks or when you require the outcome of one thread’s operation before continuing with the main thread or other threads. Essentially, the join() method lets one thread wait until another thread completes its task, enabling synchronization and orderly execution of threads.

Example:

By only getting ingredients for the biryani, we aren’t able to eat the biryani directly. We only got to eat biryani when the meal was done.

Cooking Biryani: Preparing the ingredients for biryani is like starting multiple threads in a program. Each thread represents an individual task, and they may execute concurrently.

Eating Biryani: Eating the biryani is like the main thread’s execution. The main thread represents the main program or application. The main thread may need the results of the other threads’ computations before proceeding with its own task.

Joining Threads: Joining threads is like waiting for the cooking process to complete before eating the biryani. When a thread joins another thread, it waits for that thread to finish its execution. The main thread (or any other thread that calls join()) waits until the joined thread(s) complete their tasks before continuing with its own operations.

Life Cycle of Thread with join():

Life Cycle of Thread with join

New State: The thread is in the “New” state when it is created, but the start() method has not been called yet. At this stage, the thread has not started executing.

Runnable State: After calling the start() method, the thread transitions to the “Runnable” state. It is eligible to be scheduled for execution by the thread scheduler, but it may not be currently running.

Running State: If the thread scheduler selects the thread for execution, it moves into the “Running” state. The run() method is being executed.

Blocked State (Joining Thread): When the current thread (let’s call it Thread A) calls the join() method on another thread (let’s call it Thread B), Thread A enters the “Blocked” state. Thread A waits for Thread B to complete its execution before resuming its own execution.

Runnable State (Again): After Thread B completes its execution or terminates, Thread A transitions back to the “Runnable” state. It becomes eligible for execution again.

Running State (Again): If the thread scheduler selects Thread A for execution, it moves back into the “Running” state, and its execution continues.

Terminated State: Eventually, the run() method of Thread A finishes its execution, and the thread enters the “Terminated” state. At this point, the thread’s life cycle ends.

Example Program:

public class Main {
    public static void main(String[] args) throws InterruptedException{
        MyThread t = new MyThread();
        t.start();//starting the thread
        t.join(2000);//calling the join()
        for(int i=0;i<3;i++){
            System.out.println("TechVidvan-Main Thread");
        }
    }
}
class MyThread extends Thread{
    public void run(){
        for(int i=0;i<3;i++){
            System.out.println("TechVidvan-Thread1");
        }
    }
}

Output:

TechVidvan-Thread1

TechVidvan-Thread1

TechVidvan-Thread1

TechVidvan-Main Thread

TechVidvan-Main Thread

TechVidvan-Main Thread

The main thread starts MyThread by calling t.start().MyThread starts running concurrently, and it prints “TechVidvan-Thread1” three times. The main thread calls t.join(2000), which means it will wait for a maximum of 2000 milliseconds (2 seconds) for MyThread to complete its execution. The main thread is blocked until MyThread finishes or until the specified timeout of 2000 milliseconds elapses, whichever occurs first. In this case, MyThread completes its execution before the 2000 milliseconds are over. Once MyThread completes, the main thread continues executing the for loop, and it prints “TechVidvan-Main Thread” three times. The join(2000) method blocks the main thread for at most 2000 milliseconds. If MyThread completes its execution before the timeout (2000 milliseconds), the main thread continues immediately. If MyThread does not complete within the timeout, the main thread resumes after the specified time, regardless of whether MyThread has finished or not.

Overloading join():

class JoinExample {
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> {
            try {
                Thread.sleep(3000);
                System.out.println("TechVidvan Thread 1 completed.");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        Thread thread2 = new Thread(() -> {
            try {
                Thread.sleep(2000);
                System.out.println("TechVidvan Thread 2 completed.");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        Thread thread3 = new Thread(() -> {
            try {
                Thread.sleep(1000);
                System.out.println("TechVidvan Thread 3 completed.");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        thread1.start();
        thread2.start();
        thread3.start();
        // Using join() to wait for threads to complete
        thread1.join(); // Wait for thread1 to complete
        System.out.println("Main thread resumed after thread1.");
        thread2.join(1500); // Wait for thread2 to complete or 1.5 seconds
        System.out.println("Main thread resumed after thread2 (or timeout).");
        thread3.join(1000, 500000); // Wait for thread3 to complete or 1 second and 500,000 nanoseconds
        System.out.println("Main thread resumed after thread3 (or timeout).");
        System.out.println("Main thread finished.");
    }
}

Output:

TechVidvan Thread 3 completed.

TechVidvan Thread 2 completed.

TechVidvan Thread 1 completed.

Main thread resumed after thread1.

Main thread resumed after thread2 (or timeout).

Main thread resumed after thread3 (or timeout).

Main thread finished.

In this example:

  • Three threads (thread1, thread2, and thread3) are started, each simulating some work with different durations.
  • The join() method is used in three different ways:

1. thread1.join() waits for thread1 to complete.

2. thread2.join(1500) waits for thread2 to complete or a maximum of 1.5 seconds.

3. thread3.join(1000, 500000) waits for thread3 to complete or a maximum of 1 second and 500,000 nanoseconds.

  • After each join() operation, the main thread prints a message indicating whether it resumed after waiting for the thread to complete or due to a timeout.
  • The output demonstrates how the join() method allows the main thread to synchronize with the completion of the other threads and control the order of execution.
  • Remember that actual execution times can vary due to the nature of thread scheduling, so the exact output might differ each time you run the example.

Summary

The join() method ensures that the calling thread (Thread A) waits for the specified thread (Thread B) to finish before it proceeds. This is useful when you want to perform tasks in a specific order or when you need the result of Thread B’s computation before continuing with Thread A’s operations.