跳到正文
hello world

1. 为什么要学习 Java 并发

发布于阅读量 0

这篇专辑和以往不一样,不再只是看 API,而是用一个比较具体的小项目来贯穿:批量给 PDF 文件添加水印

这个例子不算复杂,但刚好能把很多并发知识串起来。比如:

一个 PDF 一个任务;
多个 PDF 可以同时处理;
不能一次性开太多线程;
主线程需要知道所有 PDF 什么时候处理完;
每个 PDF 可能成功,也可能失败;
处理时间太长时,还要考虑超时。

这些问题其实就已经涉及到 Java 并发里最常见的一批工具了。


为什么不用普通顺序执行?

假设现在 input 目录下有 10 个 PDF 文件。

最简单的写法当然是一个一个处理:

for (File file : files) {
    addWatermark(file);
}

这种写法很好理解,也最安全。

但是它的问题也明显:

比如一个 PDF 处理需要 3 秒,10 个 PDF 就差不多要 30 秒。文件再多一点,比如 100 个,等待时间就很难接受了。

而且 PDF 打水印这件事,并不是说必须处理完第一个,才能处理第二个。大多数情况下,每个文件之间是独立的:

a.pdf 可以处理;
b.pdf 也可以同时处理;
c.pdf 也不需要等 a.pdf 完成。

既然任务之间没有强依赖,那就可以考虑并发处理。


并发不是越多越好

一开始很容易有一个误区:既然多线程能变快,那我是不是每个 PDF 都开一个线程?

比如:

100 个 PDF
↓
创建 100 个线程
↓
同时处理

看起来很爽,但实际项目里这样很危险。

线程本身也是资源。线程太多,会带来几个问题:

CPU 频繁切换线程;
内存占用变高;
磁盘读写压力变大;
PDF 文件比较大时,还可能导致内存不够;
程序不一定更快,反而可能更慢。

所以学习 Java 并发,不是学会“怎么开线程”就完了,更重要的是学会怎么控制并发数量

比如一次最多处理 3 个 PDF:

第 1 批:a.pdf、b.pdf、c.pdf
第 2 批:d.pdf、e.pdf、f.pdf
第 3 批:g.pdf、h.pdf、i.pdf

这样既能比单线程快,又不会把机器压垮。

这就是后面要学的 SemaphoreThreadPoolExecutor 这些工具存在的原因。


这个 PDF 水印项目能学到什么?

我准备用这个项目一步一步学习 Java 并发。

刚开始先用最基础的 Thread,看看线程到底是怎么启动的,start()run() 有什么区别,为什么主线程有时候会提前结束。

然后会用 join() 让主线程等待子线程完成。

接着会遇到线程安全问题,比如多个线程同时修改一个变量,为什么结果会不对。这个时候再学习 synchronizedReentrantLock,就不会觉得它们只是语法了,而是能明白它们到底在解决什么问题。

后面会进入工作中更常用的几个工具:

Semaphore:控制同一时间最多几个任务在执行;
CountDownLatch:等待一批任务全部完成;
ThreadPoolExecutor:用线程池管理任务,而不是不停 new Thread;
Future:拿到异步任务的处理结果;
CompletableFuture:把异步任务继续编排起来,比如处理完成后生成下载地址、记录日志、处理异常。

这样学下来,每一个类都不是孤立的,而是围绕一个具体问题出现。


为什么这个例子适合学并发?

PDF 打水印这个例子有几个好处。

第一,它是文件处理任务,和真实项目比较接近。不是那种只写 count++ 的玩具例子。虽然 count++ 很适合理解线程安全,但它离业务还是远了一点。

第二,它天然适合拆成多个任务。一个 PDF 就是一个任务,这和线程池里的 RunnableCallable 很容易对应起来。

第三,它能引出很多真实问题。

比如:

处理一个 PDF 失败了怎么办?
某个 PDF 处理太慢怎么办?
怎么知道所有 PDF 都处理完了?
怎么统计成功和失败数量?
怎么限制同时处理的文件数量?

这些问题都不是背 API 能解决的,必须放到代码里跑一遍,才会真正理解。


我的学习思路

这次学习我不打算一上来就写最完美的代码。

我会先写很简单、甚至有点笨的代码。比如每个 PDF 都 new Thread(),然后再慢慢发现问题:

线程太多怎么办?
主线程怎么等?
任务结果怎么拿?
异常怎么处理?
超时怎么办?

每遇到一个问题,再引入一个新的并发工具。

这样学起来会更自然一点。

因为在真实工作中,很多技术也不是一开始就“为了用而用”,而是代码写着写着发现有问题,然后才需要更合适的工具。


先记住一个核心判断

学并发之前,我觉得可以先记住一句话:

并发不是为了多开线程,而是为了让多个独立任务更合理地同时执行。

这里有两个关键词:

独立任务;
合理地同时执行。

如果任务之间本来就有先后依赖,那就不能乱并发。

如果机器资源有限,也不能无限并发。

所以后面学习 Java 并发,其实一直都在围绕这几个问题:

任务怎么拆?
线程怎么用?
并发数量怎么控制?
结果怎么收集?
异常怎么处理?

PDF 水印项目只是一个入口。真正要学的是这一套处理问题的思路。

1. 为什么要学习 Java 并发 - Java并发之从Thread到CompletableFuture - 上下文网