Course – LS – All

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE

1. Overview

Multi-thread programming allows us to run threads concurrently, and each thread can handle different tasks. Thus, it makes optimal use of the resources, particularly when our computer has a multiple multi-core CPU or multiple CPUs.

Sometimes, we’d like to control multiple threads to start at the same time.

In this tutorial, we’ll first understand the requirement, especially the meaning of “the exact same time”. Moreover, we’ll address how to start two threads simultaneously in Java.

2. Understanding the Requirement

Our requirement is: “starting two threads at the exact same time.”

This requirement looks easy to understand. However, if we think about it carefully, is it even possible to start two threads at the EXACT same time?

First of all, each thread will consume CPU time to work. Therefore, if our application is running on a computer with a single-core CPU, it’s impossible to start two threads at exact same time.

If our computer has a multi-core CPU or multiple CPUs, two threads can possibly start at the exact same time. However, we cannot control it on the Java side.

This is because when we work with threads in Java, the Java thread scheduling depends on the thread scheduling of the operating system. So, different operating systems may handle it differently.

Moreover, if we discuss “the exact same time” in a more strict way, according to Einstein’s special theory of relativity:

It is impossible to say in an absolute sense that two distinct events occur at the same time if those events are separated in space.

No matter how close our CPUs sit on the motherboard or the cores located in a CPU, there are spaces. Therefore, we cannot ensure two threads start at the EXACT same time.

So, does it mean the requirement is invalid?

No. It’s a valid requirement. Even if we cannot make two threads start at the EXACT same time, we can get pretty close through some synchronization techniques.

These techniques may help us in most practical cases when we need two threads to start at “the same time.”

In this tutorial, we’ll explore two approaches to solve this problem:

All approaches follow the same idea: We won’t really start two threads at the same time. Instead, we block the threads immediately after the threads start and try to resume their execution simultaneously.

Since our tests would be related to thread scheduling, it’s worth mentioning the environment to run the tests in this tutorial:

  • CPU: Intel(R) Core(TM) i7-8850H CPU. The processor clocks are at between 2.6 and 4.3 GHz (4.1 with 4 cores, 4 GHz with 6 cores)
  • Operating System: 64-bit Linux with Kernel version 5.12.12
  • Java: Java 11

Now, let’s see CountDonwLatch and CyclicBarrier in action.

3. Using the CountDownLatch Class

CountDownLatch is a synchronizer introduced in Java 5 as a part of the java.util.concurrent package. Usually, we use a CountDownLatch to block threads until other threads have completed their tasks.

Simply put, we set a count in a latch object and associate the latch object to some threads. When we start these threads, they will be blocked until the latch’s count becomes zero.

On the other side, in other threads, we can control under which condition we reduce the count and let the blocked threads resume, for example, when some tasks in the main thread are done.

3.1. The Worker Thread

Now, let’s have a look at how to solve our problem using the CountDownLatch class.

First, we’ll create our Thread class. Let’s call it WorkerWithCountDownLatch:

public class WorkerWithCountDownLatch extends Thread {
    private CountDownLatch latch;

    public WorkerWithCountDownLatch(String name, CountDownLatch latch) {
        this.latch = latch;
        setName(name);
    }

