最近在看杨中科老师的课程,
https://www.bilibili.com/video/BV1KB4y1c7V2?p=43&vd_source=2f6c1b009448fb3a45b18078a369d8a0
其中有一个场景,读取一个PDF文件中的所有图片,并将图片输出到一个目录中,使用杨老师封装的包,可以非常容易的实现该需求(基础)。(公司业务中也有类似场景)但是当PDF文件较大,使用单线程处理文件耗时较长,因此考虑多线程实现一个较短耗时的读取图片操作。
线程的创建和销毁消耗资源严重,建议使用线程池进行管理,且某里公司规范要求i需要使用ThreadPoolExecutor来实现线程池。
每个线程负责处理一定页数的PDF文件,并将其中的图片输出到指定目录,当所有线程处理完毕,关闭IO流,符合了CountDownLatch的应用范围。
贴个代码,即将更新代码说明,并修复页码下标越界问题,以下代码性能,由full gc 风险。
public class PDFImages {
static ThreadPoolExecutor executor;
public static void main(String[] args) {
//ThreadPoolExecutor executor = null;
executor = new ThreadPoolExecutor(5,40,100, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(10),Executors.defaultThreadFactory(),new ThreadPoolExecutor.CallerRunsPolicy() );
PDDocument pdf = PDFHelpers.openFile("C:\\Users\\HP\\Desktop\\简历\\深入浅出Spring Boot 2.x.pdf");
int pageNum = pdf.getNumberOfPages();
int lacthers = pageNum / 10 + 1 ;
CountDownLatch latch = new CountDownLatch(lacthers);
for (int i = 1; i <= lacthers; i++) {
executor.execute(new ReadPDFThread(i,pdf,latch,pageNum));
}
try {
latch.await();
pdf.close();
} catch (Exception e) {
e.printStackTrace();
}
}
public static class ReadPDFThread implements Runnable{
int page;
PDDocument pdf;
CountDownLatch latch;
int endPageNum;
public ReadPDFThread(int page,PDDocument pdf,CountDownLatch latch,int endPageNum){
this.page = page;
this.pdf = pdf;
this.latch = latch;
this.endPageNum = endPageNum;
}
@Override
public void run() {
System.out.println("当前线程"+this.page+"执行任务");
//多线程读取页码范围内的文件,并将其中的图片保存到目标文件夹中
long startPage = page * 10 - 10 ;
long endPage = page * 10 ;
if(startPage + 10 >= endPageNum){
endPage = endPageNum;
}
for(long i=startPage;i<endPage;i++){
PDPage page = pdf.getPage((int)i);
List<byte[]> bytes = PDFHelpers.parseImages(page, ".png");
for(byte[] abyte : bytes){
IOHelpers.writeAllBytes("D:/temp/pdfImages/"+i+"ye"+System.currentTimeMillis()+".png",abyte);
}
}
latch.countDown();
}
}
}
依赖的包
<dependency>
<groupId>com.yzk18</groupId>
<artifactId>yzk18-docs</artifactId>
<version>1.3</version>
</dependency>
任务在线程池中执行的过程
1.工作线程数<核心线程数,向线程池中添加线程,当前任务会被新线程执行。
2.工作线程数>=核心线程数,检查等待队列是否可新增排队任务,任务会加入等待队列。
3.检查到等待队列已满,就会执行拒绝策略,该任务踢出队列或是报错等。
回收
常用等待队列
1.有界任务队列,数组实现,需要指定大小,影响效率得关键因素
new ArrayBlockingQueue(size);
2.无界任务队列,链表实现
new LinkedBlockingQueue();
3.优先级队列,加入得任务指定优先级,java中有10级,但是不同操作系统优先级定义不同,当调度优先级抢占式算法,多个代码级别优先级映射到同一个优先级中,例如linux中得7级映射,不推荐使用。
4.同步队列,有任务直接添加新线程执行,直到工作线程数达到最大线程数,无法分配新线程则执行拒绝策略,不常用。
拒绝策略
1.拒绝执行抛异常
new ThreadPoolExecutor.AbortPolicy()
2.拒绝执行不抛异常
new ThreadPoolExecutor.DiscardPolicy()
3.挤队头任务,不常用
new ThreadPoolExecutor.DiscardOldestPolicy()
4.谁提交谁执行
保证了任务不会丢失,存在效率降低得问题,
当设置这个策略后,任务队列已满,新任务会被线程调用方会直接执行,出现任务插队情况。
new ThreadPoolExecutor.CallerRunsPolicy()
拒绝策略需要按项目需求选择,例如选择挤掉队头和抛任务策略,任务被挤掉之后可以搭配MQ进行任务补偿,保证结果的最终一致性。