*作用
线程池的工作主要是控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,超出数量的线程排队等候,等其它线程执行完毕,再从队列中取出任务来执行。
*优势
-
线程复用
降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
-
控制最大并发数
提高响应速度。当任务到达时,任务可以不需要等待线程创建就能立即执行。
-
管理线程
提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池则可以进行统一的分配,调优和监控。
*架构
Java中的线程池是通过Executor框架实现的
左侧
Executor
:相当于集合类中的Collection接口
ExecutorService
:继承了Executor接口的子接口,比Executor更强大,通常我们都用它
AbstractExecutorService
:实现了ExecutorService接口的抽象类
ThreadPoolExecutor
:重点,底层就是这个类实现的线程池
右侧
ScheduledExecutorService
:带时间轮询片调度的,也就是一个定时的线程执行接口
工具类
Executors
:使用这个类就可以拿到线程池
*常用的几种线程池
public class MyThreadPoolDemo {
public static void main(String[] args) {
// 固定数,core = max,
// 创建一个线程池,一池有N个固定的线程,有固定线程数的线程,执行长期任务性能好
ExecutorService es = Executors.newFixedThreadPool(5);
// 单一数,core/max = 1
// 一个线程池中只有一个线程,只能一个任务一个任务的执行
ExecutorService es1 = Executors.newSingleThreadExecutor();
// 可变数,core = 0,带缓存的线程池
// 不需要指定容量,线程池根据需要创建新线程,可以自动将新的任务分配给已创建的空闲线程,可扩容,遇强则强
// 可以创建非常多的线程,这些线程只要一空闲就都会被销毁
ExecutorService es2 = Executors.newCachedThreadPool();
// 支持定时任务的线程池
ExecutorService es3 = Executors.newScheduledThreadPool();
testThreadPool(es2);
}
public static void testThreadPool(ExecutorService es) {
try {
for (int i = 1; i <= 10; i++) {
es.execute(() -> {
System.out.println(Thread.currentThread().getName() + "\t正在执行任务");
});
// 模仿网络延迟,会导致线程分配的不同
TimeUnit.MILLISECONDS.sleep(1);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
es.shutdown();
}
}
}
*七大参数
-
corePoolSize
:常驻核心线程数 -
maximumPoolSize
:最大线程数,必须>=1控制资源并发
-
keepAliveTime
:多余线程的存活时间如果当前池中线程数量大于核心线程数,并且线程的空闲时间大于指定的存活时间,那就会将线程池的线程数量降到常驻核心线程数
-
unit
:keepAliveTime
的时间单位 -
workQueue
:阻塞队列,被提交但未被执行的任务如果任务有很多,就会将目前多的任务放在队列里面
只要有线程空闲,就回去队列里面取出新的任务,继续执行 -
threadFactory
:线程池中生成线程的线程工厂,一般用默认 -
handler
:拒绝策略当队列满了,并且工作线程>=线程池的最大线程数(
maximumPoolSize
)时如何来拒绝请求执行的runnable
的策略
*ThreadPoolExecutor底层原理
3个创建线程池的方法实际上调用的都是ThreadPoolExecutor
的重载
*线程池底层工作原理
-
在创建了线程池之后,开始等待请求
-
当调用execute()方法添加一个请求任务时,线程池会做出如下判断
如果正在运行的线程数量小于
corePoolSize
,那么马上创建线程运行这个任务如果正在运行的线程数量大于或等于
corePoolSize
,那么将这个任务放入队列如果这个时候队列满了且正在运行的线程数量还小于
maximumPoolSize
,那么需要创建非核心线程立刻运行这个任务如果队列满了且正在运行的线程数量大于或等于
maximumPoolSize
,那么线程池会启动饱和拒绝策略来执行 -
当一个线程完成任务时,它会从队列中取下一个任务来执行
-
当一个线程无事可做超过一定的时间(
keepAliveTime
)时,线程会判断如果当前运行的线程数大于
corePoolSize
,那么这个线程就会被停掉所以线程池的所有任务完成后,它最终会收缩到corePoolSize的大小
*实际生产中,使用三种方式的哪一种创建线程
哪种也不用,只用自定义的
*自定义线程池
ExecutorService es = new ThreadPoolExecutor(
2,
Runtime.getRuntime().availableProcessors() + 1, // 得到CPU内核数
3L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
maximum该怎么设置
首先调用Runtime.getRuntime().availableProcessors()
得知服务器是几核的
根据业务类型来决定
CPU密集是指该任务需要大量的运算,没有阻塞,CPU一直全速运行
- CPU密集任务只有在真正的多核CPU上才可能得到加速(通过多线程)
- 所以CPU密集型任务一般配置尽可能少的线程数量
- 公式:CPU核数+1个线程的线程池
IO密集型任务,即该任务需要大量的IO,会产生大量的阻塞
- 在单线程上运行IO密集型的任务,会导致大量的CPU运算能力浪费在等待上
- 公式一:由于IO密集型任务线程并不是一直在执行任务,所以应配置尽可能多的线程,例如:CPU核数*2
- 公式二:CPU核数/(1-阻塞系数(0.8-0.9之间))
*拒绝策略
AbortPolicy(默认):直接抛出RejectedExecutionException异常阻止系统正常运行
CallerRunsPolicy:“调用者运行”一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量。
DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务。
DiscardPolicy:该策略默默地丢弃无法处理的任务,不予任何处理也不抛出异常。如果允许任务丢失,这是最好的一种策略。