    @Override public void run() {
        try {
            System.out.printf("[ %s ] created, blocked by the latch...\n", getName());
            latch.await();
            System.out.printf("[ %s ] starts at: %s\n", getName(), Instant.now());
            // do actual work here...
        } catch (InterruptedException e) {
            // handle exception
        }
    }

We’ve added a latch object to our WorkerWithCountDownLatch class. First, let’s understand the function of the latch object.

In the run() method, we call the method latch.await(). This means, if we started the worker thread, it would check the latch’s count. The thread would be blocked until the count is zero.

In this way, we can create a CountDownLatch(1) latch with count=1 in the main thread and associate the latch object to two worker threads we want to start at the same time.

When we want the two threads to resume doing their actual jobs, we release the latch by invoking latch.countDown() in the main thread.

Next, let’s take a look at how the main thread controls the two worker threads.

3.2. The Main Thread

We’ll implement the main thread in the usingCountDownLatch() method:

private static void usingCountDownLatch() throws InterruptedException {
    System.out.println("===============================================");
    System.out.println("        >>> Using CountDownLatch <<<<");
    System.out.println("===============================================");

    CountDownLatch latch = new CountDownLatch(1);

    WorkerWithCountDownLatch worker1 = new WorkerWithCountDownLatch("Worker with latch 1", latch);
    WorkerWithCountDownLatch worker2 = new WorkerWithCountDownLatch("Worker with latch 2", latch);

    worker1.start();
    worker2.start();

    Thread.sleep(10);//simulation of some actual work

    System.out.println("-----------------------------------------------");
    System.out.println(" Now release the latch:");
    System.out.println("-----------------------------------------------");
    latch.countDown();
}

Now, let’s call the usingCountDownLatch() method above from our main() method. When we run the main() method, we’ll see the output:

===============================================
        >>> Using CountDownLatch <<<<
===============================================
[ Worker with latch 1 ] created, blocked by the latch
[ Worker with latch 2 ] created, blocked by the latch
-----------------------------------------------
 Now release the latch:
-----------------------------------------------
[ Worker with latch 2 ] starts at: 2021-06-27T16:00:52.268532035Z
[ Worker with latch 1 ] starts at: 2021-06-27T16:00:52.268533787Z

As the output above shows, the two worker threads started almost at the same time. The difference between the two start times is less than two microseconds.

4. Using the CyclicBarrier Class

The CyclicBarrier class is another synchronizer introduced in Java 5. Essentially, CyclicBarrier allows a fixed number of threads to wait for each other to reach a common point before continuing execution.

Next, let’s see how we solve our problem using the CyclicBarrier class.

4.1. The Worker Thread

Let’s first take a look at the implementation of our worker thread:

public class WorkerWithCyclicBarrier extends Thread {
    private CyclicBarrier barrier;

    public WorkerWithCyclicBarrier(String name, CyclicBarrier barrier) {
        this.barrier = barrier;
        this.setName(name);
    }

    @Override public void run() {
        try {
            System.out.printf("[ %s ] created, blocked by the barrier\n", getName());
            barrier.await();
            System.out.printf("[ %s ] starts at: %s\n", getName(), Instant.now());
            // do actual work here...
        } catch (InterruptedException | BrokenBarrierException e) {
            // handle exception
        }
    }
}

The implementation is pretty straightforward. We associate a barrier object with the worker threads. When the thread starts, we call the barrier.await() method immediately.

In this way, the worker thread will be blocked and waiting for all parties to invoke barrier.await() to resume.

4.2. The Main Thread

Next, let’s look at how to control two worker threads resuming in the main thread:

private static void usingCyclicBarrier() throws BrokenBarrierException, InterruptedException {
    System.out.println("\n===============================================");
    System.out.println("        >>> Using CyclicBarrier <<<<");
    System.out.println("===============================================");

    CyclicBarrier barrier = new CyclicBarrier(3);

    WorkerWithCyclicBarrier worker1 = new WorkerWithCyclicBarrier("Worker with barrier 1", barrier);
    WorkerWithCyclicBarrier worker2 = new WorkerWithCyclicBarrier("Worker with barrier 2", barrier);

    worker1.start();
    worker2.start();

    Thread.sleep(10);//simulation of some actual work

    System.out.println("-----------------------------------------------");
    System.out.println(" Now open the barrier:");
    System.out.println("-----------------------------------------------");
    barrier.await();
}

Our goal is to let two worker threads resume at the same time. So, together with the main thread, we have three threads in total.

As the method above shows, we create a barrier object with three parties in the main thread. Next, we create and start two worker threads.

As we discussed earlier, the two worker threads are blocked and waiting for the barrier’s open to resume.

In the main thread, we can do some actual work. When we decide to open the barrier, we call the method barrier.await() to let two workers continue execution.

If we call usingCyclicBarrier() in the main() method, we’ll get the output:

===============================================
        >>> Using CyclicBarrier <<<<
===============================================
[ Worker with barrier 1 ] created, blocked by the barrier
[ Worker with barrier 2 ] created, blocked by the barrier
-----------------------------------------------
 Now open the barrier:
-----------------------------------------------
[ Worker with barrier 1 ] starts at: 2021-06-27T16:00:52.311346392Z
[ Worker with barrier 2 ] starts at: 2021-06-27T16:00:52.311348874Z

We can compare the two start times of the workers. Even if the two workers didn’t start at the exact same time, we’re pretty close to our goal: the difference between the two start times is less than three microseconds.

5. Using the Phaser Class

The Phaser class is a synchronizer introduced in Java 7. It’s similar to CyclicBarrier and CountDownLatch. However, the Phaser class is more flexible.

For example, unlike CyclicBarrier and CountDownLatch, Phaser allows us to register the thread parties dynamically.

Next, let’s solve the problem using Phaser.

5.1. The Worker Thread

As usual, we have a look at the implementation first and then understand how it works:

public class WorkerWithPhaser extends Thread {
    private Phaser phaser;

