目录
7. ThreadPoolExecutor 工作原理展示(重点)
1. 线程正常的生命周期
我们知道,线程是有生命周期的,在中间不出现阻塞情况下,线程从一开始的新建状态——>就绪状态——>运行状态——>死亡状态。
2. 为什么要用线程池?
在我们实际开发业务需求时,往往会有很多的用户访问我们的业务,如果来一个用户就创建一个线程,用户结束又销毁线程,如此频繁往复,是非常消耗系统的性能的,因此在实际开发业务时,我们往往会使用线程池。
线程池的好处就是,我们可以在线程池中创建多个线程,当我们有业务需要用到线程时,它会自动到线程池中拿取已经存在线程而不会再去创建新的线程,当线程完成了相应业务后,它会把使用过的线程再还到线程池中而不会销毁它,等待下一次任务的执行,如此一来,就节省了线程的创建与销毁这一动作,提高了程序的运行效率。
3. 线程池的核心原理
(1)我们在初始创建线程池的时候,线程池是空的,当然我们也可以在创建线程池的时候就定义好线程池中有几个线程。
(2)当我们需要用到线程时,线程池会去创建新的线程对象,当任务执行完毕之后,它会把线程归还给线程池,下次再有业务需要用到线程时,不会去创建新的线程,直接复用线程池中已经存在的线程。
(3)如果有多个任务都需要使用线程,但是线程池中的线程都正在工作时,而且也无法在创建多余的线程,那么当钱的任务就会进行等待,什么时候有空余线程,任务就会再次开启。
4. 怎样创建线程池?
Java已经帮我们封装好了一个关于线程池的工具类,名叫 Executors,我们可以根据这个工具类去创建不同类型的线程池。这里我把 Executors 的源码中所有创建线程池的方式列举下来如下图
这些以 new 开头的都是线程池的构造方法,我们可以用它创建线程池。此外,线程池的类型也有很多种,但底层逻辑都差不多。这里我就随便拿两个当例子说一下。
(1)public static ExecutorsService newCachedThreadPool() ,可以创建一个没有线程上限的线程池,从严格意义上来说,它并不算是没有上限,它的上限就是 int 类型的上限,大约是21亿多,我们看它的源码即可得知,这里它的上显示 Integer 的最大值,但我们肯定不会去创建这么多的线程,服务器也承受不住的。另外先记住,该构造方法底层 new 了一个ThreadPoolExecutor,后面会说到。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
(2)public static ExecutorsService newFixedThreadPool(int nThreads),可以创建一个有上限的线程池,上限就是我们传入的参数。
源码如下所示,这里也先记住,该构造方法 new 了一个ThreadPoolExecutor,后面会说到。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
5.线程池的代码实现
测试线程池如何使用,我们先来定义一个线程类
// 实现 Runnable 接口
public class MyRunnable implements Runnable{
// 重写 run 方法,循环5次
@Override
public void run() {
for (int i = 0; i < 5; i++) {
// 获取当前线程名称并打印
System.out.println(Thread.currentThread().getName() + "------" + i);
}
}
}
然后再定义一个线程池,给它多添加几个任务
public static void main(String[] args) {
// 创建一个有上限的线程池,上线设置为3
ExecutorService pool = Executors.newFixedThreadPool(3);
// 给线程池添加任务,多添加几个,超出它的上线
// 任务1
pool.submit(new MyRunnable());
// 任务2
pool.submit(new MyRunnable());
// 任务3
pool.submit(new MyRunnable());
// 任务4
pool.submit(new MyRunnable());
// 任务5
pool.submit(new MyRunnable());
// 任务6
pool.submit(new MyRunnable());
}
运行 main 方法,得到如下结果
可以看到,在控制台中,尽管我们定义了6个线程任务,但线程池中却只有3个线程,分别是pool-1-thread-1,pool-1-thread-2,pool-1-thread-3,这也对应了我们创建线程池时标注的最大线程数量3,而且各位也可以发现,pool-1-thread-2 这个线程打印了两次for循环,其实下面还有线程pool-1-thread-1与线程pool-1-thread-3打印的另外两次for循环,从从我们也可以看出来,虽然我们定义了6个线程任务,但我们定义了一个线程池数量最大为3的线程池,当任务开始时,最先来的三个任务会直接使用线程池中的线程,当第四个第五个第六个任务来到时,它会在线程池的外边排队等待,当前三个任务有人把任务做完将线程归还给线程池后,其他排队等待的线程就可以取出线程池去空余的线程去完成自己的任务了!!!
6. ThreadPoolExecutor 源码分析
刚才我们举得那两个例子,特别用加粗字体说明了这两个线程池底层都是 new 了一个ThreadPoolExecutor,下面我们就来看一下它的源码。
在源码中,有好几个ThreadPoolExecutor 的构造方法,其中有一个最重要的,如下所示,该方法中一共有7个参数,我把它们代表的意义都列举出来。
对于 ThreadPoolExecutor 线程池来讲,我们重点需要理解的有两点,第一点就是这一大堆参数各自代表的含义,第二就是线程池的工作原理
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
int corePoolSize:代表线程池中的核心线程数量;
int maximumPoolSize:代表线程池中最大线程数量;
这里我先解释一下,该线程池中,核心线程永远不会被销毁,除非将线程池销毁,除了核心线程,我们还会有临时线程,就好比是临时工,如果我们线程池的任务过多,核心线程已经忙不过来了,就会有临时线程过来帮忙,当临时线程空闲了一段时间之后没有接到任务,它就又会被销毁。
临时线程 = 最大线程数量 - 核心线程数量
long keepAliveTime:临时线程允许存活的时间数值,如60,100,2000等任意long类型的数都可以;
TimeUnit unit:这个参数与上一个是紧密联系的,代表的是单位,可以为秒,分钟,小时;
如果上面 keepAliveTime 参数数值为60,TimeUnit 单位为Second,则表示表示空余线程可以空闲存活60秒;如果TimeUnit 单位为Minutes,则表示表示空余线程可以空闲存活60分钟,否则就会被销毁。
BlockingQueue<Runnable> workQueue:代表阻塞队列,如果我们的核心线程都在工作时,结果还有任务过来,就会暂时进入到这个阻塞队列中去,可以定义该队列的大小;
ThreadFactory threadFactory:表示我们定义的线程是从哪来的,即创建线程的方式;
RejectedExecutionHandler handler:表示当等待队列满了,临时线程也在工作状态的时候该如何处理,我们可以抛出异常,也可以拒绝服务等等一系列措施。
7. ThreadPoolExecutor 工作原理展示(重点)
刚才我大致说明了 ThreadPoolExecutor 的几个参数代表的意思,下面我来举个例子让大家更好的去理解它
如下所示
现在我定义一个线程池,核心线程数量定义为3,临时线程数量也定义为3,等待队列长度也定义为3,当我们来了10个任务需要线程池去完成时,我来一步步说明,前提是第十个任务来了第一个任务还没有完成,这里我简单描述,各位应该能看懂
(1)第一步,当来了前三个任务,而且是首次接受到任务时,线程池会去创建三个核心线程去处理任务123,如下图所示
(2)第二步,当来了任务456时,因为核心线程都处在工作状态,此时线程456 就会进入到等待队列(也叫阻塞队列)中去排队等待,而不是由临时线程去处理它们;如下图所示
(3)第三步,当来了任务789时,因为此时已经没有空余线程了,等待队列又是满的,此时线程池就会再去创建三个临时线程去处理这任务789;如下图所示
(4)第四步:当提交了第十个任务时,大家可以看到,此时核心线程在工作中,临时线程也在工作中,等待队列也满了,那么此时线程池就会触发拒绝策略;
(5)新来的任务就会被线程池拒绝服务,Java中一共有四种拒绝策略供我们选择,我们可以自行设置,如果不设置默认就是直接将新来的任务丢弃并抛出异常。
此外有一点需要注意,这四种任务拒绝策略都是静态内部类,我们直接通过类名.方法名调用即可。 一般情况下我们也不会去更改拒绝策略,所以其他三种各位同学可以作为了解哦!