1、线程池
- 线程的创建和关闭依然需要花费时间,如果每个任务都开一个线程,那么可能就会出现
创建+销毁
的时间>
任务真实
的时间。 - 其次,线程也是占用内存的,大量线程处理不当可能出现
OOM
异常。 - 因此大量的线程只会
拖垮
应用系统。需要对线程进行控制和管理。
1.1、什么是线程池?
- 简单的说:当想要使用线程的时候,直接去线程池中
取
,当然,线程池中线程的个数也是有限的。当一个线程完成
工作的时候,将这个线程返回
线程池中。 - 特点:
线程复用、控制最大并发数、管理线程
1.1.1、为什幺要使用线程池?
- 多核处理的好处是
省略上下文的切换开销
。 好处
:
- 降低资源消耗。
- 提高响应速度。(任务到达,
不需要
等到线程创建
就可以立刻
执行)。 - 提高线程的可管理性。
1.2、JDK对线程池的支持
- JDK提供了Executor框架去控制多线程。本质就是一个线程池。
- 架构图如下图所示:
Executors
类是一个线程池工厂角色,通过它可以拥有一个特定功能的线程池
。而ThreadPollExecutor
类实现了Executor
接口,所以,任何Runnable对象可一个被ThreadPoolExecutor线程池调度。Executors
类提供的几个常见
创建线程池的方法:
Executors.newFixedThreadPool(int i)
Executors.newSingleThreadExecutor()
Executors.newCacheThreadPool()
Executors.newScheduledThreadPool(int corePoolSize)
1.3、核心线程池的内部实现
- 查看上述方法的源码:
- newFixedThreadPool(int i)
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
- newSingleThreadExecutor()
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
- newCachedThreadPool()
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
- newScheduledThreadPool(int corePoolSize)
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
- 所以,发现最后都指向了
ThreadPoolExecutor
类
1.3.1、线程池的七大核心参数
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
corePoolSize
:核心线程数,线程池中的常驻
核心线程数。maximumPoolSize
: 线程池能够容纳同时执行的最大线程数,此值必须大于等于1。keepAliveTime
:多余的空闲线程存活时间。unit
:keepAliveTime的单位
。workQueue
:任务队列,被提交的但未被执行的任务。就是一个阻塞队列。threadFactory
:表示生成线程池中工作线程的线程工厂,用于创建线程池,一般用默认即可
。handler
:拒绝策略,表示当队列满了并且工作线程大于线程池的最大线程数(maximumPoolSize3)时,如何来拒绝
请求执行的Runnable的策略。
1.3.2、拒绝策略
- 所有的拒绝策略都实现了
RejectedExecutionHandler
接口
AbortPolicy
:默认,直接抛出RejectedExcutionException异常,阻止系统正常运行。- DiscardPolicy:直接丢弃任务,不予任何处理也不抛出异常,如果运行任务丢失,这是一种好方案。
- CallerRunsPolicy:该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者(从哪儿来的回哪儿去)。
- DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务(喜新厌旧)。
1.4、工作原理
- 银行周末只开两个窗口,此时正好来了两个人,那么正好处理完毕。
- 或者两个人办理业务时间很长,而后续又有很多人进入银行,新来的客户进入等待区。
- 如果此时还有办理业务的人,人多到超过了等待区能容纳的数量,此时就开放所有的窗口,此时新增的窗口接待非等待区的。
- 如果人继续增多,导致接待不过来了,执行拒绝策略了,不能出现踩踏事件。
- 当人流减少,或者没有人再进入银行,那么就新开的窗口慢慢的在规定时间内就会关闭,最后回到常规开放的两个窗口。
- 在创建线程池之后,等待提交过来的任务请求。
- 当调用
execute()
方法添加一个请求任务的时候,线程池做判断:
2.1. 如果正在运行的线程数量小于corePoolSize
,那么就马上创建线程运行这个任务。
2.2. 如果正在运行的线程数量大于等于corePoolSize
,那么将这个请求任务放入阻塞队列
中。
2.3. 如果队满并且运行的线程数量小于maximumPoolSize
,那么创建非核心线程立刻运行这个任务。
2.4. 如果队满并且运行线程数量大于等于maximumPoolSize
,那么线程池启动拒绝策略
。 - 当一个线程完成任务,会从队列中取下一个任务来执行。
- 当一个线程等待时间超过
keepAliveTime
,线程池会进行判断
4.1. 如果当前运行的线程数大于corePoolSize
,那么这个线程停掉。 - 线程池的所有任务完成之后,最后会收缩到
corePoolSize
大小。
1.5、ThreadFactory
- 在线程池的七大核心参数还有一个
ThreadFactory
没谈到,这个参数的作用是什么?线程池中的线程是从何而来?其实线程池中的所有线程都来源于ThreadFacotry
。 - ThreadFactory就是一个接口,只有一个用来创建线程的方法,当线程池需要
新建
线程的时候,就会调用这方法。Thread newThread(Runnable r);
1.6、手写一个线程池
public class ThreadPoolDemo {
public static void main(String[] args) {
final Integer corePoolSize = 2;
final Integer maximumPoolSize = 5;
final Long keepAliveTime = 1L;
ExecutorService executorService = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
try {
for (int i = 0; i < 10; i++) {
final int tempInt = i;
executorService.execute(() -> {
System.out.println(Thread.currentThread().getName() + "\t 给用户:" + tempInt + " 办理业务");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
executorService.shutdown();
}
}
}
- 注意点:关闭线程池,如果当前有线程正在执行,shutdown()
不会
立刻暴力关闭终止所有任务,会等待所有任务执行完毕之后再关闭,但并不会等待所有线程执行完成之后再返回。简单说就是:调用shutdown()方法的时候,这是一个信号,说我这个线程池不再接受
其他行任务
1.7、线程池的参数选择。
- corePoolSize和maximumPoolSize是根据生产环境决定的。
- 根据具体业务配置,分为CPU密集型和IO密集型。
2.1. CPU密集型
:该任务需要大量运算
,且没有
阻塞,CPU火力全开。只有在真正的多核CPU才能得到加速。任务配置尽可能少的线程数量,通常为:CPU核数 + 1个线程数
。
2.2. IO密集型:该任务需要大量IO操作
(大量阻塞)。IO密集型任务中使用多线程可以大大的加速
程序的运行,即使在单核CPU上,这种加速主要就是利用
了被浪费掉的阻塞时间
。IO密集的时候,大部分线程都被阻塞,所以需要多配置线程数,公式:CPU核数 / (1 - 阻塞系数)
阻塞系数在0.8 ~ 0.9左右。