哈喽,各位小伙伴们,你们好呀,我是喵手。运营社区:掘金/C站/腾讯云/阿里云/华为云/51CTO(全网同号);欢迎大家常来逛逛,互相学习。
今天我要给大家分享一些自己日常学习到的一些知识点,并以文字的形式跟大家一起交流,互相学习,一个人虽可以走的更快,但一群人可以走的更远。
我是一名后端开发爱好者,工作日常接触到最多的就是Java语言啦,所以我都尽量抽业余时间把自己所学到所会的,通过文章的形式进行输出,希望以这种方式帮助到更多的初学者或者想入门的小伙伴们,同时也能对自己的技术进行沉淀,加以复盘,查缺补漏。
小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦。三连即是对作者我写作道路上最好的鼓励与支持!
前言
在如今项目开发中,并发已经成为必不可少的一部分,特别是在高性能的应用场景中。随着多核处理器的普及,我们有机会让多个线程并行执行,从而显著提升程序的性能和响应速度。但与此同时,这种并行执行也带来了不少挑战,其中最为棘手的之一,就是如何确保多个线程按顺序执行?这个问题想必大家都耳熟能详,那到底如何实现呢?又有哪些方式可以实现?接下来,我们来深入聊聊它。
作为开发者,我们常常会遇到这样的场景:多个线程需要依赖前一个线程的执行结果,或者必须按一定的顺序来完成某项任务。比如说,假设我们有三个线程,线程A负责加载数据,线程B需要在线程A加载完数据之后进行处理,线程C则依赖线程B的处理结果。如果这些线程顺序混乱,结果可能会出错,甚至程序崩溃,最终服务宕机,那如何避免或者实现呢。
记得自己刚开始接触多线程编程时,也曾为线程的执行顺序问题困扰过。并发任务执行时,线程独立、自由地执行,看起来非常高效,但实际操作中,线程之间的协作往往是需要精心设计的。怎么保证线程按照预定的顺序执行呢?这成了一个让我头疼的问题。幸运的是,Java提供了很多工具来帮助我们实现线程间的顺序控制。这些工具,真实帮我太多,接下来我就要给大家隆重介绍一波了。
1. 使用 join()
方法:最基础的同步工具
join()
方法可能是Java中最简单也最常用的控制线程顺序执行的工具。通过调用 join()
,我们可以让一个线程等待另一个线程执行完毕后再继续执行。这种方法非常直观,易于实现。在某些情况下,join()
足够满足我们对线程顺序的需求,尤其是线程执行顺序非常简单且没有复杂的依赖关系时。
为什么选择 join()
?
join()
方法实现起来非常简单,可以让我们在主线程中直接控制子线程的执行顺序。在代码中,只需要在每个线程启动后,调用该线程的 join()
方法,确保前一个线程完成后才启动下一个线程。
示例代码
针对如上示例代码,这里我给大家详细的代码剖析下,以便于帮助大家理解的更为透彻,帮助大家早日掌握。
java
package com.demo.java.test;
public class SequentialThreads {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> System.out.println("Thread 1"));
Thread t2 = new Thread(() -> System.out.println("Thread 2"));
Thread t3 = new Thread(() -> System.out.println("Thread 3"));
t1.start();
t1.join(); // 等待 t1 执行完后再启动 t2
t2.start();
t2.join(); // 等待 t2 执行完后再启动 t3
t3.start();
t3.join(); // 等待 t3 执行完,程序结束
}
}
在这个简单的例子中,t1
会先启动并执行,主线程会调用 t1.join()
,让主线程等待 t1
完成后再启动 t2
。同理,t2
会在执行完成后,主线程再等待它完成,然后启动 t3
。通过这种方式,我们能确保线程按顺序执行,代码结构清晰。
案例执行结果
如上案例结果运行展示如下,仅供参考:

