一 线程池是什么
1.线程池主要用于管理和优化线程的使用,线程池通过维护一组线程,并在需要执行任务时从线程池中分配线程,从而避免频繁创建/销毁线程的开销。
2.为解决开销问题---》
1)线程池
2)协程(纤程) “虚拟线程‘本质上就是协程
3.引入以上两种为什么可以提高效率?
·直接创建/销毁线程--》需要用户态+内核态配合完成
·通过线程池--》只需用户态,无需内核态配合
直接调用api创建/销毁线程,这个过程需要内核来完成,而内核完成工作很多时候是不可控的。而使用线程池提前把线程都创建好,放到用户态代码写的数据结构中,之后使用的时候直接从池子里取,用完再放回,这个过程完全是用户态代码,不需要与内核进行交互。
协程本质上也是纯用户态操作,规避了内核操作。不是在内核中提前建好线程,而是用一个内核的线程来表示多个协程。(纯用户态,进行协程之间的调度)
二 标准库中的线程池 ThreadPoolExecutor
1.标准库的线程池中把线程分为两种:
1)corePoolSize 核心线程(正式员工)
2)maximumPoolSize 核心态+非核心态(实习生/临时工)
2.动态扩展
线程池被创建出来的时候,里面包含核心线程数这么多的线程,此时线程池里也就是包含4个线程。
线程池会提供应该submit方法,向里面添加任务,每个任务都是一个Runnable对象,若当前添加的任务较少,4个线程足以处理则只有4个线程在工作了。
若添加的任务很多,4个线程处理不过来(很多任务在排队等待执行),此时线程池就会自动创建出新的线程来支撑更多的任务。 创建出的线程总数不能超过最大线程数。
一段时间后,若任务没那么多了,部分线程会被释放(回收),回收只是把非核心线程回收掉,此时线程池中的线程数目不会小于核心线程数。
-----》保证了任务多的时候的效率,任务少的时候的开销!
3.实际开发中的线程数目如何设置?
不仅和电脑配置有关--》决定有多少个cpu核心,还与程序的实际特点有关:
1)cpu密集型:代码完成的逻辑都要通过cpu来完成 (代码逻辑都是在进行算术运算/条件判断/循环判断/函数调用等,这种代码跑起来利马占满一个cpu核心)
这种类型的代码,线程数目不能超过CPU逻辑核心数
2)IO密集型:代码大部分时间都在等待IO(等待IO不消耗cpu,不参与调度) (标准输入输出/sleep/操作硬盘/操作网络等)
这种类型的程序每个线程消耗的cpu只有一点点,要考虑的是其他方面:网络程序、网卡宽带的瓶颈等。
3)以上两个模型都是理想化的,实际开发中的程序,逻辑中既包含cpu操作,也包含IO操作,属于cpu密集和IO密集,介于两者之间,因此,要根据实验的方式来找到合适的值:
对程序进行性能测试,测试过程中设置不同的线程池的数值,多次运行程序,代码中记录执行时间,同时记录下系统消耗的资源(cpu占用/内存占用等),修改不同的值,最终根据实际程序的响应速度和系统开销等 再来综合权衡。----》”控制变量法“
4.keepAliveTime 非核心线程允许空闲的最大时间,非核心线程在线程池不忙时回收,但不是立即回收
5.BlockingQueue 线程池的任务队列
线程池内部中需要一个队列这样的数据结构,把要执行的任务保存起来。后续线程池内部的工作线程就会消费这个队列从而完成具体的任务执行。
线程池提供的submit方法,让其他线程把任务提交给线程池,这些线程就会submit到这个队列中,这个队列中存的元素就是Runnable对象,要执行的逻辑就是run方法里的内容。
6.ThreadFactory 工厂模式---》标准库中提供的用来创建线程的工厂类
这个线程工厂,主要是为了批量的给要创建的线程设置一些属性,在工厂方法中把线程的属性提前初始化好
核心思路:不再使用构造方法创建对象,给构造方法包装一层
7.RejectExecutionHandler 拒绝策略
是一个枚举类型,在任务量超出负荷时采用何种拒绝策略
◦ AbortPolicy():超过负荷,直接抛出异常
◦ CallerRunsPolicy():调⽤者负责处理多出来的任务
◦ DiscardOldestPolicy():丢弃队列中最⽼的任务
◦ DiscardPolicy():丢弃新来的任务.
三 线程池的实现
1.核心操作为submit,将任务加入线程池中
2.使用Worker类描述一个工作线程,使用Runnable描述一个任务
3.使用BlockingQueue 线程池的任务队列 组织所有的任务
4.每个worker线程要做的事情:不停的从BlockingQueue中取任务并执⾏
5.指定⼀下线程池中的最⼤线程数maxWorkerCount;当当前线程数超过这个最⼤值时,就不再新增
线程了