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
这样既能比单线程快,又不会把机器压垮。
这就是后面要学的 Semaphore、ThreadPoolExecutor 这些工具存在的原因。
这个 PDF 水印项目能学到什么?
我准备用这个项目一步一步学习 Java 并发。
刚开始先用最基础的 Thread,看看线程到底是怎么启动的,start() 和 run() 有什么区别,为什么主线程有时候会提前结束。
然后会用 join() 让主线程等待子线程完成。
接着会遇到线程安全问题,比如多个线程同时修改一个变量,为什么结果会不对。这个时候再学习 synchronized 和 ReentrantLock,就不会觉得它们只是语法了,而是能明白它们到底在解决什么问题。
后面会进入工作中更常用的几个工具:
Semaphore:控制同一时间最多几个任务在执行;
CountDownLatch:等待一批任务全部完成;
ThreadPoolExecutor:用线程池管理任务,而不是不停 new Thread;
Future:拿到异步任务的处理结果;
CompletableFuture:把异步任务继续编排起来,比如处理完成后生成下载地址、记录日志、处理异常。
这样学下来,每一个类都不是孤立的,而是围绕一个具体问题出现。
为什么这个例子适合学并发?
PDF 打水印这个例子有几个好处。
第一,它是文件处理任务,和真实项目比较接近。不是那种只写 count++ 的玩具例子。虽然 count++ 很适合理解线程安全,但它离业务还是远了一点。
第二,它天然适合拆成多个任务。一个 PDF 就是一个任务,这和线程池里的 Runnable、Callable 很容易对应起来。
第三,它能引出很多真实问题。
比如:
处理一个 PDF 失败了怎么办?
某个 PDF 处理太慢怎么办?
怎么知道所有 PDF 都处理完了?
怎么统计成功和失败数量?
怎么限制同时处理的文件数量?
这些问题都不是背 API 能解决的,必须放到代码里跑一遍,才会真正理解。
我的学习思路
这次学习我不打算一上来就写最完美的代码。
我会先写很简单、甚至有点笨的代码。比如每个 PDF 都 new Thread(),然后再慢慢发现问题:
线程太多怎么办?
主线程怎么等?
任务结果怎么拿?
异常怎么处理?
超时怎么办?
每遇到一个问题,再引入一个新的并发工具。
这样学起来会更自然一点。
因为在真实工作中,很多技术也不是一开始就“为了用而用”,而是代码写着写着发现有问题,然后才需要更合适的工具。
先记住一个核心判断
学并发之前,我觉得可以先记住一句话:
并发不是为了多开线程,而是为了让多个独立任务更合理地同时执行。
这里有两个关键词:
独立任务;
合理地同时执行。
如果任务之间本来就有先后依赖,那就不能乱并发。
如果机器资源有限,也不能无限并发。
所以后面学习 Java 并发,其实一直都在围绕这几个问题:
任务怎么拆?
线程怎么用?
并发数量怎么控制?
结果怎么收集?
异常怎么处理?
PDF 水印项目只是一个入口。真正要学的是这一套处理问题的思路。