The Thread.join() Method in Java

1. Overview

In this tutorial, we’ll discuss the different join() methods in the Thread class. We’ll go into the details of these methods and some example code.

Like the wait() and notify() methodsjoin() is another mechanism of inter-thread synchronization.

You can have a quick look at this tutorial to read more about wait() and notify().

2. The Thread.join() Method

The join method is defined in the Thread class:

public final void join() throws InterruptedException

Waits for this thread to die.

When we invoke the join() method on a thread, the calling thread goes into a waiting state. It remains in a waiting state until the referenced thread terminates.

We can see this behavior in the following code:

class SampleThread extends Thread {
    public int processingCount = 0;

    SampleThread(int processingCount) {
        this.processingCount = processingCount;
        LOGGER.info("Thread Created");
    }

    @Override
    public void run() {
        LOGGER.info("Thread " + this.getName() + " started");
        while (processingCount > 0) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                LOGGER.info("Thread " + this.getName() + " interrupted");
            }
            processingCount--;
        }
        LOGGER.info("Thread " + this.getName() + " exiting");
    }
}

@Test
public void givenStartedThread_whenJoinCalled_waitsTillCompletion() 
  throws InterruptedException {
    Thread t2 = new SampleThread(1);
    t2.start();
    LOGGER.info("Invoking join");
    t2.join();
    LOGGER.info("Returned from join");
    assertFalse(t2.isAlive());
}

We should expect results similar to the following when executing the code:

INFO: Thread Created
INFO: Invoking join
INFO: Thread Thread-1 started
INFO: Thread Thread-1 exiting
INFO: Returned from join

The join() method may also return if the referenced thread was interrupted.  In this case, the method throws an InterruptedException.

Finally, if the referenced thread was already terminated or hasn’t been started, the call to join() method returns immediately.

Thread t1 = new SampleThread(0);
t1.join();  //returns immediately

3. Thread.join() Methods with Timeout

The join() method will keep waiting if the referenced thread is blocked or is taking too long to process. This can become an issue as the calling thread will become non-responsive. To handle these situations, we use overloaded versions of the join() method that allow us to specify a timeout period.

There are two timed versions which overload the join() method:

“public final void join(long millis) throws InterruptedException
Waits at most millis milliseconds for this thread to die. A timeout of 0 means to wait forever.”

“public final void join(long millis,intnanos) throws InterruptedException
Waits at most millis milliseconds plus nanos nanoseconds for this thread to die.”

We can use the timed join() as below:

@Test
public void givenStartedThread_whenTimedJoinCalled_waitsUntilTimedout()
  throws InterruptedException {
    Thread t3 = new SampleThread(10);
    t3.start();
    t3.join(1000);
    assertTrue(t3.isAlive());
}

In this case, the calling thread waits for roughly 1 second for the thread t3 to finish. If the thread t3 does not finish in this time period, the join() method returns control to the calling method.

Timed join() is dependent on the OS for timing. So, we cannot assume that join() will wait exactly as long as specified.

4. Thread.join() Methods and Synchronization

In addition to waiting until termination, calling the join() method has a synchronization effect. join() creates a happens-before relationship:

“All actions in a thread happen-before any other thread successfully returns from a join() on that thread.”

This means that when a thread t1 calls t2.join(), then all changes done by t2 are visible in t1 on return. However, if we do not invoke join() or use other synchronization mechanisms, we do not have any guarantee that changes in the other thread will be visible to the current thread even if the other thread has completed.

Hence, even though the join() method call to a thread in the terminated state returns immediately, we still need to call it in some situations.

We can see an example of improperly synchronized code below:

SampleThread t4 = new SampleThread(10);
t4.start();
// not guaranteed to stop even if t4 finishes.
do {
       
} while (t4.processingCount > 0);

To properly synchronize the above code, we can add timed t4.join() inside the loop or use some other synchronization mechanism.

5. Conclusion

join() method is quite useful for inter-thread synchronization. In this article, we discussed the join() methods and their behavior. We also reviewed code using join() method.

As always, the full source code can be found over on GitHub.