前言
相信经常面试的同学,对下面这些问题一定不陌生吧,关于线程池的问题几乎已经成了Java开发者面试的家常菜了
- 线程池的初始化参数都有哪些?
- 线程池的执行流程是怎样的?
- execute方法和submit方法有什么区别
- 线程池有哪些状态
- 工作中都在哪些地方用到了线程池
相信阅读完本篇源码分析环节的同学,都能自己找到答案
这是一篇授人以渔的文章,觉得看烦了前面这些基础知识介绍的可以跳过,直接翻到源码分析环节
线程池介绍
Java 线程池是一种多线程处理机制,它通过管理一组线程来提高线程的使用效率,减少线程创建和销毁的开销。线程池具有下面这些优势
- 降低资源消耗:通过重复利用已创建的线程,避免了频繁创建和销毁线程所带来的巨大开销。
- 提高响应速度:当任务到达时,无需等待线程的创建,而是可以直接从线程池中获取已有的线程来执行任务,从而显著提高了系统的响应速度。
- 提高线程的可管理性:线程池提供了一种集中管理线程的方式,通过线程池,我们可以方便地控制线程的数量、生命周期等,从而提高了线程的可管理性,同时也有助于避免线程过多导致的系统资源耗尽等问题。
使用示例:Hello World
import java.io.IOException;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicLong;
/**
* @author imalasong
* @date 2025-01-18 22:01
*/
public class ExecutorStudy {
// 创建线程池
final static ExecutorService executor = new ThreadPoolExecutor(
5, // 核心线程数
10, // 最大线程数
10, // 存活时间
TimeUnit.SECONDS, // 时间单位,这里是秒
new LinkedBlockingQueue<>(10), // 任务队列
new ThreadFactory() {
AtomicLong incre = new AtomicLong();
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
//定义线程名称
thread.setName("my-thread-"+incre.incrementAndGet());
return thread;
}
},//线程工厂,负责创建线程
new ThreadPoolExecutor.DiscardPolicy() //拒绝策略
);
public static void main(String[] args) throws IOException, ExecutionException, InterruptedException {
// 使用方式一:没有返回值模式,将任务提交到线程池
executor.execute(() -> {
System.out.println(Thread.currentThread().getName() + " is running");
try {
Thread.sleep(1000); // 模拟任务执行时间
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " has finished");
});
// 使用方式二:有返回值模式,将任务提交到线程池
Future<String> submit = executor.submit(new Callable<String>() {
@Override
public String call() throws Exception {
System.out.println(Thread.currentThread().getName() + " is running");
try {
Thread.sleep(1000); // 模拟任务执行时间
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " has finished");
return "imalasong";
}
});
//获取执行结果
System.out.println("执行结果:"+submit.get());
//阻塞程序,防止程序立马结束
System.in.read();
// 关闭线程池
executor.shutdown();
}
}
如何创建一个线程池
new 就完事了
//伪代码,不信你copy到idea中试试
ExecutorService executor = new ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
使用 ThreadPoolExecutor 的构造函数:这是最常用的创建线程池的方式,通过直接调用 ThreadPoolExecutor 的构造函数,我们可以完全控制线程池的各种参数,从而根据实际需求创建出最适合的线程池,如上所示。
在这个构造函数中,各个参数的含义如下:
-
corePoolSize:核心线程数,即线程池中始终保持存活的线程数量。即使这些线程处于空闲状态,它们也不会被销毁,除非设置了 allowCoreThreadTimeOut(true),此时核心线程在空闲时间超过 keepAliveTime 时也会被销毁。
-
maximumPoolSize:最大线程数,即线程池中允许存在的最大线程数量。当任务队列已满且当前运行的线程数小于最大线程数时,线程池会创建新的线程来执行任务;当线程数达到最大线程数且任务队列仍然已满时,线程池会根据拒绝策略来处理新的任务。
-
keepAliveTime:存活时间,即当线程池中线程数量超过核心线程数时,多余的空闲线程在被销毁之前等待新任务的最长时间。如果在这段时间内有新的任务到达,那么这些空闲线程就会被复用,继续执行新的任务;如果在等待时间结束后,该空闲线程仍然没有接到新的任务,那么它就会被销毁,以释放系统资源。
-
unit:时间单位,用于指定 keepAliveTime 的时间单位。
-
workQueue:任务队列,用于存储等待执行的任务。它是一个 BlockingQueue 类型的对象,常见的实现类有 ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、PriorityBlockingQueue 等。不同的任务队列实现类具有不同的特性,例如 ArrayBlockingQueue 是一个有界队列,它的容量在创建时就已经确定,一旦队列满了,新的任务就无法再加入队列;而 LinkedBlockingQueue 是一个无界队列(也可以通过构造函数指定为有界队列),它理论上可以容纳无限多个任务,除非系统资源耗尽。
-
threadFactory:线程工厂,用于创建新的线程。它是一个 ThreadFactory 类型的接口,通过实现这个接口,我们可以自定义线程的创建方式,例如设置线程的名称、优先级、是否为守护线程等。如果不指定线程工厂,线程池会使用默认的线程工厂 Executors.defaultThreadFactory(),这个默认的线程工厂创建的线程具有相同的优先级(Thread.NORM_PRIORITY),并且都不是守护线程。
-
handler:拒绝策略,用于处理当线程池无法接受新任务时(即任务队列已满且线程数达到最大线程数)的情况。它是一个 RejectedExecutionHandler 类型的接口,Java 提供了几种常见的拒绝策略实现类,例如 ThreadPoolExecutor.AbortPolicy(默认拒绝策略)、ThreadPoolExecutor.CallerRunsPolicy、ThreadPoolExecutor.DiscardPolicy、ThreadPoolExecutor.DiscardOldestPolicy 等。此外,我们还可以通过实现 RejectedExecutionHandler 接口来自定义拒绝策略。
源码分析
终于来到源码环节了,相信前面枯燥的知识大家一定都看腻了。
下面我们从源码的角度来分析线程池的核心实现:首先从构造器入手;然后再分析execute()方法,看看线程池是如何处理提交的任务的;另外一个提交任务的方法submit() 留给大家自己去探索了(底层也是通过execute方法实现的)
构造器和核心属性
//线程池的初始状态是RUNNING,初始时0个线程中运行
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int COUNT_MASK = (1 << COUNT_BITS) - 1;
// 线程池的五种状态
private static final int RUNNING = -1 << COUNT_BITS;
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;
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;
}
看上面这段代码可以发现,构造器中主要是进行一些基本参数校验和属性赋值。其中具有一个关键的属性:ctl。ctl一个字段有两层含义,第一层是用来表示运行中的线程数量,第二层是用来表示线程池的状态。
一个字段当两个字段用,怎么实现的呢?ctl是个原子的Integer类型,占用4个字节,也就是32位。用高27位来表示线程数量,低三位表示线程池的状态。由此可以推算出线程池最多可以运行(2^29)-1 个线程,也就是536870911个;线程池的初始化有五个,分别是:RUNNING、SHUTDOWN、STOP、TIDYING、TERMINATED
execute方法:提交任务到线程池
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
//一、如果运行的线程数量小于核心线程数corePoolSize,
// 就会尝试启动一个新线程来执行任务
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
//二、尝试把任务加入到任务队列中
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
// 2.1 重新检查状态,如果有必要,回滚队列
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
// 2.2 重新检查状态,启动一个新的线程
addWorker(null, false);
}
//三、加入任务队列失败,尝试创建一个新线程来执行这个任务,
// 注意这里创建的不是核心线程
else if (!addWorker(command, false))
//3.1 执行拒绝策略
reject(command);
}
请注意注释中一、二、三这三条主线,其他细节可以忽略,通过分析可以发现,核心代码落在了addWorker方法上,下面我们继续接着分析
addWorker方法:创建线程
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (int c = ctl.get();;) {
// 校验线程池和任务队列的状态
if (runStateAtLeast(c, SHUTDOWN)
&& (runStateAtLeast(c, STOP)
|| firstTask != null
|| workQueue.isEmpty()))
return false;
for (;;) {
//校验线程数
if (workerCountOf(c)
>= ((core ? corePoolSize : maximumPoolSize) & COUNT_MASK))
return false;
if (compareAndIncrementWorkerCount(c))
break retry;
c = ctl.get(); // Re-read ctl
if (runStateAtLeast(c, SHUTDOWN))
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
//Worker是线程的包装类,代表着一个线程
w = new Worker(firstTask);
final Thread t = w.thread;
if (t != null) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
int c = ctl.get();
//继续校验线程池的状态
if (isRunning(c) ||
(runStateLessThan(c, STOP) && firstTask == null)) {
if (t.getState() != Thread.State.NEW)
throw new IllegalThreadStateException();
//workers集合存放着所有正在运行的线程
//将线程添加的workers集合中
workers.add(w);
workerAdded = true;
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
}
} finally {
mainLock.unlock();
}
if (workerAdded) {
//启动线程
t.start();
workerStarted = true;
}
}
} finally {
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}
上面这段代码看着长,其实理解起来也不难,核心逻辑是:进行多层校验后,创建一个Worker对象,并且加入到workers集合中,然后调用start方法启动线程对象,下面我们来看看Worker类吧
private final class Worker extends AbstractQueuedSynchronizer
implements Runnable{
//线程对象
final Thread thread;
//第一个要执行的任务
Runnable firstTask;
// 完成的任务数量
volatile long completedTasks;
Worker(Runnable firstTask) {
setState(-1);
this.firstTask = firstTask;
//通过线程工厂创建线程,这里就是创建线程池时指定的线程工厂
//因为Worker也实现了Runnable接口,所以可以把this作为参数传到线程中
this.thread = getThreadFactory().newThread(this);
}
public void run() {
runWorker(this);
}
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
while (task != null || (task = getTask()) != null) {
w.lock();
//校验线程池状态
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
try {
//前置钩子,可以用来扩展
beforeExecute(wt, task);
try {
//执行真正的任务
task.run();
//后置钩子,可以用来扩展
afterExecute(task, null);
} catch (Throwable ex) {
//后置钩子,可以用来扩展
afterExecute(task, ex);
throw ex;
}
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
//销毁线程
processWorkerExit(w, completedAbruptly);
}
}
...省略其他代码
}
还记得上面分析过的addWorker方法吧,在方法的最后调用t.start()启动了线程。因为Worker实现了Runnable接口,所以线程启动就会依次调用Worker的run()方法、runWorker()方法。
runWorker也是比较核心的方法,其核心逻辑就是:获取待执行的任务(优先获取Worker类绑定的初始任务,如果为空的话再通过getTask方法去任务队列里获取任务)、校验线程池状态、执行beforeExecute钩子函数、执行用户提交的任务、执行afterExecute钩子函数、销毁线程。
!!!!!!!
看到这里相信大家一定会有个疑问吧:如果按照这个流程执行下去,所有线程在执行完任务之后都会被销毁掉才对,为什么任务都执行完了,核心线程继续存活呢
看上图,我通过visualvm工具捕获了实时的线程数据,可以发现my-thread-1、my-thread-2这两个线程就是我们线程池里面的创建的核心线程。
下面我来揭晓谜底了,核心线程不会消亡的根本原因是因为getTask方法。
runWorker方法中有个while循环,如果任务队列中一直有任务的话while循环肯定不会停止,会一直执行下去,但如果任务队列是空的呢,获取不到任务会怎样,答案是:会阻塞!!!!别忘了,我们在创建线程池的时候使用的是阻塞队列
new LinkedBlockingQueue<>(10), // 任务队列
下面我们再来简单看看getTask方法吧
private Runnable getTask() {
boolean timedOut = false;
for (;;) {
int c = ctl.get();
//校验线程池的状态
if (runStateAtLeast(c, SHUTDOWN)
&& (runStateAtLeast(c, STOP) || workQueue.isEmpty())) {
decrementWorkerCount();
return null;
}
int wc = workerCountOf(c);
//allowCoreThreadTimeOut这个先不要管,它默认是false
// 再看看后面这段,wc就是真正运行的线程数量,
//corePoolSize是在创建线程池时设置的核心线程数量,
//当运行中的线程数量大于核心线程数量时:timed就是true
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c))
return null;
continue;
}
try {
//timed等于true时,表示运行的线程数量超过核心线程数:这个时候就会
//执行poll方法超时等待,否则就会执行take方法无限期等待。
// keepAliveTime就是在创建时指定的存活时间,到达指定时间之后
//还没有任务的话就会返回null.然后回到上一步的runWorker()方法中,
//就会退出while循环,接着就会销毁线程
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
结语
好了,到了这里,我们的线程池之旅也算结束了。相信看到这里之后,你对线程池已经有了一个初步的认识。以后再遇到面试官问线程池的问题,你一定能按照自己的理解去做答了。
加餐时刻
相信仔细阅读的小伙伴一定发现了一个问题,我在hello world示例中明明设置的核心线程数是5,但visualvm中显示只有两个线程。
这是因为jdk的juc包提供的ThreadPoolExecutor线程池是按需加载的,因为我在示例代码中只提交了两个任务。在提交任务的时候线程池就会去判断当前运行的线程池数量是否大于核心线程数量,如果小于就创建新线程来执行这个任务。
但是,在Tomcat的源码中,Tomcat没有用jdk的线程池,而是自己写了一个线程池ThreadPoolExecutor,他的做法就不一样了,他会在初始化的时候就会自动创建所有核心线程。请看代码
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
...省略部分代码
//初始化所有核心线程
prestartAllCoreThreads();
}
public int prestartAllCoreThreads() {
int n = 0;
while (addWorker(null, true)) {
++n;
}
return n;
}
为什么要初始化所有核心线程??因为Tomcat的一个追求高性能的服务器,在启动的时候就把线程都初始化话,能避免在用户请求的时候再去初始化线程,从而减少一定的性能损失
666666666666666666666666