In Java, joining threads allows one thread to wait for the completion of another thread before proceeding.
The Thread.join() method is crucial for ensuring that certain operations occur after other threads have finished executing.
This is especially useful in scenarios where you need to synchronize thread execution or coordinate the result of multiple threads.
The join() method blocks the calling thread until the thread on which join() was called terminates. There are three variations of the join() method:
join() โ Waits indefinitely until the thread completes.
join(long millis) โ Waits for the specified time (in milliseconds) for the thread to finish.
join(long millis, int nanos) โ Waits for the specified time (in milliseconds and nanoseconds) for the thread to finish.
1. Basic Example of Thread.join()
In this first example, we will demonstrate how the main thread can wait for a secondary thread to complete using the join() method.
Example 1: Basic Join
class MyThread extends Thread { @Override public void run() { for (int i = 1; i <= 5; i++) { System.out.println("Thread: " + i); try { Thread.sleep(500); // Sleep for 500 milliseconds } catch (InterruptedException e) { System.out.println(e); } } } } public class JoinExample1 { public static void main(String[] args) { MyThread thread = new MyThread(); thread.start(); try { thread.join(); // Main thread waits for thread to finish } catch (InterruptedException e) { System.out.println(e); } System.out.println("Main thread has finished after child thread."); } }
Output:
Thread: 1 Thread: 2 Thread: 3 Thread: 4 Thread: 5
Main thread has finished after child thread.
Explanation:
The join() method ensures that the main thread waits for the MyThread to complete before printing “Main thread has finished after child thread.”.
The main thread will only continue once the child thread has finished its execution.
2. Using join(long millis) to Specify Wait Time
The join(long millis) method allows the main thread to wait for a specific amount of time before it resumes execution.
If the specified time elapses before the thread finishes, the main thread continues without waiting for the child thread to finish.
Example 2: Using join(long millis)
class MyThreadWithDelay extends Thread { @Override public void run() { for (int i = 1; i <= 5; i++) { System.out.println("Thread: " + i); try { Thread.sleep(1000); // Sleep for 1 second } catch (InterruptedException e) { System.out.println(e); } } } } public class JoinExample2 { public static void main(String[] args) { MyThreadWithDelay thread = new MyThreadWithDelay(); thread.start(); try { thread.join(2500); // Main thread waits for 2.5 seconds } catch (InterruptedException e) { System.out.println(e); } System.out.println("Main thread proceeds after 2.5 seconds."); } }
Output:
Thread: 1 Thread: 2 Main thread proceeds after 2.5 seconds. Thread: 3 Thread: 4 Thread: 5
Explanation:
The join(2500) makes the main thread wait for 2.5 seconds. After this time, the main thread resumes execution, regardless of whether the child thread has finished.
In this case, the child thread continues running, but the main thread proceeds after the specified waiting time.
3. Using join(long millis, int nanos) for Higher Precision
The join(long millis, int nanos) method allows even finer control over the waiting time, providing additional precision using nanoseconds.
Example 3: Using join(long millis, int nanos)
class MyThreadWithNanoDelay extends Thread { @Override public void run() { for (int i = 1; i <= 5; i++) { System.out.println("Thread: " + i); try { Thread.sleep(1000); // Sleep for 1 second } catch (InterruptedException e) { System.out.println(e); } } } } public class JoinExample3 { public static void main(String[] args) { MyThreadWithNanoDelay thread = new MyThreadWithNanoDelay(); thread.start(); try { thread.join(1000, 500000); // Main thread waits for 1.5 seconds (1 second and 500,000 nanoseconds) } catch (InterruptedException e) { System.out.println(e); } System.out.println("Main thread proceeds after 1.5 seconds."); } }
Output:
Thread: 1 Thread: 2 Main thread proceeds after 1.5 seconds. Thread: 3 Thread: 4 Thread: 5
Explanation:
The join(1000, 500000) makes the main thread wait for approximately 1.5 seconds.
After the waiting time elapses, the main thread proceeds with execution, even if the child thread has not yet finished.
4. Joining Multiple Threads
You can also use the join() method to synchronize multiple threads. In this case, the main thread can wait for several threads to finish before proceeding.
Example 4: Joining Multiple Threads
class WorkerThread extends Thread { private String name; public WorkerThread(String name) { this.name = name; } @Override public void run() { for (int i = 1; i <= 3; i++) { System.out.println(name + ": " + i); try { Thread.sleep(500); // Sleep for 500 milliseconds } catch (InterruptedException e) { System.out.println(e); } } } } public class JoinExample4 { public static void main(String[] args) { WorkerThread thread1 = new WorkerThread("Thread 1"); WorkerThread thread2 = new WorkerThread("Thread 2"); WorkerThread thread3 = new WorkerThread("Thread 3"); thread1.start(); thread2.start(); thread3.start(); try { thread1.join(); // Wait for thread1 to finish thread2.join(); // Wait for thread2 to finish thread3.join(); // Wait for thread3 to finish } catch (InterruptedException e) { System.out.println(e); } System.out.println("Main thread proceeds after all threads are finished."); } }
Output:
Thread 1: 1 Thread 2: 1 Thread 3: 1 Thread 1: 2 Thread 2: 2 Thread 3: 2 Thread 1: 3 Thread 2: 3 Thread 3: 3 Main thread proceeds after all threads are finished.
Explanation:
The main thread waits for thread1, thread2, and thread3 to finish execution before proceeding.
By calling join() on each thread, the main thread ensures that all worker threads complete before it moves on.
5. Handling Interrupted Threads with join()
When using join(), a thread may be interrupted. Itโs important to handle this scenario using the InterruptedException.
Example 5: Handling Interrupted Threads
class InterruptibleThread extends Thread { @Override public void run() { for (int i = 1; i <= 3; i++) { System.out.println("Thread: " + i); try { Thread.sleep(500); // Sleep for 500 milliseconds } catch (InterruptedException e) { System.out.println("Thread was interrupted!"); return; } } } } public class JoinExample5 { public static void main(String[] args) { InterruptibleThread thread = new InterruptibleThread(); thread.start(); try { thread.join(); } catch (InterruptedException e) { System.out.println("Main thread was interrupted!"); } System.out.println("Main thread proceeds after thread completion or interruption."); } }
Output:
Thread: 1 Thread: 2 Thread: 3 Main thread proceeds after thread completion or interruption.
Explanation:
If the thread gets interrupted while waiting, the InterruptedException is thrown.
In this case, the main thread catches the exception and handles the interruption gracefully.
6. Using join() in a Thread Pool
In real-world applications, we often use thread pools to manage multiple threads. Even within a thread pool, you can use join() to wait for specific threads to complete their tasks.
Example 6: Using join() with a Thread Pool
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; class ThreadPoolTask extends Thread { private String taskName; public ThreadPoolTask(String taskName) { this.taskName = taskName; } @Override public void run() { System.out.println(taskName + " is running."); try { Thread.sleep(1000); } catch (InterruptedException e) { System.out.println(taskName + " was interrupted."); } System.out.println(taskName + " has finished."); } } public class JoinExample6 { public static void main(String[] args) { ExecutorService executor = Executors.newFixedThreadPool(3); ThreadPoolTask task1 = new ThreadPoolTask("Task 1"); ThreadPoolTask task2 = new ThreadPoolTask("Task 2"); ThreadPoolTask task3 = new ThreadPoolTask("Task 3"); executor.execute(task1); executor.execute(task2); executor.execute(task3); executor.shutdown(); // Gracefully shut down the executor try { task1.join(); task2.join(); task3.join(); } catch (InterruptedException e) { System.out.println(e); } System.out.println("Main thread proceeds after all tasks are completed."); } }
Output:
Task 1 is running. Task 2 is running. Task 3 is running. Task 1 has finished. Task 2 has finished. Task 3 has finished. Main thread proceeds after all tasks are completed.
Explanation:
In this example, the ExecutorService manages multiple threads.
After submitting the tasks to the thread pool, the join() method ensures that the main thread waits for the tasks to finish.
Conclusion
The join() method in Java is a powerful tool for controlling the execution order of threads.
It allows one thread to wait for the completion of another before proceeding, making it useful for scenarios where you need to synchronize threads or ensure that certain tasks complete before moving forward.
Key points:
Use join() to make one thread wait for another.
The join(long millis) and join(long millis, int nanos) methods allow you to specify how long to wait before continuing.
Handling InterruptedException is important when dealing with threads.
You can join multiple threads or use join() in thread pools to coordinate task completion.