相信线程池这个东东只要只要是做过开发的童鞋们都熟悉,面试也是经常会问到的一个问题,用起来那是相当地简单啊,而且功能强劲,优化的又好,使用还去除了冗余的代码层级,提高了代码的可读性,创建线程池只需要用到四种
- newCachedThreadPool 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
- newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
- newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
- newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
随便拿出一个来实例化举例下,创建线程的代码是这样的
- // 创建指定大小的线程池,池里最多只能有2个线程
- ExecutorService executorService = Executors.newFixedThreadPool(2);
- // 创建新线程
- executorService.execute(new Runnable() {
- @Override
- public void run() {
- System.out.println("我是线程池创建出来的线程");
- }
- });
进入源码看看,我们可以看到,其实线程池用的是 ThreadPoolExecutor 这个类
- public static ExecutorService newFixedThreadPool(int nThreads) {
- return new ThreadPoolExecutor(nThreads, nThreads,
- 0L, TimeUnit.MILLISECONDS,
- new LinkedBlockingQueue<Runnable>());
- }
在看看 ThreadPoolExecutor 这个类的构造函数,发现它其实是有7个参数的
- public ThreadPoolExecutor(int corePoolSize,
- int maximumPoolSize,
- long keepAliveTime,
- TimeUnit unit,
- BlockingQueue<Runnable> workQueue,
- ThreadFactory threadFactory,
- RejectedExecutionHandler handler) {
- }
线程池底层执行流程图
参数说明
那这 7 个参数分别代表什么呢?这就到了我们文章的重点部分了,别着急,我们一个个地详细解答;
1、corePoolSize 【核心线程数】
核心线程数就是线程池实际的线程数量,且这些线程一旦开启后就不会轻易关闭,队列中的任务执行时优先使用空闲的核心线程,直到超过 keepAliveTime 时间后才会关闭;
2、 maximumPoolSize 【最大线程数】
线程池能 容纳的最大线程数量,当核心线程满了,并且队列也满了之后,在来新任务的话,就会使用最大线程数的空间来创建新线程;
3、 keepAliveTime 【空闲线程存活时间】
核心线程在执行完任务后不会立马关闭,而是直到超过 keepAliveTime 时间后才会关闭,如果把 keepAliveTime 为 0 ,那么执行完任务后会立马回收(这一点很多博主都写错了,说是设置为0将永不回收,其实是错误的)
4、unit 【空闲线程存活时间单位】
keepAliveTime 的时间单位,主要有以下几种:
- TimeUnit.DAYS 天
- TimeUnit.HOURS 小时
- TimeUnit.MINUTES 分钟
- TimeUnit.SECONDS 秒
- TimeUnit.MILLISECONDS 毫秒
- TimeUnit.MICROSECONDS 微秒
- TimeUnit.NANOSECONDS 纳秒
5、 workQueue 【等待执行的任务队列】
当核心线程运行的任务数量已经满了之后,那么在添加进来的任务就进入 workQueue 队列中,待核心线程任务执行完后会依次执行队列中的任务;等待执行的队列遵循先进先出,后进后出的原则,就是先加进来的任务优先执行;默认任务队列使用的 ConcurrentLinkedQueue 队列,是一个无界的非阻塞队列;无界是没有界限的意思,大小不受限制,会自动扩容;就像动态数组一样;
6、 threadFactory 【创建新线程时使用的工厂】
创建新线程时使用的工厂类,默认使用 DefaultThreadFactory ,可自定义工厂,只需要继承 ThreadFactory 接口即可;可以用来设定线程名、是否为daemon(守护线程)等等 ,
7、 handler 【拒绝策略】
当工作队列中的任务已到达最大限制,并且线程池中的线程数量也达到最大限制,这时如果有新任务提交进来,该如何处理呢。这里的拒绝策略,就是解决这个问题的,拒绝策略需要实现 RejectedExecutionHandler 接口,jdk中提供了4种拒绝策略
-
CallerRunsPolicy 直接在主线程运行被拒绝的任务,除非线程池已关闭,已通过实验证实;我们看看源代码,r.run() 里面的 r 其实就是主线程
-
AbortPolicy 直接丢弃任务,并抛出RejectedExecutionException异常;
-
DiscardPolicy 直接丢弃任务,什么也不做,看源码就知道,里面啥也没有
- DiscardOldestPolicy 抛弃进入队列最早的那个任务,然后尝试把这次拒绝的任务放入队列
当然除了jdk自带的策略外,你也可以通过实现 RejectedExecutionHandler 接口来自定义策略;
配置线程池
要合理地配置线程池,根据代码密集型进行配置即可
- CPU密集型:
- 任务需要大量运算,没有阻塞的情况,cpu全速运行;
- 配置最大线程数 = CPU核数
- IO密集型 :
- 大量访问io。产生阻塞,尽量使用多线程技术;
- 配置最大线程数 = 2 * CPU核数