震惊,线程池参数讲的这么明白!!!!
一:为什么存在线程池???
线程池存在的最大作用就是提高效率.
并发编程,使用多进程就可以了,
但线程比进程更轻量,在频繁创建销毁的时候,更有优势.
但随着时代的发展,对于"频繁"有了新的定义了.
以前,一个服务器1s处理1k个请求,就认为是时频繁了;
现在,一个服务器1s要处理几万个请求.
现有的要求下,频繁创建销毁线程,开销也变的越来越明显了,
为了使开销变得更小,就引入了线程池.
二:为什么引入线程池之后,能够提升效率??
直接创建/销毁线程,是需要用户态+内核态配合完成的工作,
内核通过调用系统API,创建/销毁线程,但内核完成的工作是不可控的;
而通过线程池,创建/销毁线程,只通过用户态即可,不需要内核态的配合,
如果使用线程池,在创建线程池的时候,就会创建一些线程(提前把线程都创建好),放到用户态代码中写的数据结构中去,后面用的时候,随时从池子里取线程,用完了,就放回池子里面去,这个过程,完全是用户态代码,不需要和内核态进行交互,省去了一些频繁创建/销毁线程的过程.
三:标准库线程池:构造方法参数解析
3.1:int corePoolSize, int maximumPoolSize
/**
* corePoolSize:核心线程数
* maximumPoolSize:最大线程数
* 标准库中线程是这样设定的:把线程分为两类:
* 1:核心线程: corePoolSize
* 2:非核心线程 ;
* maximumPoolSize:核心线程数+非核心线程数
*/
3.1.1:动态扩展
一个线程池,刚被创建的时候,里面就包含核心线程数那么多的线程,比如corePoolSize=4;
线程池会提供一个submit方法,让其他线程把任务提交给他,每个任务就是一个Runnable对象,
如果当前添加的任务比较少,4个线程就足以能够处理,就只有4个线程在工作了,如果添加的任务比较多,4个线程处理不过来了(有很多线程在排队等待执行),这个过程,线程池就会自动创建出新的线程,来支撑更多的任务.(但创建出来的线程总数,不能超过最大线程数).
过了一段时间之后,任务没有那么多了,线程空闲了,部分线程就会被释放掉(回收了),回收只是把非核心线程回收掉,至少保证线程池线程数目不少于核心线程数.
但实际开发中,核心线程数和最大线程数应该设置多少??
不仅仅和电脑配置有关(决定了有多少个CPU核心),更重要的是和程序的实际特点有关.
程序分为两大类:
1:CPU密集型程序
2:IO密集型程序
3.1.2:CPU密集型程序
Thread t1=new Thread(()->{
int count=0;
while(true){
count++;
}
});
形如上述代码:代码逻辑都是在进行算术运算,条件判定,循环判定,函数调用…这种程序一跑,就能立即吃满一个CPU核心.
此时,线程数目,是不应该超过CPU逻辑核心的.
3.1.3:IO密集型程序:
代码大部分时间在等待IO,(等待IO是不消耗CPU,不参与调度的).
代表:标准输入输出,sleep(),操作键盘,操作网络…
由于代码大部分都是在等待,那么瓶颈就不在CPU上,每个线程只消耗CPU一点点,更多考虑的是其他方面(比如网络程序,要考虑网卡带宽的瓶颈(比如网卡是1Gbps,一个线程读写速率是100Mbps,最多搞10个线程))
3.1.4:确定线程数的方法
时间开发中,很多时候,一个程序,逻辑中既包含CPU操作,又包含IO操作,是介于CPU密集和IO密集两者之间的.
我们应该根据实验的方式,找到一个合适的值.
对程序进行性能测试,测试过程中,设定不同的线程池的数值,把程序多运行几次,代码中记录执行时间,同时记录系统消耗的资源(CPU占用/内存占用…),最终根据实际程序的响应速度和系统开销,综合权衡,找到一个你觉得最合适的值.
3.2: long keepAliveTime, TimeUnit unit
/**
* keepAliveTime,非核心线程,允许空闲的最大时间
* 非核心线程,要在线程不忙的时候,回收掉,但不是立即回收,
* 只有超过这个最大空闲时间,线程还未工作,才会被回收掉
* TimeUnit unit,单位:s,分钟,小时,毫秒........
*/
3.3:BlockingQueue workQueue
线程池的任务队列.
根据实际场景,指定一个合适的capacity的队列进去,也可以使用带有优先级的阻塞队列.
线程池会提供submit方法,让其他线程把任务提交给线程池,线程池内部需要有一个队列这样的数据结构,把要执行的任务保存起来(每个队列存的就是Runnable对象,要执行的逻辑就是run()方法里的内容),后续线程池内部的工作线程,就会消费这个队列,从而来完成具体的任务.
3.4:ThreadFactory threadFactory
3.4.1:工厂模式:
工厂模式,也是一种设计模式.
主要为了解决构造方法太坑了的问题.
/**
* 创建一个Point类
*/
class Point{
public Point(double x,double y){
//笛卡尔坐标系下的构造方法
}
public Point(double r,double a){
//极坐标系
}
}
创建一个点,在数学中,可以在极坐标系下,也可以在笛卡尔坐标系下.
但此时,上述两个方法不能构成重载,无法通过构造方法,来表示不同的构造点的方式.
工厂模式,核心思路:不再使用构造方法来创建对象.而是给构造方法包装一层.
class Point{
//把构造方法包装起来的方法,就称为"工厂方法"
//这样写代码的套路,就叫做"工厂模式"
public static Point makePointByXY(double x,double y){
Point p=new Point();
p.setX(x);
p.setY(y);
return p;
}
public static Point makePointByRa(double r,double a){
Point p=new Point();
p.setR(r);
p.setA(a);
return p;
}
}
我们还可以用单独的类,来进行提供工厂方法,这个单独的类,就叫作工厂类.
class Point{
}
class PointBuilder{
//把构造方法包装一层的方法,就称为"工厂方法"
public static Point makePointByXY(double x,double y){
Point p=new Point();
p.setX(x);
p.setY(y);
return p;
}
public static Point makePointByRa(double r,double a){
Point p=new Point();
p.setR(r);
p.setA(a);
return p;
}
}
ThreadFactory threadFactory 就是标准库提供的,用来创建线程的工厂类.
这个线程工厂,主要是为了批量的给要创建的线程设置一些属性,线程工厂,在它的工厂方法中,就把线程的属性提前初始化好了.
平时一般不会用ThreadFactory,主要是搭配线程池来使用.
/**
* RejectedExecutionHandler handler;拒绝策略
* 其实是一个枚举类型,规定了采用哪种拒绝策略
* 如果当前队列满了,仍要继续添加任务,直接阻塞不合适
*ThreadPoolExecutor.AbortPolicy ,
* 直接抛出异常,让程序员快速的知道,任务处理不过来了,直接罢工了(之前的任务也不执行了)
* ThreadPoolExecutor.CallerRunsPolicy ,线程池不管它,哪个线程添加的任务,哪个线程来执行
* ThreadPoolExecutor.DiscardOldestPolicy ,丢弃掉最老的任务,让新的任务去队列中排队
* ThreadPoolExecutor.DiscardPolicy 丢弃掉最新的任务(添加任务的线程执不执行不知道),线程池仍然完成原来的任务
*
*/