文章目录
前言
最近面试过程中经常被问到多线程方面的问题,尤其是线程池,因为这也是工作中常用的创建线程的方式,本篇博客在于记录自己对于线程池的执行原理及如何创建线程池去执行任务的一个理解,同时方便后期回顾复习
一、为什么要使用线程池创建线程?
我总结了两方面原因:
- 1.每次创建线程都需要占用一定的内存空间,如果无限地去创建线程可能会浪费内存空间,严重的话会导致内存溢出
- 2.我们的CPU资源是有限的,同一时刻一个CPU只能处理一个线程,如果有大量的请求进来了,创建了大量的线程,很多线程是没有CPU执行权的,那么这些线程都得等待,会造成大量的线程之间的切换,同时也会导致性能变慢
二、线程池的核心参数(重点)
以ThreadPoolExecutor为例,我们可以看到它的构造函数,有7个核心参数,接下来,我会对它们做一个简单的解释
1.核心线程数
- 就是说任务进来之后,线程池首先创建核心线程来执行任务,比如有两个任务进来,如果定义了核心线程数为2,则会创建2个核心线程来执行任务,任务再进来的话,就会进入阻塞队列
2.最大线程数
- 最大线程数 = 核心线程数 + 救急线程数,如果核心线程数为2,最大线程数为3,那么救急线程数就是1
3.救急线程的存活时间
- 意思是说,如果在救急线程存活时间内没有任务的话,救急线程就会被释放,但是核心线程是不会被释放的
4.救急线程的时间单位
- 就是一个TimeUnit枚举类,它定义了几种时间单位:NANOSECONDS(纳秒)、MICROSECONDS(微秒)、MILLISECONDS(毫秒)、SECONDS(秒)、MINUTES(分)、HOURS(时)、DAYS(天)
5.任务队列
- 任务阻塞队列,当新任务进来的时候,如果此时没有空闲核心线程去执行,那么任务就会先进入到任务阻塞队列中进行等待
6.线程工厂
- 一般用来定义线程的名字
7.任务拒绝策略
- 阻塞队列已经满了,也没有空闲的救急线程,此时如果新进入一个任务的话,那么会按照任务拒绝策略对新任务进行处理,有四种任务拒绝策略:
- 1.AbortPolicy: 直接抛出异常
- 2.CallerRunsPolicy: 用调用者所在的线程来执行任务,一般就是主线程
- 3.DiscardOldestPolicy:丢弃阻塞队列中最靠前的任务,并执行当前任务
- 4.DiscardPolicy:直接丢弃任务
三、线程池的执行原理
通俗的说,当新任务被提交的时候,判断核心线程数是否已满,如果没有则创建核心线程执行任务,如果核心线程数已满,则将新任务添加到阻塞队列中,若阻塞队列也满了,则判断当前线程数是否小于最大线程数,如果小于则创建救急线程来执行任务,如果当前线程数等于最大线程数,表示没有空闲线程来执行任务了,只能走拒绝策略,新任务就会根据拒绝策略进行处理。
四、一个小案例
详细代码:
```java
public class TestThreadPoolExecutor {
static class MyTask implements Runnable {
private final String name;
private final long duration;
public MyTask(String name) {
this(name, 0);
}
public MyTask(String name, long duration) {
this.name = name;
this.duration = duration;
}
@Override
public void run() {
try {
System.out.println("MyThread...running..." + this);
Thread.sleep(duration);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public String toString() {
return "MyTask(" + name + ")";
}
}
public static void main(String[] args) throws InterruptedException {
AtomicInteger c = new AtomicInteger(1);
ArrayBlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(2);
LinkedBlockingQueue linkedBlockingQueue = new LinkedBlockingQueue();
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
2,
3,
0,
TimeUnit.MILLISECONDS,
queue,
r -> new Thread(r, "myThread" + c.getAndIncrement()),
new ThreadPoolExecutor.AbortPolicy());
showState(queue, threadPool);
threadPool.submit(new MyTask("1", 3600000));
showState(queue, threadPool);
threadPool.submit(new MyTask("2", 3600000));
showState(queue, threadPool);
// threadPool.submit(new MyTask("3"));
// showState(queue, threadPool);
// threadPool.submit(new MyTask("4"));
// showState(queue, threadPool);
// threadPool.submit(new MyTask("5",3600000));
// showState(queue, threadPool);
// threadPool.submit(new MyTask("6"));
// showState(queue, threadPool);
}
private static void showState(ArrayBlockingQueue<Runnable> queue, ThreadPoolExecutor threadPool) {
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
List<Object> tasks = new ArrayList<>();
for (Runnable runnable : queue) {
try {
Field callable = FutureTask.class.getDeclaredField("callable");
callable.setAccessible(true);
Object adapter = callable.get(runnable);
Class<?> clazz = Class.forName("java.util.concurrent.Executors$RunnableAdapter");
Field task = clazz.getDeclaredField("task");
task.setAccessible(true);
Object o = task.get(adapter);
tasks.add(o);
} catch (Exception e) {
e.printStackTrace();
}
}
System.out.println("pool size:" + threadPool.getPoolSize() + ", " + tasks);
}
}
在面代码中,我们定义了一个静态内部类,用来创建任务,在主方法中定义了线程池,以及它的一些核心参数,比如定义的核心线程数是2,最大线程数是3,那么救急线程数就是1了,救急线程的存活时间是0ms,任务阻塞队列是ArrayBlockingQueue,线程工厂定义了线程的名称,以及任务拒绝策略定义了AbortPolicy,当新任务来的时候,如果当前线程数等于最大线程,同时任务队列也满了的话,就会触发拒绝策略。
当我们只提交两个任务的时候,会创建两个核心线程去执行任务,此时阻塞队列是空的,运行结果如下:
当我们提交三个任务的时候,第三个任务会进入到阻塞队列:
当我们提交四个任务,第三个和第四个任务会进入阻塞队列
如果我们提交5个任务,由于阻塞队列我们一开始定义的容量是2,那么第五个任务就会创建救急线程去执行
当我们提交第六个任务的时候,由于阻塞队列满了,而且当前线程数也已经达到了最大线程数,此时就会触发任务拒绝策略,就会抛出异常:
总结
这篇博客简单说了一下线程池的核心参数和执行原理,以及用一个小案例说明它的一个工作流程,后面会继续更新线程池方面的知识点,比如有哪些阻塞队列,它们内部的原理是什么。