一、业务场景
最近在做项目时,遇到一个业务场景:由于手机浏览器直接加载大PDF文件可能会导致加载失败,因此当用户上传PDF文件到FastDFS的时候,需要将pdf原文件上传,并且按照pdf文件页数转化成对应的图片在上传到FastDFS上。因为PDF转图片是一个比较耗时的操作,因此需要用到异步任务。
二、分析
首先想到的就是主线程去执行上传pdf原文件的操作,然后再开启一个线程来进行PDF转图片并上传fastdfs的操作,pdf原文件上传成功后即返回给用户上传成功的标志。
如果每发起一次请求,开启一个线程,当并发请求多的时候会导致线程数量非常多。而线程是非常宝贵的资源,随着并发访问量的提升,会发生线程堆栈溢出,创建新线程失败等问题。因此可以考虑采用线程池的方式来避免这种问题。(小伙伴,有没有感觉很熟悉,是不是非常类似于网络编程中的BIO和伪异步IO)。
三、java中的线程池
首先先谈一谈为什么要用线程池?
其一,减少创建和销毁线程所消耗的资源。其二,就是将当前任务与主线程隔离,能实现和主线程的异步执行,需要注意的是,一味的开线程也不一定能提高性能,线程休眠也需要耗费内存资源,所以需要合理选择线程池的大小。
我们的任务(Task)提交的线程池之后,又是按照什么样的规则去运行的呢?
1、exector一个线程之后,如果线程池中的线程数未达到核心线程数,则会立马启用一个核心线程去执行。
2、exector一个线程之后,如果线程池中的线程数已经达到核心线程数,且workQueue未满,则将任务放到workQueue中等待执行。
3、exector一个线程之后,如果线程池中的线程数已达到核心线程数但未超过非核心线程数,且workQueue已满,则开启一个非核心线程来执行任务。
4、execute一个线程之后,如果线程池中的线程数已经超过非核心线程数,则按照Hanlder策略做对应的方案,拒绝、交给调用线程处理等。
几种常见线程池:
1、CachedThreadPool :用于并发执行大量短期的小任务,或者是负载较轻的服务器
2、FixedThreadPool:需要限制当前线程数量,用于负载比较重的服务器。
3、SingleThreadExecotrs:用于串行执行任务的场景,每个任务按顺序,不需要并发执行。
4、ScheduledThreadPoolExecutor 用于需要多个后台线程执行周期任务,同时需要限制线程数量的场景
自定义线程池:如果任务是CPU密集型(需要进行大量计算、处理),则应该配置尽量少的线程,比如CPU个数+1,这样可以避免每个线程都需要使用很长时间,但是有太多线程争抢资源的情况。如果任务是IO密集型(主要时间都在I/O,空闲时间比较多)则应该配置多一些线程,比如CPU个数的两倍,这样能够充分压榨CPU的性能。
线程池框架图如下所示:
四、示例代码
1、异步任务类
//模拟异步任务 public class ExecutorDemo { private ExecutorService executor= Executors.newFixedThreadPool(4); public void asynTask(){ executor.submit(new Runnable() { @Override public void run() { try { Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } int sum=0; for (int i=0;i<1000;i++){ sum+=i; } System.out.println(sum); System.out.println("-------------asynTask end--------------"); } }); } }
2、主任务类
public class ExecutorDemoClient { public static void main(String[] args){ ExecutorDemo e =new ExecutorDemo(); e.asynTask(); System.out.println("-------------main end--------------"); }
输出结果示例:
可以看到主线程已经结束了,然后异步任务线程才结束。
四、总结:
通过线程池的方式可以解决异步的问题,但是呢,如果异步任务执行失败,可能会伴随着数据的丢失等。