前面两篇文章,对 java 开启 线程 ,以及线程的状态做了简要分析。通过分析不难发现,通过 继承 Thread 类、实现Runnable接口、实现Callable接口这三种方式不仅复杂,而且可能带来不可预估的线程问题。本章着重对第四种方式: 线程池 开启 多线程 的方式进行探讨。
一、自动开启线程池
首先,我们来看一下Executor接口的实现类图(为了更直观,我把 Executor 的实现类全部生成出来)
通过上图,不难看出,以下几个关键点:
1、Executor线程池相关顶级接口,它将任务的提交与任务的执行分离开来
2、ExecutorService继承并扩展了Executor接口,提供了Runnable、FutureTask等主要线程实现接口扩展
3、 ThreadPoolExecutor 是线程池的核心实现类,用来执行被提交的任务
4、ScheduledExecutorService继承ExecutorService接口,并定义延迟或定期执行的方法
5、ScheduledThreadPoolExecutor继承ThreadPoolExecutor并实现了ScheduledExecutorService接口,是延时执行类任务的主要实现
了解了类之间的 依赖关系,那我们来关注下 Executors工具类中,Executors是 自动创建线程池的工具类。 我们再来看一下源码。
从上图不难看出,共有六种线程池新建方法。我们根据源码来逐一来探讨下。
1、new CachedThreadPool()
特点 : CachedThreadPool 是一种用来处理大量短时间工作任务的线程池。它会试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程;
如果线程闲置的时间超过 60 秒,则被终止并移出缓存;长时间闲置时,这种线程池,不会消耗什么资源。其内部使用SynchronousQueue 作为工作队列
2、new FixedThreadPool(int)
特点 :参数N 指定固定的线程数量,其背后使用的是无界的工作队列,任何时候最多有 n 个工作线程是活动的。这意味着,如果任务数量超过了活动队列数目,将在工作队列中等待空闲线程出现;如果有工作线程退出,将会有新的工作线程被创建,以补足指定的数目 n。
3、new ScheduledThreadPool(int corePoolSize)
特点 : 类似与new SingleThreadScheduledExecutor(),建的是个 ScheduledExecutorService,可以进行定时或周期性的工作调度,区别在于单一工作线程还是多个工作线程。
4、new SingleThreadExecutor()
特点: 工作线程数目限制为 1,操作一个无界的工作队列;它保证了所有任务的都是被顺序执行,最多会有一个任务处于活动状态,并且不允许使用者改动线程池实例,可避免其改变线程数目
4、new SingleThreadScheduledExecutor()
特点: 创建单线程池,返回 ScheduledExecutorService,可以进行定时或周期性的工作调度。
6、new WorkStealingPool(int parallelism)
特点: Java 8 加入这个创建方法,其内部会构建ForkJoinPool,利用Work-Stealing算法,并行地处理任务,不保证处理顺序。
二、手动开启线程池
大家通过阅读new CachedThreadPool() 、new SingleThreadExecutor()、new FixedThreadPool(int n)三个方法的源代码不难看出,这三个方法都是对 都是对ThreadPoolExecutor的封装。那我们是不是也可以通过ThreadPoolExecutor()方式来手动开启线程池呢?答案是肯定的。那我们还是老样子,根据源码来解析ThreadPoolExecutor()的使用。
从源码可以看出, ThreadPoolExecutor()提供了四种 构造方法 。网上对ThreadPoolExecutor的参数解析比较全,为了节省时间,我们直接看下图:
我们通过代码配置来更直观的体验上述配置。
踩坑指南: 如果我们再局部使用了线程池,核心线程数大于0的情况下,启动后线程池会一直保留 不会自动关闭, 所以当我们局部方法中使用了线程池,一定记得手动关闭,避免内存泄露
如何避免上述情况发生呢:
1)调用shutdown()方法。注意 调用 shutdown 方法并不会立刻导致线程池被关闭且销毁,而是线程池不会再接收新的任务,等线程池之前正在执行的所有任务完毕之后,才会被关闭并销毁。
2)另外一种操作是把 核心线程数量指定为0 ,这样的话线程池在执行完任务后就会 自动关闭 线程池了。
三、ThreadPoolExecutor使用注意事项
1、任务执行情况
1)任务数小于等于corePoolSize
任务直接被线程池中已存在的核心线程执行
2)任务数大于corePoolSize且小于等于 corePoolSize+队列容量
无法被及时执行的任务会先进入队列等待,然后等核心线程空闲了被执行
3)任务数大于 corePoolSize+队列容量且小于等于maximumPoolSize+队列容量
其中一部分任务会在队列中等待,队列放不下的任务会创建新的线程去执行。这些 新创建的线程也叫做非核心线程,任务执行完毕后,在指定存活时间内没有新任务要 执行的话,就会被销毁。
4)大于maximumPoolSize+队列容量
线程池没有办法执行该任务了,就会执行拒绝策略的方法。
2、拒绝策略
上面我们说到,ThreadPoolExecutor 中最后一个参数是拒绝策略,入如果使用默认策略,当任务无法被线程池执行时,就会走拒绝策略的代码并报 RejectedExecution Exception 异常。
当然,我们也有四种策略可以选择,拒绝策略如下所示:
拒绝策略的选择很重要,如果出现线程丢弃的情况,一定注意业务补偿。
3、钩子函数
ThreadPoolExecutor提供了受保护的可重写的钩子函数,用于在线程池 初始化或者执行完任务后做一些特殊处理,同样也提供了在线程池终止时可以覆写的terminated方法。源码如下所示:
protected void beforeExecute(Thread t, Runnable r) { }
protected void afterExecute(Runnable r, Throwable t) { }
protected void terminated() { }
4、获取线程池的执行结果 (submit、invokeAny、invokeAll)
如果我们要获取线程的返回结果,可以使用以下三个方式获得
1) submit() : 将一个任务提交给线程池执行,返回Future
2) invokeAny(tasks) :提交一个任务集合给线程池之执行,当其中任意一个任务完成后就返回该任务的执行结果,并取消其他任务
3)invokeAll( tasks): 提交一个任务集合给线程池之执行,使每个任务都运行,返回所有任务执行的结果
四、对于核心线程数和最大线程数设置的建议
我们在配置核心线程数和最大线程数时,该怎么去配置呢?一般我们会把要执行的任务分为“CPU密集型”、“IO密集型”两大类,根据分类我们可以按照如下所示去配置:
1、IO密集型: 一般指跟内存或磁盘交互较多的任务,比如 网络请求,读写文件 等操作。这种类型的任务一般阻塞时间会长一些,我们可以将最大线程数量设置的多一点,比如 cpu 核数的两倍,以便于出现cpu密集型的任务可以得到执行。
2、 CPU 密集型:一般指大量的计算任务,此时,我们可以将设备CPU的核数作为最大线程数量
总结:
本篇文章主要从自动开启线程池和手动开启线程池两部分做了简要说明,并详细说明了 ThreadPoolExecutor的一些原理和参数。希望能给大家使用线程池的时候做一些正确指引。
相关文章