本章将会介绍 Java 多线程并发编程的入门知识,从 Java 多线程常用实现开始,由浅入深了解 Java 两种常用的线程池创建使用及其适用场景。通过对 java.util.concurrent.ThreadPoolExecutor 源码的解析,了解自定义 Java 线程池的几个重要参数,并掌握线程池内在的执行逻辑,达到自定义 Java 线程池的目的。
并发与并行
在进行 Java 多线程编程之前,首先分享一个这个概念:并发和并行。
这两个概念在实际工作很少去刻意区分,属于非常基础的知识。如果你想了解 Java 多线程编程,就必需先搞清楚这两个概念以及差异。
并发 (Concurrency) 和并行 (Parallelism) 都是指同时处理多个事务的能力,但是这两个概念本质上还是有差异的。总结来说并行指的是时间上的同时发生,而并发并不一定是。如果站在观察者角度来看,并发看起来很像并行。
要想更好地理解,小故事是无法避免的,下面请让听一听我这个 “超市结账” 版本。
这里有一家叫 “小八” 的超市,里面只有一个收银台,但是确有两个收银通道。平时空闲的时候只开放一条收银通道,人多的时候开发两条收银通道,让所有顾客更快完成结账付款,减少等待时间。
但实际情况是这样的,只有一位收银员,但是收银台对于顾客是黑盒,顾客完全无法了解收银台里面如何运行,更无法知道真相:只有一位收银员。
对于单个顾客,他们结账流程是:1. 把商品挨个扫描计价;2. 计算顾客应付金额;3. 顾客付款;4. 顾客收拾商品结束购物。
当空闲的时候,每个顾客结账过程大约需要 1 分钟。当繁忙的时候,开放两条通道,平均每个顾客结账也需要 1 分钟。乍一看,收银台的性能提升了 1 倍。
实际情况是这样的:这位收银员,一边等待第一条通道顾客出示付款码,一边给第二条通道的顾客扫描计价;一边等待第一条通道的顾客自己打包商品,一边再给第二条通道的顾客找零钱。如图 1-1 所示:
这个小故事里面,超市相当于我们的计算机,收银台或者收银员相当于 CPU。当我们只有一颗 CPU 时,依然可以同时处理两条结账通道的顾客。这里的通道相当于线程。原来 1 分钟只能完成一个结账周期,通过增加结账通道提升了 1 倍的性能。
如果你站在顾客的视角,两条通道顾客在同时结账,这个就叫做并发。如果超市老板又招聘了一位收银员,两位收银员分别处理两条结账通道,两条通道相互不影响,这个就叫做并行。如图 1-2 所示:
对于 CPU 来说,程序就相当于是超市结账的顾客,所以在使用 Java 进行性能测试中,我们关心更多的就是并发。
并发和并行是计算机科学中两个密切相关但概念上不同的术语,主要用于描述任务的执行方式。
特征总结:
特性 | 并发(Concurrency) | 并行(Parallelism) |
---|---|---|
定义 | 多个任务交替执行,体现为任务之间的协作与调度,侧重任务切换。 | 多个任务同时执行,强调同时性,利用多核或多处理器资源。 |
执行单位 | 任务可以在单核或多核上通过时间片轮转执行(分时共享)。 | 任务必须在多核、多线程或多处理器上同时执行。 |
特点 | - 更注重任务的逻辑结构(任务可以部分完成)。 - 强调程序的设计能力,避免竞态条件。 |
- 强调硬件能力,要求硬件支持同时运行。 - 提升任务吞吐量。 |
典型场景 | - 多任务处理:如在 GUI 中,UI 响应用户交互的同时处理后台数据更新。 - 异步 I/O 操作。 |
- 科学计算:大规模矩阵计算、图像处理等。 - 并行数据处理,如 MapReduce、GPU 运算。 |
硬件要求 | 不需要依赖多核,多线程环境即可实现。 | 需要依赖多核、多处理器或 GPU。 |
技术示例 | - Java 的线程池(如 Executor)。 - Golang 的 Goroutines(协程)。 |
- CUDA 的 GPU 编程。 - OpenMP 多线程计算。 |
关键问题 | 如何设计任务的交替逻辑,避免死锁、资源竞争。 | 如何分配任务到多个计算单元,最大化硬件利用率。 |
书的名字:从 Java 开始做性能测试 。
如果本书内容对你有所帮助,希望各位多多赞赏,让我可以贴补家用。赞赏两位数可以提前阅读未公开章节。我也会尝试制作本书的视频教程,包括必要的答疑。
FunTester 原创精华