1️⃣ 背景
假设我们有一个 PDF 文件,需要:
-
将每页渲染成图片
-
编码为 PNG
-
上传到服务器
-
获取访问 URL
如果 PDF 很大,顺序处理每页很慢。
解决办法:多线程并行处理每页。
2️⃣ 核心技术点
-
@Async
-
Spring 提供的异步执行注解
-
方法加上
@Async后,会被线程池异步调用 -
必须在 Spring 管理的 Bean 中调用
-
-
线程池(ThreadPoolTaskExecutor)
-
控制并发数量,防止内存/CPU 过载
-
示例:
@Bean("doSomethingExecutor") public Executor doSomethingExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(4); executor.setMaxPoolSize(8); executor.setQueueCapacity(100); executor.setThreadNamePrefix("pdf-async-"); executor.initialize(); return executor; } -
-
CompletableFuture
-
用于收集异步任务的返回结果
-
可以统一等待所有任务完成或处理异常
-
3️⃣ 核心步骤
步骤1:定义异步方法
@Async("doSomethingExecutor")
public CompletableFuture<String> processPage(String filePath, int pageIndex) {
try (PDDocument document = PDDocument.load(new File(filePath))) {
PDFRenderer renderer = new PDFRenderer(document);
BufferedImage image = renderer.renderImageWithDPI(pageIndex, 96, ImageType.RGB);
// 转 PNG
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ImageIO.write(image, "PNG", baos);
image.flush();
// 上传
String objectName = UUID.randomUUID() + ".png";
try (InputStream inputStream = new ByteArrayInputStream(baos.toByteArray())) {
fileClient.putObject("application-file", objectName, inputStream);
}
String fileUrl = fileClient.getObjectIPUrl("application-file", objectName);
return CompletableFuture.completedFuture(fileUrl);
} catch (Exception e) {
CompletableFuture<String> failedFuture = new CompletableFuture<>();
failedFuture.completeExceptionally(e);
return failedFuture;
}
}
重点:
-
每页任务独立,避免
PDDocument多线程安全问题 -
异常通过
completeExceptionally返回
步骤2:触发异步任务(for 循环)
List<CompletableFuture<String>> futures = new ArrayList<>();
for (int i = 0; i < pageCount; i++) {
futures.add(pdfPageProcessor.processPage(filePath, i));
}
// 等待所有任务完成
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
// 获取结果
List<String> urls = futures.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList());
解释:
-
循环只是触发任务,真正并行是线程池执行
-
allOf.join()等待所有任务完成 -
join()可以获取每页的 URL 或抛出异常
步骤3:控制并发
-
PDF 渲染是内存密集型操作
-
建议线程池核心数 4~8,根据服务器配置调整
executor.setCorePoolSize(4); executor.setMaxPoolSize(8);
-
避免一次性开太多线程导致 OOM
4️⃣ 流程示意
主线程
│
│ for i = 0..N-1
│
├─> 调用 @Async 方法
│ │
│ └─> 提交任务到线程池
│ │
│ ├─> 线程1 执行第1页
│ ├─> 线程2 执行第2页
│ └─> ...
│
└─> CompletableFuture.allOf 等待完成
│
└─> 收集每页 URL
5️⃣ 优化建议
-
一次性加载 PDF
-
避免每页都重新打开 PDF 文件
-
每页可创建独立
PDFRenderer渲染
-
-
降低 DPI 或控制图片大小
-
减少内存占用
-
-
合理控制线程池大小
-
避免 CPU 或内存过载
-
-
异常统一处理
-
避免部分页失败被吞掉
-
6️⃣ 总结
-
多线程的关键:@Async + 线程池
-
循环只是触发异步任务,不是真正的并行
-
CompletableFuture用于收集结果 -
内存/线程池大小是性能瓶颈
500



被折叠的 条评论
为什么被折叠?



