我们先从创建开始说起,创建的话主要有以下这几个部分。
第一个,就是我们在创建它的时候,它有构造函数,那构造函数对于线程池而言,参数比较复杂,像之前我们其他的类的构造函数很简单,比如我们定义一个 Map,可能连构造函数里面参数不传都可以。但是在这边的线程池它有非常多的构造函数,并且每一个的含义必须需要我们了解清楚,才能去把它非常良好的给创建出来。
第二个,内容就是我们应该手动创建还是自动创建。其实自动创建是非常方便的,我们也强调自动化,但是对于线程池而言,自动创建有一些弊端,在一定情况下我们手动创建会更好。
第三个,就线程池里面会有一些线程,线程池里面的数量这个数量应该怎么选择?我们假设有 1000 个任务,我们应该用几个线程去执行它是比较合适的呢?
第四个,就是关于停止现成的方法,这和我们的线程停止创建不一样,我们在创建线程和停止线程的时候是上山容易下山难。这边刚好反过来,这边是上山难下山容易。也就是创建实际上要考虑的东西相对比较多,但是停止的话相对会容易一些。
线程池的参数
首先我们就来学习线程池构造函数的参数,在这里我们给出六个参数,这六个参数是我们在创建线程池的时候所需要掌握的。
参数名 | 类型 | 含义 |
---|---|---|
corePoolSize | int | 核心线程数 |
maxPoolSize | int | 最大线程数 |
keepAliveTime | long | 空闲线程的存活时间 |
theadFactory | ThreadFactory | 当线程池需要新的线程的时候,会使用 threadFactory 来生成新的线程 |
workQueue | BlockingQueue | 用于存放任务的队列 |
Handler | RejectedExecutionHandler | 由于线程池无法接受你所提交的任务的拒绝策略 |
第一个大家跟随我的鼠标是call,process,它的含义呢是核心线程数。
在这一页ppt。
里面我不能把它完全的介绍清楚,所以我先跟大家把这六个大体是用来做什么的先过一下,然后呢我们再一个一个地、详细地去解释。
第二个呢是max,process,就是最大的线程数。
第三个呢是keep,like,time,保持存活时间。
第四个是我们的work,▁q是我们的任务存储队列,它通常是一个阻塞队列的类型。
第五个是县城工厂,县城工厂是用来创建新县城的,而最后一个呢是拒绝策略的一个处理器。
比如说我们最后没有办法承担更多的任务了,我们需要拒绝就由这个处理器去执行拒绝的内容。
corePoolSize 与 maxPoolSize
我们首先来学习 corePoolSize 和 maxPoolSize 这两个参数具体含义。如果你之前没有接触过线程池的话,你会觉得它的设计实际上是比较巧妙的。
我们先来看一下这两个参数有什么不同,或者说它们分别是什么含义。corePoolSize 指的是核心线程数,线程池在完成初始化之后,里面是没有线程的,也就线程池初始化时线程数默认为 0。当有新的任务提交后,会创建新线程执行任务,如果不做特殊设置,此后线程数通常不会再小于corePoolSize,因为它们是核心线程,即便未来可能没有可执行的任务也不会被销毁。如果这个时候我们假设 corePoolSize 的值设置为 5,于是你突然一下子放过来 5 个任务,那我就创建 5 个线程,这非常好理解。
但是我们还有一个参数叫 maxPoolSize,这又是什么意思呢?有的时候我们这个任务实际上它的量并不是说均匀的,也不是说固定的。同样一个线程池,它可能在今天执行的任务非常非常多,那在明天可能就很少,这个不均匀的事情是常态。在这种情况下,我们就引入了 maxPoolSize 来帮助我们更好的应对这种情况。在刚才,我们假设已经将 corePoolSize 的值设置为 5,并且已经创建了 5 个线程,那么这 5 个线程通常会一直存活,因为它是我们的核心线程数,所以核心的数量就是保持为 5。所以即便是这一段时间内没有新的任务进来,它也不会把线程数量减小到 5 以下,除非发生异常的情况,这种情况我们先忽略。那么有的时候可能这 5 个线程不足以去处理这么多的任务,突然来了很多任务的话,我们就需要用到更多的线程。
那么这个更多的线程也需要有一个上限,因为这个多也不能是无穷无尽的多,所以这个上限就是我们的 maxPoolSize 。
我们来看一下这张图,如下图所示:
这张图是这么理解的首先从左侧开始看,左侧是最开始的核心线程池的容量,我们的任务进来,它就会创建线程,直到达到了这个 corePoolSize 的值,比如是 5 个,这是它的核心处理线程。
然后这个时候比如说又有一些线程过来了,它会放到它的存储队列中。我们知道在最开始的参数中有一个是存储队列,这个队列里面就是用来存放这些任务的,也就是说他并不想轻易的突破这个 corePoolSize 的值,比如这个 corePoolSize 的值为 5,那么现在这五个线程都在工作,又来了 5 个线程,它就会把这新的五个线程放到队列中,然后等到这 5 个线程谁处理完了再去队列拿获取一个线程来执行。所以这个时候还没有进入到 currentPoolSize ,也没有突破我们的 corePoolSize。
可是如果我们的队列满了,因为我们的队列在一定情况下,你是可以给他设置一个容量的。假设这个容量是 10,那么假设现在我们的核心线程 5 个都在处理,并且队列的容量是 10 ,并且这10 个都已经被塞满了,还想要更多的任务想放进到这个线程池里面去执行。那么这个时候它的扩展性就体现出来了。
这个时候线程池就会继续的在这个数量的基础上去增加新的线程来帮助我们执行,因为它觉得现在我这些线程已经不足以去处理这么多的任务了。当前的线程池的容量就会大于我们的 currentSize,并且逐渐的往外扩展。
那扩展到什么时候呢?最多能扩展到我们的 maxPoolSize。所以这个 current size 它是我们当前线程的数量,它是包含 core pool size 和 current pool size 这两个大方块,而 max pool size 是最大的容量,它是包含我们整个三个方块。
添加线程规则
我们来具体看一下这个添加线程规则。
第一个,是如果我们的线程数它是小于我们核心线程数的,那自然而然,即便是有其他的线程还处于空闲,我现在线程还不够,所以我还会新建一个线程来执行新的任务,只要任务提交进来,我就新建线程。
第二个,但是如果说我们的线程等于或者是大于我们核心核心线程。我们先看等于的情况,等于我们的核心线程了,比如 5,但是少于我们的最大值,最大值是 10。那么这种情况下,新的任务会被放到队列中,而不是说优先再去拓展这个线程,因为还有一个队列,先往往队列里放放,如果不下了。这个时候假设真的放不下了,并且我们的线程现在还是 5,小于我们的 10,就说明极限还没有达到。于是我们就试图去突破我们的 5,创建新的线程去运行任务,那么总有一个极限。
第三个,就是现在队列也满了,并且线程数也达到 10 了,这是极限线程数了,我不能再多了。
这个时候如果还有任务要进来,我就必须去执行拒绝策略了,这就是我们最开始参数中在这里所说的拒绝。
所以到现在我们可以看出已经有几个参数被我们涉及到了,第一个是核心线程数,它是我们线程池中常驻的线程的数量。第二个是最大线程,也就是当任务特别多的时候,那可能会激活这个最大的值。
workQueue 是我们的工作队列,其实这个队列是用来放各种各样的任务的,然后当这个队列也放满了,而 maxPoolSize 最多的线程数也达到了,那么就会去利用 Handler 参数,这个 Handler 参数去执行拒绝策略。
那么我们现在再用图来把这个事情更加的清晰化。如下图所示:
如果一个任务进来,它会首先检查核心线程池满了没有?如果没有满,自然是创建核心线程,如果满了,此时看队列满没满,如果没满,就将任务放到队列中,而队列也满了,就创建更多的线程,这个线程指的是 maxPoolSize。如果连 maxPoolSize 都满了,此时没办法了,只能拒绝了。所以这个流程图相当于是把我们整个的流程给说清楚了。
所以这个时候我们就可以总结一下:
对于我们的线程添加而言,它的规则是需要逐个判断的,它判断的顺序是这样的,首先它判断我们的核心线程数量,也就是我们的 corePoolSize,然后再去判断队列是否满,最后再去判断队列如果满了最大的线程数量是否满?所以它是这样的一个判断顺序,如果全满了就拒绝。
理解之后,我们再举一个例子,这个时候我们做一个比喻,比如我们是去吃烧烤,在秋天,吃烧烤的时候,通常情况下还不是特别冷,也不是特别热,我们会优先选择在店里面吃,店里面假设有 5 张桌子,那我们去吃,这 5 张桌子是始终存在的。它们就比喻我们的 corePoolSize。可是如果说这里面的桌子不够用了,有更多的客人来了,此时我们就要拓展,拓展就是把一些临时的桌子搬到外面去,搬到外面去就是我们的 maxPoolSize。因为外面的容量也是有限的,比如说外面我们还能放 5 张桌子,等到收摊的时候,外面的椅子是会被收回来的,而里面的桌子是不会被处理的,因为里面的始终存在。
我们再举一个线程池的实际例子,这时候的参数都是实打实的。我们假设核心池为 5,最大值为 10,队列的容量是 100。那么这个时候你任务过来了,首先我会创建 5 个核心池的线程数量,再来任务就被添加到这个队列中,那队列的容量为 100,所以我们会一直往里添加,添加直到 100 满了之后,如果还有任务进来,那么我再创建新的线程,使得最多的线程数可以达到 10 个。那么到了 10 个之后,如果再来任务就拒绝。
增减线程的特点
通过以上的流程的学习。我们来总结一下关于这个增减线程的特点。
实际上我们发现,虽然我们了解了一系列的规则和流程,但是对于它的特点,我们再看一下会觉得更加清晰。
第一个,比如我们创建一个 corePoolSize 和 maximumPoolSize,让它们大小相同的这样一个线程池会怎么样?实际上它就变成了一个固定大小的线程池。除了最开始的扩容阶段之外,后面它的现成的数量是不会变化的,即便把在多的任务往队列,即便队列都塞满了,它也不会再往外扩展了。
第二个,线程池它实际上是希望保持更少的线程数量,只有在负载变得很大的时候,才会增加线程数量,这个特点是显而易见的,因为它只有当队列塞满的时候,才去尝试去扩容,而队列只要没满,就不会把线程的容量增大。可以这么理解,线程池它并不希望有太多的线程参与进来。
第三个,就是如果说我们把最大的线程数量设置为一个很高的值,比如说整形的最大值,例如将其设置为 Integer.MAX_VALUE,这个值实在是过于高了,我们都不必要知道它是多少,只要知道永远几乎就塞不满。那么在这种情况下,它就说明那我们的线程池是可以容纳任意数量的并发任务的。为什么,因为队列还是存在的,队列的容量也是有限的。假设队列就是 100 个,但是如果队列满了之后,还想有任务进来,假设 2 千个、3 千个,当这些任务进到这个线程池之后呢,就会根据规则,发现队列已经满了。下一步是什么呢,下一步就是要创建更多线程了,直到达到我最大值。可是这个最大值是达不到的,因为它太大了,所以这个时候就会创建 N 多线程,可能几千个这样的线程来同时去处理这些任务。所以通过这样的一个设定,可以让我们的线程池的容量几乎说是不受限制,也就是它的线程数是任意扩展的。
第四个,就是我们只有队列满的时候才会去创建更多的线程,但是如果你的队列是没有界限的,这是有可能的。我们第三点所讲的是队列有界限,但现成没界限。第四点是队列都没有界限,那队列还没满的情况下,是不可能去创建多于核心线程数的线程的,因为它的规则首先是要求队列满了才会去创建新的线程。这个时候我们使用无界队列,比如 LinkedBlockingQueue,它本身没有一个容量的限制,在这种情况下,来再多的任务都会被放到这个队列中,所以线程数量是不会膨胀的,也不会超过 corePoolSize,即便这个时候你设置这个 maxPoolSize 设置成再大都没有用的,因为不会超过 corePoolSize。
keepAliveTime
keepAliveTime 的含义是保持存活时间,这是一个什么意思呢?我们刚才讲过,在一定情况下会突破核心线程数量,可是你只管突破,不管回收也是不行的。因为我们的核心就是那么多。如果这段时间你突破了,也有义务在后面不忙的时候,把这些冗余的线程给回收回来,这就是我们 keepAliveTime 的作用了。
如果说我们的线程数多于了我们核心的数量,而且剩下的这些线程空闲了,并且空闲的时间超过了我们指定的这个参数的时间,此时它们就会被终止,相当于是被回收了,这是一种机制,这种机制相当于是可以在我们线程数过多冗余的时候减少资源消耗。默认情况下是多于我们的核心线程数,多余部分会被回收,没有超过的这部分由于它们是核心的数量,不应该被回收。除非去修改它的参数,比如将 allowCoreThreadTimeout 设置为 true,这种情况下,它连核心的线程数量也会被回收,通常我们不采用这样的方法。所以我们会给我们的线程,实际上是留有一定的缓冲时间。当发现那些冗余的线程超过了缓冲时间都没有一些任务可以被执行的话,就说明它们已经暂时没有存在的必要了,就会把他们给回收进来。
ThreadFactory
ThreadFactory 实际上是一个线程工厂,它的作用是生产线程以便执行任务。
我们新的线程都是由 ThreadFactory 创建的。如果不去自己指定的,也可以使用默认的。默认是 Executos.defaultThreadFactory()。 默认的创建出来的线程都是在同一个线程组。并且拥有同样的优先级优先级事务,并且都不是守护线程。
如果我们想自己指定我们的线程工厂也可以,我们可以把所对应的线程名、线程组、优先级、是不是守护线程等都可以根据自己的需要去设置。不过通常情况下,我们也没有必要这么做,我们用默认的线程工厂基本上就足够了。通过下面的源码,我们就明白了。
static class DefaultThreadFactory implements ThreadFactory {
private static final AtomicInteger poolNumber = new AtomicInteger(1);
private final ThreadGroup group;
private final AtomicInteger threadNumber = new AtomicInteger(1);
private final String namePrefix;
DefaultThreadFactory() {
SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup() :
Thread.currentThread().getThreadGroup();
namePrefix = "pool-" +
poolNumber.getAndIncrement() +
"-thread-";
}
public Thread newThread(Runnable r) {
Thread t = new Thread(group, r,
namePrefix + threadNumber.getAndIncrement(),
0);
if (t.isDaemon())
t.setDaemon(false);
if (t.getPriority() != Thread.NORM_PRIORITY)
t.setPriority(Thread.NORM_PRIORITY);
return t;
}
}
我们来看一下源码,大家就一清二楚了。DefaultThreadFactory 是实现了 ThreadFactory 接口,这个类很简单。在创建线程的时候,会调用 newThread() 方法。而 newThread() 方法也是用到了 new Thread() 方法,只不过往里面传了很多参数。比如线程名字、是不是守护线程、优先级是什么样,这个就是线程工厂。通常情况下,我们用默认的线程工厂可以满足我们绝大多数情况的需求。
学习到这里,还没有学习到的参数就是 workQueue 和 Handler 。
workQueue
workQueue 指的是工作队列,对于工作队列而言,通常我们有三种队列类型。
第一种,是直接交换的队列,其英文全称为 SynchronousQueue, 如果说我们的任务不会特别多的话,我们只是把任务通过这个队列做一下简单的中转交到线程去处理的话,那么我们就可以使用这个队列。这个队列它本身内部是没有一个容量。比如前面说到的队列有 10 个容量,那么里面可以存 10 个。但是如果我们使用 SynchronousQueue 这种队列的话,里面是存不下任务的。所以如果我们使用了 SynchronousQueue 这种队列,我们就要注意我们的 maxPoolSize 可能要设置的大一点。因为我们没有队列作为缓冲了,可能很容易就会创建新的线程。
第二种,是无界队列,其英文全称为 LinkedBlockingQueue。这个前面有所介绍, LinkedBlockingQueue 不会被塞满。所以如果我们的 corePoolSize 这个线程核心数量都正在忙,那么新的任务就会被放到这个队列中。所以在这种情况下,设置 maxPoolSize 设置为多大都没有用,因为它都会直接放到这个队列中,而队列也放不满。如果我们使用这种队列,确实是可以防止我们流量突增。因为流量增进来,我们处理不完的话,我们就放到队列中。但是也有风险,比如处理的速度跟不上任务提交的速度。那么这个队列里面的任务就会越来越多,可能会造成内存浪费或者是 OOM 异常。
第三种,比较典型的就是有界队列。ArrayBlockingQueue 是一个典型有界队列,是可以设置一个队列大小的。那比如把它设置成 10,在这种情况下,如果用有界队列,那么线程池的 maxPoolSize 参数就有意义了。因为当队列容量满了之后,就需要去创建新的线程。
我们可以根据我们的需要去选择适合我们的队列种类。