文章目录
- 1、线程池
- 2、分类
- 3、线程池的使用
- 4、工作流程
- 5、拒绝策略
- 6、线程池的七个参数
- 7、自定义线程池
- 8、什么时候考虑使用线程池?
1、线程池
线程池和数据库连接池的理念很相似,对于数据库连接池:普通的连接数据库是建立一个JDBC连接,执行完sql之后,就会关闭,即销毁connection对象,再次连接还需要重复上述步骤。当与数据库交互频繁时,这种模式会严重影响程序的性能,因此有了数据库连接池。对应到线程池thread pool,就是线程池里维护着多个线程,等待监督管理者分配执行任务。线程池带来的好处就是:
- 降低资源消耗:降低避免频繁创建和销毁线程的代价
- 提高响应速度:任务达到时,不用再等待创建线程
- 线程管理方便:线程过多,调度开销大,用线程池可防止过分调度,且可以做统一的监控、分配、调优
关于线程切换的例子:10 年前单核 CPU 电脑,假的多线程,像马戏团小丑玩多个球,CPU 需要来回切换。 现在是多核电脑,多个线程各自跑在独立的 CPU 上,不用频繁切换,效率高。
2、分类
Java 中的线程池是通过 Executor 框架实现的,该框架中用到了 Executors(工具类)、ExecutorsExecutorService、ThreadPoolExecutor这几个类
线程池有以下几类:
- 一池N线程:Executors.newFixedThreadPool(int num)
- 一池一线程:Executors.newSingleThreadExecutor()
- 可扩容池,根据需求创建一定数量的线程,遇强则强:Executors.newCachedThreadPool()
3、线程池的使用
- 创建线程池对象
- 调用execute方法提交任务
public class ThreadPoolDemo {
public static void main(String[] args) {
//一池五线程
ExecutorService threadPool = Executors.newFixedThreadPool(3);
//一池1线程
ExecutorService threadPool1 = Executors.newSingleThreadExecutor();
//一池可扩容线程
ExecutorService threadPool2 = Executors.newCachedThreadPool();
//提交10次任务到线程池
try{
for (int i = 1; i <= 20; i++) {
//提交任务到另一线程(线程池中的)
threadPool2.execute(() -> {
System.out.println(Thread.currentThread().getName() + "线程正在办理业务");
});
}
}catch(Exception e){
e.printStackTrace();
}finally{
threadPool2.shutdown();
}
}
}
以可扩容线程池为例:
4、工作流程
如图,此时常驻线程数为2,最大线程数为5,阻塞队列长度为3(黑点),此时来了两个任务1和2 ⇒ 直接常驻线程 ⇒ 那两个任务还执行完,又来了几个任务3、4、5 ⇒ 这时不是直接上最大线程,而是进入阻塞队列 ⇒ 此时又来了三个人6、7、8 ⇒ 发现阻塞队列也满了,那就开启最大线程处理6、7、8的业务 (注意新开的线程不是去处理阻塞队列了,阻塞队列的3、4、5还是在队列中继续等待) ⇒ 此时又来了一个任务9 ⇒ 走拒绝策略
注意这几点:
ExecutorService pool = Executors.newSingleThreadExecutor();
- 执行上面这句,并不会创建线程,而是执行pool.execute方法提交任务时才创建
- 常驻线程用完了,再来任务,不是直接按最大线程数启动新线程,而是阻塞队列
- 阻塞队列满了以后,按最大线程数启动新线程,且新线程处理的不是阻塞队列里的任务
看下源码,从提交任务的execute方法打断点,进入execute方法:
5、拒绝策略
阻塞队列和最大线程数量都用完后,走拒绝策略,JDK内置的拒绝策略有:
- AbortPoligy(默认):直接抛出RejectedExecutionExption异常阻止系统正常运行
- CallerRunsPoliy:既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,谁让你来的,你找谁去
- DiscardOldestPoliy:抛弃阻塞队列中等待最久的任务,然后把当前任务加入队列中
- DiscardPolicy:默默地丢弃无法处理的任务,不予任何处理也不抛出异常,如果允许任务丢失,这是最好的一种策略
6、线程池的七个参数
查看源码可以发现,不管是三种线程池中的哪种,最后都是return new ThreadPoolExecutor
,关于ThreadPoolExecutor类:
- int corePoolSize:常驻线程数量
- int maximumPoolSize:最大线程数量
- long keepAliveTime:线程存活时间,线程多长时间没被使用就关闭
- TimeUnit unit:存活时间的单位
- BlockingQueue workQueue:常驻线程用完了,再来请求线程,进入阻塞队列
- ThreadFactory threadFactory:线程工厂
- RejectedExecutionHandler handler:拒绝策略
以银行为例对比:银行大厅一共有10个窗口(最大线程数量),但平时一般只开5个(常驻线程数量),某天办理业务的人很多,5个窗口不够用,其余人来了就先在大厅椅子上坐着等(阻塞队列),结果椅子坐满了,还有人陆续来,于是10个窗口全开,还来很多人,那就只能告诉新来的今天轮不到你办了(拒绝策略)。
7、自定义线程池
Executors工具类可以创建三种线程池,但通常自定义线程池是因为,Executors返回的线程池对象有以下两个问题:
- 对于FixedThreadPool和SingleThreadPool,代码底层用的阻塞队列是LinkedBlockingQueue类型的,队列长度为Integer.MAX_VALUE,可能堆积大量请求,导致OOM
- 对于CachedThreadPool,其源码中写的最大线程数量为Integer.MAX_VALUE,创建大量线程,调度难度大且会OOM
public class ThreadPoolDemo2 {
public static void main(String[] args) {
ExecutorService threadPool = new ThreadPoolExecutor(
2, //常驻或核心线程数
5, //最大线程数
2L,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(3), //阻塞队列
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy() //拒绝策略
);
try {
for (int i = 1; i <= 20; i++) {
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + "线程正在办理业务");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
}
8、什么时候考虑使用线程池?
到这儿,线程池的作用、分类、底层代码逻辑、参数与策略的问题基本清晰,那什么时候考虑去使用线程池呢?==> 线程池适合处理耗时任务,可以充分使用目前服务器的硬件资源,加快处理速度。更确切的说是:
- 单个任务处理时间比较短
- 但需要处理的任务的数量大
此时,如果不使用线程池,随意启动许多线程,容易导致系统因创建大量线程而OOM且过渡调度(过渡切换)。比如工作中遇到一个excel数据转换后批量写入库里,就可拆为一批批的小任务去insert。还有帖子说需要限制并发执行的任务数量时也可以用线程池,这儿我先想到的反而是Semaphore信号灯这个JUC辅助类。