    public WorkerWithPhaser(String name, Phaser phaser) {
        this.phaser = phaser;
        phaser.register();
        setName(name);
    }

    @Override public void run() {
        try {
            System.out.printf("[ %s ] created, blocked by the phaser\n", getName());
            phaser.arriveAndAwaitAdvance();
            System.out.printf("[ %s ] starts at: %s\n", getName(), Instant.now());
            // do actual work here...
        } catch (IllegalStateException e) {
            // handle exception
        }
    }
}

When a worker thread is instantiated, we register the current thread to the given Phaser object by calling phaser.register(). In this way, the current work becomes one thread party of the phaser barrier.

Next, when the worker thread starts, we call phaser.arriveAndAwaitAdvance() immediately. Thus, we tell phaser that the current thread has arrived and will wait for other thread parties’ arrival to carry on. Of course, before other thread parties’ arrival, the current thread is blocked.

5.2. The Main Thread

Next, let’s move on and look at the implementation of the main thread:

private static void usingPhaser() throws InterruptedException {
    System.out.println("\n===============================================");
    System.out.println("        >>> Using Phaser <<<");
    System.out.println("===============================================");

    Phaser phaser = new Phaser();
    phaser.register();

    WorkerWithPhaser worker1 = new WorkerWithPhaser("Worker with phaser 1", phaser);
    WorkerWithPhaser worker2 = new WorkerWithPhaser("Worker with phaser 2", phaser);

    worker1.start();
    worker2.start();

    Thread.sleep(10);//simulation of some actual work

    System.out.println("-----------------------------------------------");
    System.out.println(" Now open the phaser barrier:");
    System.out.println("-----------------------------------------------");
    phaser.arriveAndAwaitAdvance();
}

In the code above, as we can see, the main thread registers itself as a thread party of the Phaser object.

After we’ve created and blocked the two worker threads, the main thread calls phaser.arriveAndAwaitAdvance() as well. In this way, we open the phaser barrier, so that the two worker threads can resume at the same time.

Finally, let’s call the usingPhaser() method in the main() method:

===============================================
        >>> Using Phaser <<<
===============================================
[ Worker with phaser 1 ] created, blocked by the phaser
[ Worker with phaser 2 ] created, blocked by the phaser
-----------------------------------------------
 Now open the phaser barrier:
-----------------------------------------------
[ Worker with phaser 2 ] starts at: 2021-07-18T17:39:27.063523636Z
[ Worker with phaser 1 ] starts at: 2021-07-18T17:39:27.063523827Z

Similarly, the two worker threads started almost at the same time. The difference between the two start times is less than two microseconds.

6. Conclusion

In this article, we’ve first discussed the requirement: “start two threads at the exact same time.”

Next, we’ve addressed two approaches to start three threads simultaneously: using CountDownLatch, CyclicBarrier, and Phaser.

Their ideas are similar, blocking two threads and trying to let them resume execution simultaneously.

Even though these approaches cannot guarantee two threads starting at the exact same time, the result is pretty close and sufficient for most cases in the real world.

As always, the code for the article can be found over on GitHub.

Course – LS – All

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE
res – REST with Spring (eBook) (everywhere)
2 Comments
Oldest
Newest
Inline Feedbacks
View all comments
Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.