53 How Count Down Latch Arranges Thread Execution Order

53 How CountDownLatch Arranges Thread Execution Order #

In this lesson, we will mainly introduce how CountDownLatch arranges the order of thread execution.

Let’s first introduce CountDownLatch. It is a concurrent flow control tool class provided by JDK. It is located in the java.util.concurrent package and was introduced in JDK 1.5. Let’s use an example to illustrate the main scenarios where it is used.

For example, when we go to an amusement park to ride a rapid river, sometimes there are not many people in the park. In this case, the administrator will ask you to wait for a while until the seats are full before starting the ride. This can save the park’s costs to some extent. The number of seats determines the number of people to wait for, which is the core idea of CountDownLatch. It needs to wait until a predetermined number is reached before it can start.

Flowchart #

We represent the example of riding the rapid river using a flowchart:

img

As can be seen from the flowchart, the initial value set by CountDownLatch is 3. Then thread T0 comes and calls the await() method. Its purpose is to make this thread start waiting for T1, T2, and T3 that come after it. Each time T1, T2, and T3 call the countDown() method, the value of 3 will be decreased by 1, i.e. from 3 to 2, from 2 to 1, and from 1 to 0. Once it is reduced to 0, T0 is considered to have reached its own trigger condition to continue running, so it resumes execution.

Main Method Introduction #

Next, let’s introduce the main methods of CountDownLatch.

(1) Constructor: public CountDownLatch(int count) {}

The constructor takes a parameter count, which is the value to count down from.

(2) await(): The thread calling the await() method starts to wait until the countdown is completed, i.e. when the count value is 0, it will continue to execute.

(3) await(long timeout, TimeUnit unit): await() has an overloaded method that takes a timeout parameter. This method has a similar function to await(), but it allows you to set a timeout. If the timeout is reached, it will no longer wait.

(4) countDown(): Decrease the value by 1, i.e. decrease the count value by 1. When it becomes 0, the threads that were waiting will be awakened.

Usage #

Next, let’s introduce two typical use cases of CountDownLatch.

Use Case 1: One thread waits for multiple other threads to complete their work before continuing its own work #

In actual scenarios, many situations require us to initialize a series of prerequisites (such as establishing a connection, preparing data). Before all these preparation conditions are completed, the next step of work cannot proceed. In such cases, CountDownLatch can be used effectively. We can let the main thread of the application continue to execute only after other threads have finished preparing.

Here’s an example from daily life, which is the scene of athletes running. For example, in a running race, there are 5 athletes, and there is a referee at the finish line. When does the race end? It is when all the athletes reach the finish line. That is, the referee waits for all 5 athletes to reach the finish line before announcing the end of the race. Let’s write this scene in code:

public class RunDemo1 {

    public static void main(String[] args) throws InterruptedException {

        CountDownLatch latch = new CountDownLatch(5);

        ExecutorService service = Executors.newFixedThreadPool(5);

        for (int i = 0; i < 5; i++) {

            final int no = i + 1;

            Runnable runnable = new Runnable() {

                @Override

                public void run() {

                    try {

                        Thread.sleep((long) (Math.random() * 10000));

                        System.out.println(no + "号运动员完成了比赛。");

                    } catch (InterruptedException e) {

                        e.printStackTrace();

                    } finally {

                        latch.countDown();

                    }

                }

            };

            service.submit(runnable);

        }

        System.out.println("等待5个运动员都跑完.....");

        latch.await();

        System.out.println("所有人都跑完了,比赛结束。");

    }

}

In this code, we create a CountDownLatch with an initial value of 5. Then we create a fixed thread pool with 5 threads and submit 5 tasks to this thread pool in a for loop. Each task represents an athlete. Each athlete will first wait for a random amount of time to simulate the running process, and then print out that they have finished the race. After running, they will call the countDown() method to decrease the count value by 1.

Then we return to the main thread. After the main thread prints “等待5个运动员都跑完…”, it will call the await() method to let the main thread start waiting. It will consider that everyone has finished the race only after the previous threads have completed. The running result of this program is as follows:

