欢迎关注微信公众号:Coding我不配
获取更多干货,一起每天进步一点点
1 为何要线程池化
线程池是一种池化技术,目的是避免线程频繁的创建和销毁带来的性能消耗。它是把已创建的线程放入“池”中,当有任务来临时就可以重用已有的线程,无需等待创建的过程,这样就可以有效提高程序的响应速度。
《Java 并发编程艺术》一书中提到使用线程池的好处:
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的资源浪费。
- 提高响应速度。当任务到达时,不需要等到线程创建就能立即执行。
- 方便管理线程。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以对线程进行统一的分配,优化及监控。
常见的池化技术:Tomcat 线程池、数据库连接池、HTTP 连接池等
2 如何创建线程池
Java 中创建线程池有以下两种方式:
- 通过 ThreadPoolExecutor 类创建(推荐)
- 通过 Executors 类创建
其实这两种方式在本质上是一种方式,都是通过 ThreadPoolExecutor 类的方式创建,因为 Exexutors 类调用了 ThreadPoolExecutor 类的方法。
2.1 ThreadPoolExecutor 方式
查看 JDK1.8 的源码,ThreadPoolExecutor 类源码有四个构造函数
ThreadPoolExecutor 类在 java.util.concurrent 包下,部分源码:
//这个包太迷人,面试官太喜欢问里面的知识点
package java.util.concurrent;
public class ThreadPoolExecutor extends AbstractExecutorService {
//七个参数的构造函数
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.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
//...其他
}
1. 参数 corePoolSize
- 表示线程池的常驻核心线程数。如果设置为 0,则表示在没有任何任务时,销毁线程池;如果大于 0,即使没有任务时也会保证线程池的线程数量等于此值;
- 应该结合实际业务设置此值的大小。若 corePoolSize 的值较小,则会出现频繁创建和销毁线程情况;若值较大,则会浪费系统资源。
2. 参数 maximumPoolSize
- 表示线程池最大可以创建的线程数。官方规定此值必须大于 0,也必须大于等于 corePoolSize 的值;
- 此值只有在任务比较多,且不能存放在任务队列时,才会用到。
3. 参数 keepAliveTime
- 表示线程的存活时间。当线程池空闲时并且超过了此时间,多余的线程就会销毁,直到线程池中的线程数等于 corePoolSize 的值为止;
- 若 maximumPoolSize 的值 等于 corePoolSize 的值,则线程池在空闲的时候不会销毁任何线程。
4. 参数 unit
- 表示存活时间的单位,配合 keepAliveTime 参数共同使用。
5. 参数 workQueue
- 表示线程池执行的任务队列;
- 当线程池的所有线程都在处理任务时,若来了新任务则会缓存到此任务队列中,然后等待执行。
6. 参数 threadFactory
- 表示线程的创建工厂,一般使用默认的线程创建工厂的方法 Executors.defaultThreadFactory()来创建线程
7. 参数 RejectedExecutionHandler
- 表示指定线程池的拒绝策略,属于一种限流保护的机制;
- 当线程池的任务已经在缓存队列 workQueue 中存满了之后,并且不能创建新的线程来执行此任务时,就会用到此拒绝策略
- 四种拒绝策略
(1) AbortPolicy: 丢弃任务并抛出异常。
(2) DiscardPolicy:丢弃任务但不抛出异常。
(3) DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务
(4) CallerRunsPolicy:由调用线程处理该任务
2.2 Executors 方式
查看 JDK1.8 的源码,Executors 类源码有 12 个创建线程的静态方法
Executors 类在 java.util.concurrent 包下,部分源码:
package java.util.concurrent;
public class Executors {
//参数一般使用默认的,对编程不可见
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
//...其他
}
从源码看来,Exexutors 类本质上调用了 ThreadPoolExecutor 类的构造方法,实现线程的创建。
3 线程池如何工作
3.1 创建线程池
先来看下具体如何创建线程池,撸一把代码
package com.coding.wbp;
import java.util.Date;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class MyThreadPool {
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 4, 10, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(20), new ThreadPoolExecutor.CallerRunsPolicy());
for (int i = 1; i < 5; i++) {
// 创建WorkerThread对象
Runnable worker = new MyThreadPool().new MyRunnable("" + i);
// 执⾏任务
executor.execute(worker);
}
}
class MyRunnable implements Runnable {
private String name;
public MyRunnable(String name) {
this.name = name;
}
private void process() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public void run() {
System.out.println("Thread-" + name + " -> start... " + new Date());
process();
System.out.println("Thread-" + name + " -> end... " + new Date());
}
}
}
运行结果:
Thread-1 -> start... Tue Jul 07 22:08:14 CST 2020
Thread-2 -> start... Tue Jul 07 22:08:14 CST 2020
Thread-1 -> end... Tue Jul 07 22:08:17 CST 2020
Thread-3 -> start... Tue Jul 07 22:08:17 CST 2020
Thread-2 -> end... Tue Jul 07 22:08:17 CST 2020
Thread-4 -> start... Tue Jul 07 22:08:17 CST 2020
Thread-3 -> end... Tue Jul 07 22:08:20 CST 2020
Thread-4 -> end... Tue Jul 07 22:08:20 CST 2020
结果分析:
模拟了 4 个任务,配置的核心线程数 corePoolSize 为 2,等待任务容量为 20,所以每次只可能存在 2 个任务在并行执行,剩下的 2 个任务存放在任务队列中等待执行。当前 2 个任务执行完,后 2 个任务开始执行。
3.2 工作流程
线程池开始工作,通过执行方法 execute()开始,此方法源码:
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
//Proceed in 3 steps:
int c = ctl.get();
//Step1 当前工作的线程数小于核心线程数corePoolSize值
if (workerCountOf(c) < corePoolSize) {
// 则创建新的线程执行此任务
if (addWorker(command, true))
return;
c = ctl.get();
}
//Step2 检查线程池是否处于运行状态,如果是则把任务添加到队列
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
// 如果是非运行状态,则将刚加入队列的任务移除
if (! isRunning(recheck) && remove(command))
reject(command);
// 如果线程池的线程数为 0 时(corePoolSize 为 0 )
else if (workerCountOf(recheck) == 0)
// 新建线程执行任务
addWorker(null, false);
}
//Step3 核心线程都在忙且队列都已爆满,尝试新启动一个线程执行失败
else if (!addWorker(command, false))
// 执行拒绝策略
reject(command);
}
线程池任务执行的主要工作流程
- 线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。
- 当执行 execute() 方法添加一个任务时,线程池会判断:
(a) 若正在运行的线程数量小于 corePoolSize 值,则立刻创建线程运行此任务;
(b) 若正在运行的线程数量大于或等于 corePoolSize 值,则将此任务放入队列;
© 若此时队列满了,而且正在运行的线程数量小于 maximumPoolSize 值,则创建非核心线程立刻运行这个任务;
(d) 若队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize 值,那么线程池会执行设置的拒绝策略。 - 当一个线程完成任务时,它会从队列中取下一个任务来执行。
- 当一个线程空闲时,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于 corePoolSize 值,那么这个线程会被销毁。
线程池任务执行的主要流程图如下:
4 常见面试题
4.1 execute 与 submit 方法区别
执行线程池任务方法有 execute() 和 submit()方法,看个Demo
public static void main(String[] args) throws ExecutionException, InterruptedException {
ThreadPoolExecutor executor =
new ThreadPoolExecutor(5, 8, 10L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(15),
Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
executor.execute(new Runnable() {
@Override
public void run() {
System.out.println("execute method.");
}
});
Future<String> future = executor.submit(new Callable<String>() {
@Override
public String call() throws Exception {
System.out.println("submit method.");
return "hello";
}
});
System.out.println("value: " + future.get());
}
运行结果
execute method.
submit method.
value: hello
两者主要区别如下:
- submit() 方法可以配合 Future 来接收线程执行的返回值,而 execute() 不能接收返回值;
- execute() 方法属于 Executor 接口的方法,而 submit() 方法则是属于 ExecutorService 接口的方法。
欢迎关注微信公众号:Coding我不配
获取更多干货,一起每天进步一点点