1. Overview
1.概述
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.
多线程编程允许我们并发地运行线程,每个线程可以处理不同的任务。因此,它能使资源得到最佳利用,特别是当我们的计算机有多个多核CPU或多个CPU时。
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.
在本教程中,我们将首先了解这一要求,特别是 “完全相同的时间 “的含义。此外,我们将讨论如何在Java中同时启动两个线程。
2. Understanding the Requirement
2.了解需求
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?
这个要求看起来很容易理解。然而,如果我们仔细想一想,是否有可能在EXACT同时启动两个线程?
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.
首先,每个线程都会消耗CPU时间来工作。因此,如果我们的应用程序运行在一台具有单核CPU的计算机上,就不可能在exact同一时间启动两个线程。
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.
如果我们的计算机有一个多核CPU或多个CPU,两个线程可能会在exact同时启动。然而,我们不能在Java方面控制它。
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.
这是因为当我们在Java中使用线程时,Java的线程调度取决于操作系统的线程调度。所以,不同的操作系统可能会有不同的处理方式。
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.
无论我们的CPU在主板上的位置有多近,或者位于CPU中的核心有多大,都有空间。因此,我们无法确保两个线程在EXACT同一时间启动。
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.
不,这是个有效的要求。即使我们不能让两个线程在EXACT同时启动,我们也可以通过一些同步技术来达到相当接近。
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:
在本教程中,我们将探讨两种方法来解决这个问题。
- Using the CountDownLatch class
- Using the CyclicBarrier class
- Using the Phaser class
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.
现在,让我们看看CountDonwLatch和CyclicBarrier的操作。
3. Using the CountDownLatch Class
3.使用CountDownLatch类
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.
CountDownLatch是Java 5中引入的同步器,是 java.util.concurrent包的一部分。通常,我们使用CountDownLatch来阻塞线程,直到其他线程完成其任务。
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.
简单地说,我们在一个latch对象中设置一个count,并将latch对象与一些线程相关联。当我们启动这些线程时,它们将被阻塞,直到锁存器的计数变为零。
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.
另一方面,在其他线程中,我们可以控制在什么条件下减少count,让被阻塞的线程恢复,例如,当主线程中的一些任务完成后。
3.1. The Worker Thread
3.1.工作线程
Now, let’s have a look at how to solve our problem using the CountDownLatch class.
现在,让我们看看如何使用CountDownLatch类来解决我们的问题。
First, we’ll create our Thread class. Let’s call it WorkerWithCountDownLatch:
首先,我们要创建我们的Thread类。让我们把它叫做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.
我们在我们的WorkerWithCountDownLatch类中添加了一个latch对象。首先,让我们了解一下latch对象的功能。
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.
在run()方法中,我们调用latch.await()方法。这意味着,如果我们启动worker线程,它将检查latch的计数。该线程将被阻塞,直到count为零。
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.
这样,我们可以在主线程中创建一个CountDownLatch(1)锁存器,count=1,并将latch对象关联到我们想要同时启动的两个工作线程。
When we want the two threads to resume doing their actual jobs, we release the latch by invoking latch.countDown() in the main thread.
当我们想让两个线程继续做他们的实际工作时,我们通过在主线程中调用latch.countDown()来释放闩锁。
Next, let’s take a look at how the main thread controls the two worker threads.
接下来,我们来看看主线程是如何控制两个工作线程的。
3.2. The Main Thread
3.2.主线程
We’ll implement the main thread in the usingCountDownLatch() method:
我们将在usingCountDownLatch()方法中实现主线程。
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:
现在,让我们从main()方法中调用上述usingCountDownLatch()方法。当我们运行main()方法时,我们会看到输出。
===============================================
>>> 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.
正如上面的输出显示,两个工作线程几乎同时启动。两个启动时间之间的差异小于2微秒。
4. Using the CyclicBarrier Class
4.使用CyclicBarrier类
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.
CyclicBarrier类是Java 5中引入的另一个同步器。从本质上讲,CyclicBarrier允许固定数量的线程在继续执行之前互相等待达到一个共同点。
Next, let’s see how we solve our problem using the CyclicBarrier class.
接下来,让我们看看如何使用CyclicBarrier类解决我们的问题。
4.1. The Worker Thread
4.1.工作线程
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.
实现是非常直接的。我们将一个barrier对象与工作线程相关联。当线程启动时,我们立即调用barrier.await() 方法。
In this way, the worker thread will be blocked and waiting for all parties to invoke barrier.await() to resume.
这样一来,工作线程将被阻塞,等待各方调用barrier.await()来恢复。
4.2. The Main Thread
4.2.主线程
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.
如上面的方法所示,我们在主线程中创建了一个有三个当事人的barrier对象。接下来,我们创建并启动两个工作线程。
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.
在主线程中,我们可以做一些实际的工作。当我们决定打开屏障时,我们调用方法barrier.await() ,让两个工作者继续执行。
If we call usingCyclicBarrier() in the main() method, we’ll get the output:
如果我们在main()方法中调用usingCyclicBarrier(),我们会得到输出。
===============================================
>>> 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.
我们可以比较工人的两个启动时间。即使这两个工人不是在完全相同的时间启动,我们也非常接近我们的目标:两个启动时间之间的差异小于3微秒。
5. Using the Phaser Class
5.使用Phaser类
The Phaser class is a synchronizer introduced in Java 7. It’s similar to CyclicBarrier and CountDownLatch. However, the Phaser class is more flexible.
Phaser类是Java 7中引入的一个同步器。它类似于CyclicBarrier和CountDownLatch。然而,Phaser类更加灵活。
For example, unlike CyclicBarrier and CountDownLatch, Phaser allows us to register the thread parties dynamically.
例如,与CyclicBarrier和CountDownLatch不同,Phaser允许我们动态地注册线程方。
Next, let’s solve the problem using Phaser.
接下来,让我们用Phaser解决这个问题。
5.1. The Worker Thread
5.1.工作线程
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.
当一个工作线程被实例化时,我们通过调用phaser.register()将当前线程注册到给定的Phaser对象。通过这种方式,当前工作成为phaser屏障的一个线程方。
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.
接下来,当工作线程启动时,我们立即调用phaser.arriveAndAwaitAdvance()。这样,我们告诉phaser,当前线程已经到达,并将等待其他线程方的到来来进行。当然,在其他线程方到来之前,当前线程是被阻塞的。
5.2. The Main Thread
5.2.主线程
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.
在上面的代码中,我们可以看到,主线程将自己注册为Phaser对象的一个线程方。
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.
在我们创建并阻断了两个worker线程后,主线程也会调用phaser.arriveAndAwaitAdvance()。通过这种方式,我们打开了phaser屏障,这样两个工作线程就可以同时恢复。
Finally, let’s call the usingPhaser() method in the main() method:
最后,让我们在main()方法中调用usingPhaser()方法。
===============================================
>>> 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
6.结语
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.
接下来,我们解决了同时启动三个线程的两种方法:使用CountDownLatch、CyclicBarrier和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.
一如既往,该文章的代码可以在GitHub上找到over。