线程池
- 上一章节我们介绍的四种创建线程的方式算是热身运动了。线程池才是我们的重点介绍对象。
- 这个是JDK对线程池的介绍。
- 但是你会问为什么上面我们创建线程池的方式是通过
Executors.newCachedThreadPool()
;
-
关于Exectors内部提供了很多快捷创建线程的方式。这些方法内部都是依赖
ThreadPoolExecutor
。所以线程池的学习就是ThreadPoolExecutor
。 -
线程池
ThreadPoolExecutor
正常情况下最好用线程工厂来创建线程。他的作用是用来处理每一次提交过来的任务;ThreadPoolExecutor
可以解决两个问题- 在很大并发量的情况下线程池不仅可以提供稳定的处理还可以减少线程之间的调度开销。
- 并且线程池提供了对线程和资源的回收及管理。
-
另外在内部
ThreadPoolExecutor
提供了很多参数及可扩展的地方。同时他也内置了很多工厂执行器方法供我们快速使用,比如说Executors.newCacheThreadPool()
:无限制处理任务。 还有Executors.newFixedThreadPool()
:固定线程数量;这些内置的线程工厂基本上能满足我们日常的需求。如果内置的不满足我们还可以针对内部的属性进行个性化设置
- 通过跟踪源码我们不难发现,内置的线程池构建都是基于上面提到的7个参数进行设置的。下面我画了一张图来解释这7个参数的作用。
- 上面这张图可以解释
corePoolSize
、maxiumPoolSize
、keepAliveTime
、TimeUnit
、workQueue
这五个参数。关于threadFactory
、handler
是形容过程中的两个参数。 - 关于
ThreadPoolExecutor
我们还得知道他虽然是线程池但是也并不是一开始就初始化好线程的。而是根据任务实际需求中不断的构建符合自身的线程池。那么构建线程依赖谁呢?上面也提到了官方推荐使用线程工厂。就是我们这里的ThreadFactory
类。 - 比如
Executors.newFixedThreadPool
是设置了固定的线程数量。那么当任务超过线程数和队列长度总和时,该怎么办?如果真的发生那种情况我们只能拒绝提供线程给任务使用。那么该如何拒绝这里就涉及到我们的RejectExecutionHandler
- 点进源码我们可以看到默认的队列好像是
LinkedBlockingQueue
; 这个队列是链表结构的怎么会有长度呢? 的确是但是Executors
还给我们提供了很多扩展性。如果我们自定义的话我们能够发现还有其他的
核心数与总线程数
- 这里对应
corePoolSize
和maxiumPoolSize
。
final ThreadPoolExecutor executorService = (ThreadPoolExecutor) Executors.newFixedThreadPool(10);
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println("我是线程1做的事情");
}
});
- 我们已
newFixedThreadPool
来分析下。首先它需要一个整数型参数。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
- 而实际上内部是构建一个最大线程数量为10,且10个线程都是核心线程(公司核心员工);这10个线程是不会有过期时间一说的。过期时间针对非核心线程存活时间(公司外包员工)。
- 当我们执行
execute
方法时。点进去看看我们发现
- 首先会判断当前任务数是否超过核心线程数,如果没有超过则会添加值核心线程队列中。注意这里并没有去获取是否有空闲线程。而是只要满足小于核心线程数,进来的任务都会优先分配线程。
- 但是当任务数处于(corePoolSize,maxiumPoolSize】之间时,线程池并没有立马创建非核心线程,这点我们从源码中可以求证。
这段代码时上面if 判断小于核心线程之后的if , 也就是如果任务数大于核心线程数。优先执行该if 分支。意思就是会将核心线程来不及处理的放在队列中,等待核心线程缓过来执行。像我们上面所说如果这个时候我们用的时有边界的队列的话,那么队列总有放满的时候。这个时候执行来到我们第三个if分支
- 这里还是先将任务添加到非核心队列中。false表示非核心。如果能添加进去说明还没有溢出非核心数。如果溢出了正好if添加就是false . 就会执行了拒绝策略。
- 下面时executor执行源码
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
else if (!addWorker(command, false))
reject(command);
}
思考
- 基于上面我们对核心数和总数的讲述,我们来看看下面这段代码是否能够正确执行吧。
public static void main(String[] args) throws InterruptedException {
ThreadPoolExecutor executorService = new ThreadPoolExecutor(10,20,0,TimeUnit.SECONDS,new ArrayBlockingQueue<>(10));
for (int i = 0; i < 100; i++) {
int finalI = i;
executorService.execute(new Runnable() {
@SneakyThrows
@Override
public void run() {
System.out.println(finalI);
TimeUnit.SECONDS.sleep(1000);
}
});
}
}
- 很不幸,我们的执行报错了。而且出发了
ThreadPoolExecutor
中的拒绝策略。而且分析日志我们能够发现成功执行的有20个任务。分别是【0,9】+【20,29】这20个任务。 - 拒绝我们很容易理解。因为我们设置了最大20个线程数加上长度为10的队列。所以该线程城同时最多只能支持30个任务的并发。另外因为我们每一个任务执行时间至少在1000秒以上,所以程序执行到第31个的时候其他都没有释放线程。没有空闲的线程给第31个任务所以直接拒绝了。
- 那么为什么是是【0,9】+【20,29】呢?上面源码分析我们也提到了,进来的任务优先分配核心线程数,然后是缓存到队列中。当队列满了之后才会分配非核心数。当第31个来临直接出发拒绝策略,所以不管是核心线程还是非核心线程都没有时间处理队列中的10个线程。所以打印是跳着的。