参考链接:通过实例来透彻分析线程池的源码---ThreadPoolExecutor_小猪快跑22的博客-优快云博客
一,
线程池的概念:
·
1)什么是线程池:
线程池就是创建一些线程,将它们的集合称之为线程池。
使用线程池可以很好地提高系统的性能,线程池在系统启动时,即创建一些空闲的线程(核心线程),程序将一个任务交给线程池,线程池就会启动一个线程来执行这个任务。执行结束以后,该(核心)线程并不会死亡,而是再次返回线程池中,成为空闲状态,等待执行下一个任务。
·
2)线程池的工作机制
1)系统是将任务传给整个线程池,线程池在拿到任务后,就将任务交给池里边的一个空闲的线程去执行任务。
·
3)使用线程池的好处
1)降低资源消耗。
通过重复利用已创建的线程,来避免了线程的创建和销毁所造成的资源消耗。
2)提高响应速度。
当任务到达时,任务可以不需要等到线程创建,就能立即执行。
3)提高线程的可管理性。
线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以对线程们进行统一分配、调优和监控,提高了对线程的可管理性。
·
4)四种线程池:
Executors:在JUC(java.util.concurrent)包下面的这个Executors执行器类中,提供了一系列的静态工厂方法,用于创建各种线程池;
其中常用的几个方法,如下:
public static ExecutorService newFixedThreadPool() //固定数量线程池
public static ExecutorService newSingleThreadExecutor() //单线程线程池
public static ExecutorService newCachedThreadPool() //可缓存线程池
public static ScheduledExecutorService newScheduledThreadPool() //可定时线程池
1、newFixedThreadPool:固定数量的线程池,
该方法返回一个 可重用的、固定线程数量的线程池;
2、newSingleThreadExecutor:单线程的线程池,
它只会用唯一的线程来执行任务,保证所有任务按照指定顺序(FIFO(先进先出), LIFO(后进先出), 优先级)执行;
3、newCachedThreadPool: 可缓存线程池,
该线程池可以根据实际情况调整池子中的线程数量,当执行当前任务时,上一个任务已经完成,会直接复用执行上一个任务的线程,而不用每次新建线程,如果上一个线程没有结束才会新建线程,可缓存型池子通常用于执行一些生存期较短的任务;
4、newScheduledThreadPool: 可定时线程池,
该线程池可以设定线程的执行时间,可以用来去执行一些定时及周期性的任务。
·
5)如何创建线程池:
JDK提供了Executor接口(隶属于java.util.concurrent包),可以让我们有效的管理和控制我们的线程。
看源码:
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
根据源码可以看出,在实际创建线程池的时候,实质调用的是ThreadPoolExecutor这个类,即,线程池执行器ThreadPoolExecutor;
即,在Executors类的内部,创建线程池的时候,实际新建的是一个ThreadPoolExecutor线程池执行器对象。
总结:说白了,线程池是通过new 一个ThreadPoolExecutor()来创建的;
·
二,线程池是如何实现线程复用的?(源码分析)
1)线程有三种实现方式,
- 一种是继承Thread,重写run方法;
- 一种是实现runable接口,然后重写run方法;
- 一种是callable接口;(异步调用,有返回值)
启动方式如下:
//第一种方式,继承Thread,重写run方法后的启动
new MyThread().start();
//第二种方式,将 `实现的runable接口的task作为参数` 传入Thread构造方法
new Thread(new Runnable() {
@Override
public void run(){
System.out.println("do something");
}
}).start();
我们知道,一般一个线程在执行完任务后就结束了,怎么再让他执行下一个任务呢?
要想实现线程复用,必须从Runnable接口的run()方法上入手;
2)看源码:
// 内部类 Worker
private final class Worker extends AbstractQueuedSynchronizer implements Runnable{
/** 各种代码... */
Runnable firstTask;
Worker(Runnable firstTask) {
setState(-1); // inhibit interrupts until runWorker
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}
public void run() {
runWorker(this);
}
}
//addWorker()方法,启动线程;
private boolean addWorker(Runnable firstTask, boolean core) {
w = new Worker(firstTask);
final Thread t = w.thread;
//...代码
// 很明显了,从这里启动线程的;
t.start();
...//代码
}
// getTask()方法,从 任务队列(workQueue)中,获取任务;
private Runnable getTask() {
// 各种代码...
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
//...
} // ...
}
// 核心部分:runWorker()方法:
final void runWorker(Worker w) {
// 各种代码...
Runnable task = w.firstTask;
w.firstTask = null;
// 各种代码...
try {
// 重点:看这个大while循环:实现的线程复用;
while (task != null || (task = getTask()) != null) {
// 各种代码...
}
}
}
3)源码分析:在线程池中,线程是如何创建的:
我们从第5节知道,线程池是通过 new 一个 ThreadPoolExecutor (线程池执行器)这个类 来创建的;
而真正的线程池里边的线程,是在ThreadPoolExecutor类里边的 一个名叫 Worker的内部类 里边创建的;
private final class Worker extends AbstractQueuedSynchronizer implements Runnable{
/** 各种代码... */
Runnable firstTask;
Worker(Runnable firstTask) {
setState(-1); // inhibit interrupts until runWorker
this.firstTask = firstTask;
//创建线程:
this.thread = getThreadFactory().newThread(this);
}
public void run() {
runWorker(this);
}
}
也就是说,只要 new Worker(),就会创建一个线程,因为Worker的构造方法里边有这么一句:this.thread = getThreadFactory().newThread(this);
。
并且创建线程的时候,会将这个内部类本身this 传到参数列表里边,去当task。
·
4)源码分析:在线程池中,线程是如何启动的:
在源码的addWorker()方法中,可以看到:t.start();
;
很明显了,从addWorker()方法这里启动线程的;
//addWorker()方法,启动线程;
private boolean addWorker(Runnable firstTask, boolean core) {
w = new Worker(firstTask);
final Thread t = w.thread;
//...代码
// 很明显了,从这里启动线程的;
t.start();
...//代码
}
·
5)源码分析:在线程池中,线程是如何运行的:
我们刚才知道,在源码的addWorker()方法中进行启动线程,
我们可以看到:w = new Worker(firstTask);
也就是说,在线程启动的时候,我们将内部类worker对象传入进去了,而内部类Worker是实现了runable接口、并重写了run()方法的;
这也意味着:jvm会执行Worker里的run方法,使线程进入运行状态;
·
6)源码分析:在线程池中,线程是如何实现复用的:
6.1)核心在runWorker()方法里边:
用Work内部类中的run()方法里边的runWorker()方法里边的while大循环,实现了线程的复用;
private final class Worker extends AbstractQueuedSynchronizer implements Runnable{
/** 各种代码... */
Runnable firstTask;
Worker(Runnable firstTask) {
setState(-1); // inhibit interrupts until runWorker
this.firstTask = firstTask;
//创建线程:
this.thread = getThreadFactory().newThread(this);
}
public void run() {
runWorker(this);
}
}
在Work内部类中的run()方法里面,有一个runWorker(this)方法;(这个this指的是内部类Work对象本身)
这个run()方法,线程启动的时候jvm会执行它,
我们再来看这个run()方法里面的runWorker()方法:
final void runWorker(Worker w) { // 各种代码...
// 核心:这个大while循环:
while (task != null || (task = getTask()) != null) {
// 各种代码...
}
// 各种代码...
}
线程复用的核心,就在这个runWorker()方法里边:
从源码中可以看出,这个runWorker()方法里面,有一个大大的while循环:
即,通过getTask()方法,可以一直获取到新的任务 ,那么这个while循环就永远在进行;
从而runWorker()方法不会停止,runWorker()方法外边的run()方法也就不会停止,继而线程会一直处于运行状态,去执行新的任务,从而达到了线程复用的目的;
·
7)剖析getTask()方法:
这个getTask()方法,很显然是用来 从任务队列(workQueue)中,获取任务;
private Runnable getTask() {
// 各种代码...
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
//...
} // ...
}
根据getTask()这个方法的源码可以看出,getTask()方法是从任务队列中(workQueue),获取任务;
这个getTask()方法里面有个三元表达式,
① 当条件为真时,从任务队列(workQueue)里面,取得要执行的任务;
② 当条件为假时,即任务队列没有任务了,则结束runWorker()方法里边的while大循环,从而,这个线程结束。
·
8) 线程复用流程总结:
①,创建一个线程池,new ThreadPoolExecutor()
;
②,新建一个线程,在线程池中,new Worker()
新建一个Worker内部类时,会新建一个线程getThreadFactory().newThread(this)
,并且会把这个Worker内部类本身传进去当作任务去执行,
③,这个worker内部类的run()方法里的runWorker()方法,写了一个while大循环,
④,当任务队列有任务时,while大循环一直进行,从而runWorker()、run()方法也就一直进行,继而该线程一直执行新的任务,达到了线程复用的目的;
⑤,当任务队列没有任务时,则结束这个while循环,继而,这个线程也就结束。
·
·
三,Java线程池的7个参数:
我们以饭店服务员为例子:
饭店只有两个正式工的服务员(核心线程),用来维持日常的经营(处理任务);
当饭店推出打折活动的时候,突然间会有大量的客人来吃饭(任务暴增),当客人的数量超过了两个(核心线程数)之后,剩余的客人就会在门口的凳子上(任务队列)排队等待,
当凳子(任务队列)也坐满了之后,于是老板就不得不找了四个临时工服务员(临时线程)来帮忙。
当打折活动过去之后,客流量高峰期过了,此时原本的两个正式工的服务员(核心线程)就已经够用了,然后等到四个临时工服务员(临时线程)的工作时间也到点了之后,老板就会撵走这四个临时工服务员(临时线程)。
1> 核心线程数(corePoolSize):
核心线程的数量,也称为线程池的基本大小;
核心线程的生命周期与该线程池一样,即永远不会被回收掉,除非该线程池都没了。
2> 最大线程数(maximumPoolSize):
-
与上边的核心线程对应的是——非核心线程(临时线程);
-
最大线程数(maximumPoolSize)指的是:线程池中允许的最大线程数,
-
核心线程数 + 非核心线程数 = 最大线程数;
3> 任务队列(workQueue):
- 当任务的数量多于corePoolSize核心线程数的时候,会将多出来的这些任务放入到任务队列中。
-
如果任务队列也满了,那么会新建一个非核心线程去执行任务;
-
如果任务队列满了,且线程池中的实际线程数量也等于最大线程数maximumPoolSize了,还是有新来的任务,那么会执行饱和策略,默认的策略是直接丢掉要加入的任务,抛出一个拒绝执行异常。
4+5> 空闲线程的存活时间 (keepAliveTime)与存活时间单位:
- 这针对的是非核心线程,即临时线程;
- 非核心线程空闲下来之后,且超过了这个存活时间也还没有任务执行,则结束和回收该线程;
- 注意:核心线程是不会被回收的,除非你任务设置要回收核心线程。
6> 线程工厂
创建线程的工厂,用来创建线程的。这个线程工厂可以自定义(在实际开发中方便业务区分和异常追溯)。
7> 拒绝策略(饱和策略)
如果任务队列满了,且线程池中的实际线程数量也等于最大线程数maximumPoolSize了,还是有新来的任务,那么会执行饱和策略,默认的策略是直接丢掉要加入的任务,抛出一个拒绝执行异常。
·
补充:关于任务队列:
队列的选择,在实际开发中这是一个难点和重点。
1)队列的长度、核心线程数、最大线程数怎么设置:
这个得根据你实际的业务去进行评估。比如说你的并发量最低的时候是多少,可以参考着去制定核心线程数;你的并发量的最高峰是多少,去参考着设定最大线程数和任务队列的长度;
2)队列有哪些种类、怎么选型:
队列你可以自己定义,定义一个数组、一个链表,都可以;
或者,也可以用现成的,Java里边的容器种类很多,比如说concurrentHashMap,CopyOnWriteList,SynchronizeList,等等。
还有就是:只要是和xxxQuque这个相关的,基本上都可以去作为队列使用,比如说ArrayBlockingQueue,LinkedBlockingQueue,PriorityQueue,DelayQueue,SynchronusQueue等;
3)有界队列与无界队列:
有界队列、无界队列的意思就是任务队列的长度,无界队列就是这个队列可以无限延长;
注意:在实际开发中,不要使用无界队列!即使是有界队列,长度也不要过大!不然容易引起OOM。
比如说:FixedThreadPool和SingleThreadPool默认允许的任务队列的长度是Integer.MAX_VALUE,CachedThreadPool默认允许的最大线程数为Integer.MAX_VALUE,这两种情况都可能导致OOM。
当任务数量特别多的时候:
允许的任务队列的长度是Integer.MAX_VALUE,可能会堆积大量的任务,把内存全部给你耗光,从而导致OOM;
允许的最大线程数为Integer.MAX_VALUE,可能会创建大量的非核心线程,把内存耗尽,从而导致OOM;
所以在实际开发中,不要使用无界队列!即使是有界队列,也要设定合理的长度!
·
补充:关于拒接策略:
当任务特别多的时候,任务队列满了,最大线程数满了,此时再有新任务来了,就要去执行拒绝策略了。
默认的拒绝策略是:直接丢掉要加入的任务,抛出一个拒绝执行异常。
很明显在实际开发中很多时候这个默认的策略是不可取的,因为有的数据是不能随意丢掉的。那么我们该如何自定义一个策略来解决问题呢?
1)使用持久化的方式,接住多余的任务,比如MQ;
我们可以把新来的这部分任务给持久化起来,比如说我们在这个服务的外边搭建一个MQ的集群,当新任务来了实在处理不了,可以先扔到MQ里边,(MQ自己有持久化机制),等到线程池来得及处理了,再从MQ里边拿出来,去进行处理。
2)其他方式:
如果任务量太大了,MQ都灌满了怎么办,动态的扩缩容MQ集群;实在实在不行,还可以从网关的位置就开始进行限流等方式;
·