线程池相关
1.JUC是什么
JUC是在并发编程中使用的工具类,在Java API中的 java.util.concurrent包
2.进程/线程
进程/线程是什么?
进程:
是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如CPU时间,内存空间,文件,文件,输入输出设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存中。
简而言之,电脑上开启的一个软件比如QQ等都是一个进程。
线程:
线程,与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
3.为什么需要使用线程池?
线程池和数据库连接池类似,可以统一管理和维护线程,减少非必要的开销。
举例JDBC请求:
如果我们每执行一个次select请求,都要去创建一个JDBC请求的话,那访问的多了,开销直接爆炸了。
所以,我们会在配置文件中配置JDBC连接池,最小连接数 10 、20 …,最大连接数 多少多少这样。
所以核心思想是复用。
频繁的开启线程和停止线程,线程需要 重复的被 CPU 从就绪——运行——停止 各个状态之间的调度切换,发生CPU上下文切换,效率就会低的多。
如果我们提前创建好了 一些固定数量的线程,一直保持运行的状态,实现复用,从而减少就绪到运行状态的切换,就提升了效率。如下图,线程的生命周期。
4.一般项目中哪里会用到线程池?
首先,项目中,我们是禁止 自己手动显示new 线程的。例如如下代码:
new Thread(new Runnable() {
@Override
public void run() {
for(int i=1;i<=4000;i++){
ticket.sale();
}
}
},"A").start();
在实际项目中,类似此等代码,假如外部调用这个 方法特别大量,会导致线程很多,这是不对的,比如使用线程池。
实际项目中,使用线程池的地方很多,比如异步的发短信,发邮件,扣积分等等。
5.线程池的好处?
线程池的核心概念的就是 复用线程,限制线程的数量,保证运行状态线程的数量。
(1)降低资源消耗:这个从这方面理解,重复利用我们固定创建该数量的线程,避免了我们重复一直创造和销毁线程带来的资源消耗。
(2)提高响应速度:任务到达时,无需等待线程创建即可立即执行(线程是我们预先创建好的)。
(3)提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性,使用线程池可以进行统一化的分配、调优和监控。(和(1)解释差不多,其实为了方便管理,线程创建的多,给我们排查问题都带来困难)
(4)提供更多更强大的功能:线程池具备可扩展性,允许开发人员向其中增加更多的功能。比如延时定时线程池 ScheduledThreadPoolExecutor(扩展),就允许任务延期执行或定期执行。
6.线程池创建方式
Executors类中提供了创建几种不同线程池的方式,这是JDK给我们封装的,
- Executors.newCachedThreadPool() :可缓存线程池
- Executors.newFixedThreadPool() :可定长度
- Executors.newScheduledThreadPool() :可定时
- Executors.new、SingleThreadPool() :单例
但实际中不会用到以上方式进行创建,因为底层都是基于ThreadPoolExecutor构造函数封装的,而这个构造函数大部分传递的参数都是无界限的,可能会导致线程池溢出,例如:源代码中一段
7.线程池如何实现线程复用的?
要实现复用,就是要 创建一个线程,不会停止或者销毁,而是一直重复利用。
开局一张图,逻辑如下图:
(1)首先提前创建好 4个线程,一直在运行状态(通过死循环实现的)
(2)提交的线程任务缓存到一个并发队列集合中,交给正在运行的线程去执行。
(3)正在运行的线程就从队列中获取该任务去执行
代码举例:
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;
/**
* 自己写一个线程池
*/
public class MyExecutor {
private List<WorkThread> workThreads;
//创建一个任务队列
private BlockingQueue<Runnable> runnables;
/**
*
* @param maxThreadCount 最大线程数
*/
public MyExecutor(int maxThreadCount,int queueSize) {
//1.提前创建好固定数量的线程
workThreads = new ArrayList<>(maxThreadCount);
runnables = new LinkedBlockingDeque<>(queueSize);
for(int i=0;i<maxThreadCount;i++){
new WorkThread().start();
}
}
//运行的线程类
class WorkThread extends Thread{
@Override
public void run() {
while (true){
//它在一直运行着 执行 任务队列里的任务
Runnable runnable = runnables.poll();
if(runnable !=null){
runnable.run();
}
}
}
}
//装载任务的方法:
public boolean execute(Runnable command){
return runnables.offer(command);
}
public static void main(String[] args) {
MyExecutor myExecutor = new MyExecutor(2,10);
for (int i=0;i<10;i++){
int finalI = i;
myExecutor.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"-"+ finalI);
}
});
}
}
}
运行结果:两个线程复用执行10个任务
8.ThreadPoolExecutor核心参数
在上面的题目中,我们看到JDK给我们提供的创建线程池的方式是通过ThreadPoolExecutor。
通过源码,我们截取一个,可以看到它的参数:
总结一下:
corePoolSize: 核心线程数,一直正在保持运行的线程
maximumPoolSize: 最大线程数,线程池允许创建的最大线程数
keepAliveTime: 超出corePoolSize后创建的线程的存活时间
unit: keepAliveTime的时间单位
workQueue: 任务队列,用于保持待执行的任务
threadFactory: 线程池内部创建线程所用的工厂
handler : 任务无法执行时的处理器
9.线程池创建的线程会一直运行吗?
不会
假如核心线程数corePoolSize设置为2,最大线程数设置为4,可以通过设置 超出corePoolSize线程数后创建的线程的存活时间为60S,则会在60S没有任务执行的时候,这多出的线程就会销毁。
10.ThreadPoolExecutor线程池执行逻辑原理
(1)当线程数小于核心线程数,创建线程
(2)当线程数大于等于核心线程数,且任务队列未满时,将任务放到队列中。
(3)当线程数大于等于核心线程数,且任务队列已满时,线程数小于最大线程数,创建线程
(4)走拒绝策略
总结:线程池能执行的任务总数 = 最大线程数+任务队列大小
11.拒绝策略类型有哪些?
通过以上的线程池执行原理,如果队列满了,且任务总数 大于 最大线程数的时候,会走拒绝策略。
这个拒绝策略是可以自己扩展的,比如存到redis里之类的,后期再补偿。
通常的拒绝策略类型有以下几种:
(1) AbortPolicy : 丢弃,抛异常
(2) CallerRunPolicy : 执行任务
(3) DiscardPolicy : 忽视,啥也不干
(4) DiscardOldestPolicy : 踢出最后一个执行的任务
(5)自定义处理器,通过实现 RejectExecutionHandler接口