用例代码分析
针对如上示例代码,这里我给大家详细的代码剖析下,以便于帮助大家理解的更为透彻,帮助大家早日掌握。
如上这个案例目的是通过创建三个线程,并确保它们按照顺序执行:首先执行 t1
,然后等待 t1
执行完毕后再执行 t2
,接着等待 t2
执行完毕后再执行 t3
,直到所有线程执行完毕,程序才结束。
我们逐行分析这段代码的执行流程:
-
创建线程对象:
javaThread t1 = new Thread(() -> System.out.println("Thread 1")); Thread t2 = new Thread(() -> System.out.println("Thread 2")); Thread t3 = new Thread(() -> System.out.println("Thread 3"));
这里创建了三个线程对象 t1
、t2
和 t3
。每个线程对象都传递了一个 Runnable
接口的实现,即一个匿名 Runnable
类,在每个线程中执行 System.out.println
输出各自的线程编号。
-
启动线程
t1
:javat1.start();
t1.start()
启动线程 t1
,这个线程会执行 System.out.println("Thread 1")
,打印 "Thread 1"。
-
等待线程
t1
执行完毕:javat1.join();
这里 t1.join()
让主线程等待线程 t1
执行完毕后再继续执行后面的代码。join()
是一种阻塞主线程的方法,直到 t1
完成执行,主线程才能继续。
-
启动线程
t2
:javat2.start();
一旦 t1
执行完,主线程会启动线程 t2
,它会执行 System.out.println("Thread 2")
,打印 "Thread 2"。
-
等待线程
t2
执行完毕:javat2.join();
这行代码类似于之前的 t1.join()
,它会让主线程等待线程 t2
执行完毕再继续执行后面的代码。
-
启动线程
t3
:javat3.start();
在 t2
执行完毕后,主线程启动线程 t3
,它会执行 System.out.println("Thread 3")
,打印 "Thread 3"。
-
等待线程
t3
执行完毕:javat3.join();
最后,t3.join()
会让主线程等待 t3
执行完毕。所有的线程执行完成后,主线程才会结束,程序运行完毕。
代码执行流程梳理
- 线程
t1
启动并输出 "Thread 1"。 - 主线程等待
t1
执行完毕。 - 线程
t2
启动并输出 "Thread 2"。 - 主线程等待
t2
执行完毕。 - 线程
t3
启动并输出 "Thread 3"。 - 主线程等待
t3
执行完毕,程序结束。
关键点:
start()
方法用于启动线程,真正的线程执行会在调用start()
后异步进行。join()
方法会让当前线程(通常是主线程)等待指定线程执行完毕再继续。这种机制保证了线程的执行顺序。- 程序中的三个线程按顺序执行,分别打印 "Thread 1"、"Thread 2" 和 "Thread 3"。
通过使用 join()
方法,确保了线程按照特定顺序执行,主线程不会提前结束,直到所有的子线程执行完毕。
优点:
- 简单直接 :无需额外的同步工具,直接通过
join()
保证线程顺序。 - 易于理解:线程按顺序启动和执行,逻辑非常直观。
缺点:
- 性能瓶颈 :如果线程执行时间较长,
join()
会阻塞主线程,这可能影响程序的整体响应性。 - 局限性 :适用于简单的线程顺序控制,对于更复杂的多线程依赖关系,
join()
的局限性就显现出来。
2. 使用 CountDownLatch
:适合多个线程的顺序执行
CountDownLatch
是一种更为灵活的线程同步工具,它通过一个计数器来控制线程的顺序。通过调用 await()
方法,线程会等待计数器的值为0,才能继续执行。而通过 countDown()
方法,线程可以减少计数器的值。
为什么选择 CountDownLatch
?
CountDownLatch
适用于需要多个线程协调执行并按顺序执行的场景。通过使用 CountDownLatch
,可以有效地控制多个线程之间的依赖关系,确保一个线程完成任务后再启动下一个线程。
示例代码:
java
package com.demo.java.test;
import java.util.concurrent.CountDownLatch;
public class SequentialThreadsWithLatch {
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch1 = new CountDownLatch(1); // 控制线程1和2
CountDownLatch latch2 = new CountDownLatch(1); // 控制线程2和3
Thread t1 = new Thread(() -> {
System.out.println("Thread 1");
latch1.countDown(); // 线程1完成,释放 latch1
});
Thread t2 = new Thread(() -> {
try {
latch1.await(); // 等待线程1完成
System.out.println("Thread 2");
latch2.countDown(); // 线程2完成,释放 latch2
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
Thread t3 = new Thread(() -> {
try {
latch2.await(); // 等待线程2完成
System.out.println("Thread 3");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
t1.start();
t2.start();
t3.start();
t1.join();
t2.join();
t3.join();
}
}
在这个例子中,t1
会首先执行,t2
必须等待 t1
完成后才能执行,而 t3
又要等到 t2
执行完成后才开始执行。通过 CountDownLatch
的 await()
和 countDown()
方法,确保线程按照预定顺序执行。
结果演示
如上案例结果运行展示如下,仅供参考:

代码分析
针对如上示例代码,这里我给大家详细的代码剖析下,以便于帮助大家理解的更为透彻,帮助大家早日掌握。
如上这段案例代码使用了 CountDownLatch
来控制多个线程的顺序执行。CountDownLatch
是一个同步工具,允许一个或多个线程等待其他线程完成某些操作,然后再继续执行。
-
定义
CountDownLatch
对象:javaCountDownLatch latch1 = new CountDownLatch(1); // 控制线程1和2 CountDownLatch latch2 = new CountDownLatch(1); // 控制线程2和3
这里定义了两个
CountDownLatch
对象:latch1
用于控制t1
和t2
的执行顺序。latch2
用于控制t2
和t3
的执行顺序。 每个CountDownLatch
的初始值为 1,意味着当countDown()
被调用一次时,latch
的计数就会减少 1。
-
创建线程
t1
:javaThread t1 = new Thread(() -> { System.out.println("Thread 1"); latch1.countDown(); // 线程1完成,释放 latch1 });
t1
线程在启动时首先打印 "Thread 1"。执行完后,调用 latch1.countDown()
,这会将 latch1
的计数减少 1,从而释放其他等待 latch1
的线程,尤其是 t2
线程。
-
创建线程
t2
:javaThread t2 = new Thread(() -> { try { latch1.await(); // 等待线程1完成 System.out.println("Thread 2"); latch2.countDown(); // 线程2完成,释放 latch2 } catch (InterruptedException e) { Thread.currentThread().interrupt(); } });
t2
线程在启动时首先调用 latch1.await()
,它会阻塞,直到 latch1
的计数变为 0(即 t1
执行完并调用 countDown()
)。一旦 t1
执行完毕并释放 latch1
,t2
继续执行,打印 "Thread 2",然后调用 latch2.countDown()
,减少 latch2
的计数,释放其他等待 latch2
的线程,尤其是 t3
线程。
-
创建线程
t3
:javaThread t3 = new Thread(() -> { try { latch2.await(); // 等待线程2完成 System.out.println("Thread 3"); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } });
t3
线程在启动时首先调用 latch2.await()
,它会阻塞,直到 latch2
的计数变为 0(即 t2
执行完并调用 countDown()
)。一旦 t2
执行完毕并释放 latch2
,t3
继续执行,打印 "Thread 3"。
-
启动线程:
javat1.start(); t2.start(); t3.start();
这三行代码启动了 t1
、t2
和 t3
线程。由于 t2
和 t3
都会依赖 CountDownLatch
来控制它们的启动顺序,t1
必须先执行完毕才能启动 t2
,而 t2
必须执行完毕才能启动 t3
。
-
等待所有线程执行完:
javat1.join(); t2.join(); t3.join();
主线程通过 join()
方法等待所有子线程完成,确保 t1
、t2
和 t3
都执行完毕后主线程才结束。join()
会阻塞当前线程,直到目标线程执行完毕。
执行流程梳理:
t1
被启动,执行并打印 "Thread 1",然后调用latch1.countDown()
。t2
被启动,但它在执行前会等待latch1
释放(即t1
执行完)。t1
执行完后,t2
继续执行并打印 "Thread 2",然后调用latch2.countDown()
。t3
被启动,但它在执行前会等待latch2
释放(即t2
执行完)。t2
执行完后,t3
继续执行并打印 "Thread 3"。- 所有线程执行完毕后,主线程结束。
关键点:
CountDownLatch
用于控制线程的执行顺序。await()
会使当前线程阻塞,直到latch
的计数值为 0。countDown()
会减少latch
的计数值。- 使用
join()
确保主线程等待所有子线程执行完毕后再退出。
优点:
- 灵活性高:适用于多个线程之间有依赖关系的情况,可以精确控制线程的启动顺序。
- 适应性强:可以支持更多的线程协调,允许更加复杂的控制。
缺点:
- 稍微复杂 :比起简单的
join()
,CountDownLatch
的使用和理解需要更多的学习和实践。 - 不可重用 :
CountDownLatch
一旦计数器变为0,就不能再使用。如果需要多次复用,需要使用其他工具,如CyclicBarrier
。
3. 使用 CyclicBarrier
:适用于多线程同步
CyclicBarrier
是另一种同步工具,它允许一组线程在某个特定的同步点等待,直到所有线程都到达该点。与 CountDownLatch
不同的是,CyclicBarrier
可以在多次任务中复用,这使得它更适合处理循环或需要复用的同步场景。
为什么选择 CyclicBarrier
?
当多个线程需要在某个特定点进行同步时,CyclicBarrier
提供了一种有效的解决方案。通过使用 CyclicBarrier
,可以确保每个线程都在指定时刻到达同步点,然后一同继续执行。
示例代码:
java
package com.demo.java.test;
import java.util.concurrent.CyclicBarrier;
public class SequentialThreadsWithCyclicBarrier {
public static void main(String[] args) throws InterruptedException {
CyclicBarrier barrier1 = new CyclicBarrier(2); // 线程1和线程2同步
CyclicBarrier barrier2 = new CyclicBarrier(2); // 线程2和线程3同步
Thread t1 = new Thread(() -> {
try {
System.out.println("Thread 1");
barrier1.await(); // 等待 t2
} catch (Exception e) {
Thread.currentThread().interrupt();
}
});
Thread t2 = new Thread(() -> {
try {
barrier1.await(); // 等待 t1
System.out.println("Thread 2");
barrier2.await(); // 等待 t3
} catch (Exception e) {
Thread.currentThread().interrupt();
}
});
Thread t3 = new Thread(() -> {
try {
barrier2.await(); // 等待 t2
System.out.println("Thread 3");
} catch (Exception e) {
Thread.currentThread().interrupt();
}
});
t1.start();
t2.start();
t3.start();
t1.join();
t2.join();
t3.join();
}
}
在这里,CyclicBarrier
保证了线程 t2
在 t1
完成后开始执行,且线程 t3
会等到 t2
完成后再开始执行。与 CountDownLatch
相比,CyclicBarrier
更适用于需要复用屏障的场景。
优点:
- 高复用性:适用于多次同步的场景,特别是需要循环或反复控制线程同步的场合。
- 线程同步:确保所有线程在同一个时间点到达同步点,然后一起执行。
缺点:
- 稍复杂 :比
CountDownLatch
的应用稍复杂,尤其是在有多次同步需求时需要更小心地处理线程的状态。
结果演示
如上案例结果运行展示如下,仅供参考:

4. 使用 ExecutorService
和任务依赖:线程池管理
如果你希望在大规模并发环境中有效地管理线程并处理任务依赖关系,ExecutorService
是一个非常好的选择。通过线程池,你可以提交多个任务,并通过 Future
对象确保任务按顺序执行。这种方式更加灵活,也适用于大规模并发和任务调度。
为什么选择 ExecutorService
?
ExecutorService
提供了一个线程池,可以高效地调度线程任务。通过 Future.get()
方法,可以确保任务按顺序执行。它不仅适用于线程的顺序控制,还可以有效管理线程资源。
示例代码:
java
package com.demo.java.test;
import java.util.concurrent.*;
public class SequentialExecutorService {
public static void main(String[] args) throws InterruptedException, ExecutionException {
ExecutorService executor = Executors.newFixedThreadPool(3);
Future<?> future1 = executor.submit(() -> System.out.println("Thread 1"));
future1.get(); // 确保线程1完成后执行线程2
Future<?> future2 = executor.submit(() -> System.out.println("Thread 2"));
future2.get(); // 确保线程2完成后执行线程3
Future<?> future3 = executor.submit(() -> System.out.println("Thread 3"));
future3.get(); // 等待线程3完成
executor.shutdown();
}
}
通过 ExecutorService
和 Future.get()
方法,我们可以精确控制任务的执行顺序,确保线程按预期的顺序执行。
优点:
- 高效管理线程:通过线程池管理线程,可以提高并发任务的处理效率。
- 灵活性强 :
Future.get()
可以确保任务按顺序执行,适用于大规模并发环境。
缺点:
- 实现较复杂 :相对于
join()
和CountDownLatch
,ExecutorService
的代码稍微复杂,需要管理线程池和任务。
结果演示
如上案例结果运行展示如下,仅供参考:

总结
回想起来,确保多线程按照顺序执行的需求其实很常见,无论是在开发一个小型的后台服务,还是在一个大型的分布式系统中,这个问题都会不时出现。在我的开发过程中,join() 方法是我最常用的工具之一。它非常简单,只需要调用 join(),线程就会等待另一个线程执行完再继续执行。这样的机制适用于任务顺序比较简单的场景,比如有几个任务按顺序依次完成,它能够帮我们有效地保证线程间的执行顺序。
但随着需求越来越复杂,CountDownLatch 和 CyclicBarrier 这两个工具开始变得更加重要。当多个线程之间有复杂依赖关系时,CountDownLatch 和 CyclicBarrier 就像是多线程世界中的"协调员"。CountDownLatch 让我们能够精确控制线程在完成某些任务后再继续执行,而 CyclicBarrier 则更适用于多个线程需要在某个时刻汇聚在一起的场景,确保它们不会提前或延迟执行。这些工具让我真正体验到并发编程的强大和灵活。
另外,ExecutorService 是我在处理大规模并发任务时的得力助手。它不仅能帮我管理线程池,还能通过 Future.get() 等方法确保任务按照顺序执行。想想以前我手动管理线程的日子,代码凌乱、复杂,还容易出错。而现在,借助线程池和任务调度,我能更好地组织多线程工作,既高效又清晰。
每种工具都有其适用的场景,join() 简单直接,适合一些顺序执行的场景;CountDownLatch 和 CyclicBarrier 更适合复杂的线程依赖;而 ExecutorService 则适用于大规模的任务管理。通过这些工具,我们不仅能解决线程顺序问题,还能提升系统的稳定性和性能。随着开发经验的积累,我越来越能体会到并发编程中的精妙之处。多线程世界并不只是简单的"并行"与"异步"而已,它关乎线程之间的协调、资源管理、错误处理等等方方面面,掌握了这些工具,就像是掌握了进入并发世界的钥匙。
总的来说,线程的顺序执行在多线程编程中是一个非常重要的需求,正确地选择和使用合适的同步工具,能够让我们的程序更加高效、稳定。在这个充满挑战和机遇的多线程世界里,掌握了这些技巧和工具,我们就能在开发过程中游刃有余,避免常见的并发问题,写出更加健壮的代码。随着技术不断进步,我相信未来会有更多更灵活的工具帮助我们应对多线程带来的挑战,让我们在高并发环境下的开发起来会更加灵活更加轻松,这也是我们所共同期待的。
... ...
文末
好啦,以上就是我这期的全部内容,如果有任何疑问,欢迎下方留言哦,咱们下期见。
... ...
学习不分先后,知识不分多少;事无巨细,当以虚心求教;三人行,必有我师焉!!!
wished for you successed !!!
⭐️若喜欢我,就请关注我叭。
⭐️若对您有用,就请点赞叭。
⭐️若有疑问,就请评论留言告诉我叭。
版权声明:本文由作者原创,转载请注明出处,谢谢支持!