等待5个运动员都跑完.....

4号运动员完成了比赛。

3号运动员完成了比赛。

1号运动员完成了比赛。

5号运动员完成了比赛。
Athlete 2 has finished the race.

Everyone has finished running, and the race is over.

It can be seen that the main thread will continue only after all 5 athletes have finished the race. Since the waiting time for each sub-thread is random, the order in which each athlete finishes the race is also random.

Method 2: Multiple threads waiting for a signal from another thread to start execution #

This is a bit opposite to the first method. Let’s list another practical scenario. For example, in a sports event, instead of referees waiting for athletes, it’s the athletes waiting for the referee. Before the athletes start running, they all wait for the referee’s command. When the command is given, all athletes start running together. We can describe this using code as follows:

public class RunDemo2 {

    public static void main(String[] args) throws InterruptedException {

        System.out.println("The athletes have 5 seconds to prepare");

        CountDownLatch countDownLatch = new CountDownLatch(1);

        ExecutorService service = Executors.newFixedThreadPool(5);

        for (int i = 0; i < 5; i++) {

            final int no = i + 1;

            Runnable runnable = new Runnable() {

                @Override

                public void run() {

                    System.out.println("Athlete " + no + " is ready, waiting for the referee's gun");

                    try {

                        countDownLatch.await();

                        System.out.println("Athlete " + no + " starts running");

                    } catch (InterruptedException e) {

                        e.printStackTrace();

                    }

                }

            };

            service.submit(runnable);

        }

        Thread.sleep(5000);

        System.out.println("5 seconds preparation time has passed, the gun fires, and the race begins!");

        countDownLatch.countDown();

    }

}

In this code, first, the message “The athletes have 5 seconds to prepare” is printed. Then a CountDownLatch object with an initial count of 1 is created. Next, a ThreadPoolExecutor with 5 threads is created, and 5 tasks are submitted to the executor using a for loop. Each task calls the await() method to start waiting initially.

Next, we go back to the main thread. The main thread waits for 5 seconds, which means the referee is doing preparation work, such as saying “On your marks, get set” and so on. After 5 seconds, the main thread prints the signal “5 seconds preparation time has passed, the gun fires, and the race begins!” and then calls the countDown() method. Once the main thread calls this method, all the threads that have previously called the await() method will be awakened. So, the output of this program is as follows:

The athletes have 5 seconds to prepare

Athlete 2 is ready, waiting for the referee's gun

Athlete 1 is ready, waiting for the referee's gun

Athlete 3 is ready, waiting for the referee's gun

Athlete 4 is ready, waiting for the referee's gun

Athlete 5 is ready, waiting for the referee's gun

5 seconds preparation time has passed, the gun fires, and the race begins!

Athlete 2 starts running

Athlete 1 starts running

Athlete 5 starts running

Athlete 4 starts running

Athlete 3 starts running

As you can see, the athletes first have 5 seconds of preparation time. Then all 5 athletes are ready and waiting for the gun to fire. After 5 seconds, the gun fires and the race begins, so the 5 sub-threads start running almost simultaneously.

Notes #

Now let’s talk about some notes about CountDownLatch:

  • As mentioned earlier, there are two methods of usage, but in fact, these two methods are not isolated. They can even be combined. For example, you can use two CountDownLatch objects, the initial count of the first one set to multiple, and the initial count of the second one set to 1. This way you can handle more complex business scenarios.
  • CountDownLatch cannot be reused. For example, can you restart the countdown after it has already counted down? Unfortunately, this is not possible. If you have such a requirement, you can consider using CyclicBarrier or creating a new CountDownLatch instance.

Summary #

When creating an instance of the CountDownLatch class, the countdown value needs to be passed in the constructor, and the thread that needs to wait calls the await() method to start waiting. Every time another thread calls the countDown() method, the count will be reduced by 1. When the count reaches 0, the waiting thread will continue to run.