CodeGym /课程 /JAVA 25 SELF /并行性入门

并行性入门

JAVA 25 SELF
第 54 级 , 课程 0
可用

1. 多线程 vs 并行性

多线程:很多,但不一定同时进行

多线程是指你的程序中有多个执行线程。每个线程就像一条独立的行动线:一个在计算,另一个等待用户输入,第三个把数据保存到文件。在 Java 中,你通过类 Thread 创建线程,实现接口 Runnable,或使用 ExecutorService 等高级工具(关于它们——在下一讲)。

但是! 多线程并不保证你的任务真的同时执行。这取决于你处理器有多少个核。如果只有一个核,线程只是彼此之间快速“切换”——快到让人感觉像是同时发生。实际上,处理器在任意时刻只执行一个线程,其余的在排队等待。

并行性:当任务真正同时进行

并行性是指你的代码确实在处理器的多个核上同时执行。如果你有一台现代电脑,带有 4816 个核——你可以把大任务拆分为相互独立的部分并分配到各个核上,从而真正加速处理。

打个比方,多线程就像只有一个厨师,他在煮汤、煎肉饼和切沙拉之间快速“切换”。并行性则是有好几个厨师,每个人负责自己的菜。

实际有什么区别?

多线程强调的是便利性和响应性。你使用多个线程来避免程序“卡住”:一个线程等待网络,另一个线程绘制界面,第三个在计算。从感觉上看一切都在并行进行,但并不一定是同时的。

并行性强调的是速度。在这里,确实有多个处理器核同时执行任务的不同部分,以更快得到结果。

换句话说:多线程帮助组织工作,而并行性让它更快。

重要:
当存在可以独立完成的任务时,多线程总是有用的。
当你希望通过把工作真正分配到多个核上来加速计算时,需要并行性。

示例:处理一个大型数组

假设我们有一个包含 1000 万个数字的数组,想要计算所有元素的总和。

顺序方式:
一个线程遍历整个数组并计算总和。简单可靠,但耗时更长。

多线程(但只有一个核):
你把数组分成 4 部分,创建 4 个线程,每个线程计算自己的那一部分。但如果你只有一个核,线程只会轮流工作——不会加速,而且线程切换的开销甚至可能让程序更慢。

并行(在多个核上):
你把数组分成 4 部分,启动 4 个线程,并且每个线程确实在自己的核上运行。最终的总和由 4 个部分相加得到。这确实更快——尤其在大数据量上。

不过,实现数组的顺序处理非常简单,你们已经多次写过这类程序:

// 示例:数组的顺序处理
int[] arr = new int[10_000_000];
// ... 填充数组 ...
long sum = 0;
for (int x : arr) {
    sum += x;
}
System.out.println(sum);

多线程和并行版本会稍微复杂一些,我们将在接下来的讲座中借助现代工具进行讲解。

2. 为什么需要并行性

现代处理器早已超越单核。即便是你的智能手机,很可能也不止四核,而台式机和服务器——八核、十六核、三十二核甚至更多。如果应用能够使用所有这些核,它就能成倍地更快运行。

过去处理器性能主要靠提升主频——大约到 2000 年代中期以前确实有效。但频率增长受到了物理极限的制约,于是进入了多处理器与多核时代。现在,能有效把工作分配到各个核上的程序才会获益。

在哪些场景下并行性确实能加速?

  • 大数据处理:日志分析、统计、聚合——凡是可以拆成彼此独立的片段的任务。
  • 渲染、图像与视频处理:每个像素或片段都可以单独处理。
  • 科学计算、建模:数学问题、仿真、模型训练。
  • 服务器端应用:同时为大量客户端提供服务。
  • 响应式应用:需要对大量事件快速响应而不阻塞主线程。

什么时候并行性帮不上忙?

  • 如果任务很小,启动并行的开销可能大于收益。
  • 如果任务无法拆成独立部分(例如每一步都依赖上一步的结果)。
  • 当有大量共享资源(例如同一个文件)时,线程会彼此干扰。

3. 并行性的常见任务

我们来看哪些任务最常被“分发”到各个核上。

大规模数组计算

  • 求和、查找最大/最小值、对大型数组进行统计。
  • 示例:计算一百万个传感器的平均温度。

集合处理

  • 过滤、排序、转换大型列表(例如处理电商订单)。
  • 示例:选出价格高于 10 000 卢布的所有订单,并按日期排序。

渲染与图形处理

  • 对图像的所有像素应用滤镜(例如转换为黑白)。
  • 每个像素都可以独立处理——并行性的理想场景。

数据分析,Big Data

  • MapReduce、聚合、对海量数据进行统计。
  • 示例:处理一年的日志以查找异常。

示例:并行求和
假设我们有一个包含 100 万个数字的数组。可以把它分成 4 个部分,在每个线程中分别计算各自的部分,然后再把结果相加。

4. 并行性的难点与挑战

调试复杂
当代码以多线程方式运行时,缺陷可能只在少见的情况下才会出现,比如线程以某种特殊方式“交错”。有时错误在 1000 次运行中才出现一次——很难捕捉。

数据竞争(race condition)
如果多个线程同时修改同一个变量或对象——可能得到不正确的结果。例如,两个线程同时增加计数器,最终值却小于预期。

同步
为避免竞争,需要对共享数据的访问进行同步——通过关键字 synchronized、锁、原子变量等工具。这会让代码更复杂,也可能带来其他问题(例如 deadlock——线程间的相互阻塞)。

负载均衡
如果你把任务分成 4 部分,而其中一部分比其他都重得多——三个线程已经结束并在空等,第四个还在跑。最终得不到加速。

开销
启动线程、线程切换、同步——这些都需要时间。如果任务很小,并行反而会拖慢执行。

表格:方法对比

方法 何时更快 何时变慢 应用示例
顺序(1 个线程) 小任务,逻辑简单 大数据量 处理 10 行
多线程(在 1 个核上) 异步任务(等待 IO CPU-bound(受限于处理器计算)的单核任务 同时下载文件
并行性(多核) 大而独立的任务 小任务,耦合度高 处理大型数组

可视化:它看起来如何

// 顺序处理(1 个线程)
[任务 1][任务 2][任务 3][任务 4]

// 单核上的多线程(调度切换)
[任务 1] [任务 2] [任务 3] [任务 4]
(实际上一次只运行一个,其余都在等待)

// 在四核上的并行性
[任务 1]    [任务 2]    [任务 3]    [任务 4]
(全部同时执行)

5. 尝试并行化时的常见错误

错误 1:盲目地把一切都并行化。 很多新人以为:“线程越多——就越快!”其实并非如此。如果任务很少或过于简单——就没有收益,有时程序甚至更慢.

错误 2:忽视同步。 如果多个线程在没有同步的情况下操作同一份数据——就会出现数据竞争、逻辑破坏以及难以捕捉的缺陷。

错误 3:为并行而并行。 并行性不是目的本身。只有当存在可以高效拆成独立部分的真实任务时,它才有意义。

错误 4:忽视任务特性。 有些任务根本无法并行(例如第 N+1 步依赖第 N 步的结果)。在这种情况下,并行性不会带来优势。

错误 5:忽视开销。 启动线程、线程切换、结果汇总——这些都需要时间。对小任务而言,这些时间可能超过任务本身的执行时间。

评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION