本文分为两部分。
第一部分是基础章节。可以帮助我们了解线程池的概念,用法,以及他们之间的的关系和实际应用。
第二部分是实现机制篇。通过源码解析,更深刻理解线程池的工作原理,以及各个概念的准确含义。
原本是一篇文章,因为篇幅太长,所以拆成的两篇,所以建议都看。
创建线程,启动/销毁线程,是一件很消耗性能的事情:
创建线程:和创建普通对象相比,还增加了分配栈空间。
启动/销毁线程:涉及到线程的调度导致线程上下文切换。
所以:看似简单的创建并使用一个线程,其实很消耗资源。
如果只执行一个任务就创建一个线程。
这就相当于:某一天,你为了出趟远门而去买了辆车。
多浪费呀,租车不香吗。
租车公司就相当于一个线程池,负责车辆(线程)的创建启动和销毁,还有公司的日常运维。
继续思考:
上面的场景是临时用车的场景。
假如不是临时用车,而是打算开10年甚至更长时间,你还会租车吗。此时肯定是去买车了对吧。
所以:
有些文章说,使用线程池还有一个考量因素:任务量比较大。我是不太认同的。
任务量大不大,那是一个整体视角。租车公司管你租几辆车。一辆也是租,十辆车也是租。只要这个地区的整体租车需求足够大就行。
站在用户角度,只需要考虑自己的单个任务时长问题。时长太短,自己单独创建线程不值得,就可以考虑使用线程池。
平时我们说:CPU四核心八线程。指的就是最多同时处理八个线程(实际上并不一定,多出来的四条线程处理能力,其实是CPU虚拟出来的。但同时能处理四个线程是肯定的)。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TestThreadPool {
public static void main(String[] args) {
TestThreadPool t = new TestThreadPool();
t.m1();
}
public void m1() {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 3; i++) {
executorService.execute(() -> {
System.out.println(Thread.currentThread().getName() + "----------start----");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "----------end----");
});
}
executorService.shutdown();
System.out.println("-----over");
}
输出
pool-1-thread-1----------start----
pool-1-thread-3----------start----
pool-1-thread-2----------start----
-----over
pool-1-thread-1----------end----
pool-1-thread-2----------end----
pool-1-thread-3----------end----
在这个demo里
三个线程由线程池executorService管理并运行。
注意:shutdown()的意思是不再接收新的线程,但会把未执行完的线程跑完。
在学线程池过程中,也许我们会接触很多类,时间长就晕了。
我来一句话概况线程池的所有相关概念:线程池吃进任务,吐出返回值。
如下图所示
从图中就可以发现,线程池相关的就是三个概念:任务(入参),线程池(核心),返回值。
问:为什么前面第一个demo中,把任务塞给线程池后,好像也没有返回值。
答:是的。所以上图中的返回值有两种。其中一种就是void,也就是:无返回值。第一个demo中就是这种情况。
这个图解释了两个问题:
什么是线程池
线程池怎么生成
下面先从简单的内置线程池说起(上图中的左下角部分)。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue());
}
ExecutorService executorService = Executors.newCachedThreadPool()
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue());
}
ExecutorService executorService = Executors.newFixedThreadPool(5)
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue()));
}
ExecutorService executorService = Executors.newSingleThreadExecutor()
实际生产中,严格来说,这三种线程池都不建议使用。
当然,如果项目中基本没有其他线程。要使用线程池的任务量也不大。也可以使用。
这是线程池的很重要的一个实现类。在定义线程池时,需要我们自己new。通过学习这个类的使用,我们可以大体了解线程池的概念。
在此之前,我们先看看Executors工具类是怎么创建的,先学习一下。后面我们再根据我们自己的业务需求自定义。
就以第一个demo中的线程池为例:ExecutorService executorService = Executors.newCachedThreadPool()
看一下Executors.newCachedThreadPool
这个静态方法
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue());
}
首先第一点,就像我们前面说的。Executors是一个工具类,它创建线程池,也是通过new的ThreadPoolExecutor类。
第二点,这个ThreadPoolExecutor入参有点多,看起来有点复杂。我们依次看一下各个参数的概念。
$ 2^{31} -1$
,几乎等于没有上限。TimeUnit.SECONDS
。加上前面的数量,就表示等待60秒。下面是ThreadPoolExecutor完整的的构造方法
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue 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.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
比我们前面看到的参数还要多两个:
只要是实现了BlockingQueue接口的实现类基本都可以,不需要我们自己实现,用JDK已有一些实现类就行,如下图所示
实现类很多,但一般像newFixedThreadPool那样,用LinkedBlockingQueue就可以,关键在于把队列定多大。
像newFixedThreadPool那样不设置new LinkedBlockingQueue
不设置,就是无限大的队列
而我们建议new LinkedBlockingQueue
,这样给阻塞队列设置一个大小。
ThreadFactory是一个需要我们实现的接口,接口内容很简单,只有一个方法
public interface ThreadFactory {
Thread newThread(Runnable r);
}
还是参考一下newFixedThreadPool。它用的是Executors.defaultThreadFactory()
这样一个默认线程工厂,它的内部实现如下图所示
我们自己创建,并不需要像上面这样设置如此繁琐。主要就是线程名称控制好就可以了(线程出了问题,马上知道是哪里的问题)。
什么叫拒绝策略:是入线程池前的筛选条件?是线程池不够用的时候的处理方案?还是报错的时候的处理方案?
答:是第二种理解方式:如果线程池已经满了,队列也满了。我们要怎么处理。所以本质上这是一种为保护线程池的服务降级策略(为保证核心线程正常执行,把一部分任务简单处理掉)。
和前面一样,同样看一下需要实现的接口:
public interface RejectedExecutionHandler {
void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}
看起来也很简单,只需要实现一个方法。需要传两个参数:一个任务,一个线程池。
还是看一下newFixedThreadPool默认使用的拒绝策略,结果发现Executors根本没管这个参数。默认拒绝策略是线程池ThreadPoolExecutor内实现的。
public static class AbortPolicy implements RejectedExecutionHandler {
public AbortPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
throw new RejectedExecutionException("Task " + r.toString() +
" rejected from " +
e.toString());
}
}
这个拒绝策略正如它的名字Abort(终止),简单粗暴,啥也没干,直接报错。
除了这个拒绝策略,ThreadPoolExecutor还为我们实现另外三种。实现类都写在ThreadPoolExecutor内部,作为内部类。
特点如下:
当然,一旦我们需要考虑拒绝策略,那么以上这些可能都无法满足实际生产需要(过于简单粗暴)。
我们需要自己实现拒绝策略接口,比如记录日志或Redis缓存,等过了高峰期再执行等。
相信通过上面的介绍,你已经对线程池有了自己的一个理解。下面我们来总结一下核心线程数,最大线程数,任务队列之间的关系。
关系图:
如果用taskCount表示当前线程池内所有任务量(正在执行的+队列等待的)。 queueSize表示队列最大长度。
源码中并没有taskCount这样一个统计数据。但我发现,如果不加这个概念去讲,很容易越讲让人越糊涂。原本是正确理解,讲完反而把人带沟里了(本质是因为线程池的设计思想其实很容易想明白,但源码写的有点反直觉)
下面我们看看,当我们执行executorService.execute
的流程图,下图展示的就是在正常执行状态下的流程(没有执行shutDown等操作)
步骤如下:
线程池(ThreadPoolExecutor)就像一个医院。线程(Thread)就是医生。医院只有固定数量的医生(核心线程数)。任务(Runnable)就是病人。
Worker就像是门诊(把医生和病人封装在一起空间里工作),门诊和医生一样都是不会随意增加的,创建出来,就和医生一起“重复使用”。(除非有新的医生被创建)
一个医生一个门诊,等待队列就是门诊外的椅子。如果门诊里有人看病,其他人就可以坐在椅子上排队(有人从门诊里出来,等待的病人就往前挪一挪)。
医院规定了一个最多医生上限(医院也要考虑用人成本。这里指的就是最大线程数)。如果某个前来看病的人发现当前医生数量已经达到最多医生上限,而且门外没座位可坐,此时他就会被拒绝。
如果医院比较贪心,虽然定了最多医生上限,却在门外放了好多椅子。病人来了就可以排队,导致病人需要排好长的队。
如果医院比较吝啬,门外只放置了少量椅子。病人来了一看,医生人数并没有超出最多医生上限,但自己也没有位置可排队。怎么办呢(任务不是人,人有没有椅子也可以自觉排队,但任务不行)。医院此时就得向其他医院临时借调医生来处理。然后这个新来的医生就会和其他医生一起把等待的线程处理完,这个临时医生发现处理完了,就可以回去了。
当然,比喻终归是比喻,能帮助我们简化理解,但也可能会引起误解。如果要看线程池具体实现逻辑,请看后面的实现机制。
线程池的状态变化本来是打算写在这一小节的。但是细想了一下,按照我的学习过程。一开始我并没有怎么关心状态变化以及怎么变化的。
基础篇是以让人知道是什么,怎么用以及大体逻辑关系。
所以这部分就放在实现机制篇里讲。
如果想现在就了解的,请看实现机制篇的最后[线程池状态变化]。
前面的自定义线程池其实已经非常灵活了,所以才有那么多的参数需要我们自己创建。
但有些场景下可能觉得这些参数还不够。这就需要自己写个线程池实现类,继承ThreadPoolExecutor,并对ThreadPoolExecutor这个类中的一些方法进行重写。ThreadPoolExecutor为我们提供了一些模板方法(钩子函数),如下所示
这些钩子方法可以在执行任务之前、之后,关闭线程池的时候被触发(AOP思想)。
当然,如果这些定制化接口还不够。那就干脆抛弃JDK给我们的实现类ThreadPoolExecutor,直接自己去实现ExecutorService接口(实际生产中应该很少有人这么干)。
创建线程池的方式:
有两种任务类型:
看一下这两个接口
@FunctionalInterface
public interface Runnable {
public abstract void run();//无返回值,也没有抛异常
}
@FunctionalInterface
public interface Callable {
V call() throws Exception;//有返回值V,并且可以抛出异常Exception
}
可以看到Runnable接口中的run方法,既没有返回值,也不抛异常。
@FunctionalInterface是什么意思?
加上这个注解之后,这两个接口都称为:函数式接口。是java8增加的“函数式编程”里的内容。
所以前面我们添加的任务可以写成:() -> {xxxx} 这种lambda表达式的形式。
如果对这一块不太清楚,可以看另一篇博客Lamdba表达式应用及总结
两种返回值,其中一种是void,没有返回值
还有一种是Future,字面意思是“将来”。的意思。表示在将来可以获得值,因为是异步获取的任务返回值。
具体实现机制可以看实现机制篇的[submit + Future + Callable实现异步返回值]
前面已经介绍了:线程池、任务、返回值。
现在把他们组装在一起。根据一开始说的线程池吃下任务,吐出返回值,看看实际有哪些组合方式。
前面我们看到把任务添加进线程池,用的是execute。其实还有另一种添加进程的方法:submit。总共有如下一些组合方式:
public void m4() {
ExecutorService executorService = Executors.newCachedThreadPool();
Future future = executorService.submit(() -> {
System.out.println("----------start----");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("----------end----");
return "ok";
});
System.out.println("------------------");
try {
System.out.println(future.get());//在这里阻塞,等待着线程池返回结果
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
} finally {
executorService.shutdown();
}
System.out.println("-----over");
}
输出
------------------
----------start----
----------end----
ok
-----over
future.get()
会阻塞住,等待线程池返回结果。
在上面的demo里,往submit方法里传入的其实是Callable类的lambda写法(和Runnable一样)。而Callable的call方法除了有返回值,还可以抛异常。那这个异常怎么捕获呢?
对Future
这句话捕获异常吗?
不,其实代码里已经写了。看future.get()
被莫名其妙的包裹了一堆异常捕获。就是用来捕获方法里的异常的。
所以:如果方法里抛异常了,就会在future.get()
被捕获到。
上面demo中只添加了一个任务。但线程池一般不止一个任务,那多个返回值怎么接收呢。
很简单:创建一个List
如果觉得这个不够优雅,可以把submit替换成invokeAll,直接传入任务集合,然后返回结果集合。
关闭线程池有两种操作:
/**
* 多线程运行,shutdown()
* 其他正在运行的线程会执行完,但不再接收新的任务(报错)
*/
public void m6() {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++) {
executorService.execute(() -> {
String threadName = Thread.currentThread().getName();
System.out.println(threadName + ": ok");
});
}
System.out.println("------------------");
executorService.shutdown();
//再次尝试添加任务
executorService.execute(() -> {
String threadName = Thread.currentThread().getName();
System.out.println(threadName + ": ok");
});
System.out.println("-----over");
}
结果:
pool-1-thread-1: ok
pool-1-thread-3: ok
------------------
pool-1-thread-2: ok
pool-1-thread-5: ok
pool-1-thread-4: ok
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task com.yc.testThread.TestThreadPool$$Lambda$2/359023572@123a439b rejected from java.util.concurrent.ThreadPoolExecutor@7de26db8[Terminated, pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 5]
at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063)
at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830)
at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1379)
at com.yc.testThread.TestThreadPool.m6(TestThreadPool.java:177)
at com.yc.testThread.TestThreadPool.main(TestThreadPool.java:16)
可见shutdown(),并没影响其他正在运行的线程,但会通过报错的方式拒绝新添加线程。
/**
* 多线程运行,shutdownNow()
* 正在运行的线程都会停止
*/
public void m7() {
ExecutorService executorService = Executors.newFixedThreadPool(2);
for (int i = 0; i < 5; i++) {
executorService.execute(() -> {
System.out.println(Thread.currentThread().getName() + ": ok");
});
}
List res = executorService.shutdownNow();
System.out.println("-----over: " + res.size());
}
输出:
pool-1-thread-1: ok
pool-1-thread-2: ok
-----over: 3
虽然还是输出了两条,但这是因为任务比较简单,执行的特别快,在执行到shutdownNow()时,有两个任务就已经执行完了。而哪些还没来得及执行的,就被中断了。
虽然shutdownNow()看起来比较粗暴,但也做到了尽量”优雅“:把没执行的任务返回来。可以看到输出3,表示还有三个任务没执行。
所以提交给线程池的任务,一定要是独立的任务,不可相互依赖。如果有依赖,可以分别提交给不同的线程池去执行。
当然,如果用newCachedThreadPool,就不会有这个问题。因为没有任务会进队列等待。
当核心线程数设置的太少,最大线程数和队列太长,那么就会出现大量任务等在队列里的情况。使得任务从提交到执行完成,经历时间过长。如果此时有其他程序等任务结果,那么极有可能出现等待超时,使得系统不可用。
如果线程池配置的过于保守,而业务量太大,则会出现大量任务被执行拒绝策略的情况。
根据前面所说的情况,我们可知,线程池的配置其实很不容易。所以就有人提出了动态线程池的方案。
简单来说就是通过监控线程池状况,设置阈值。然后通过配置环境直接动态修改线程池的参数(核心线程数,最大线程数,等待时间等)。
线程池中的监控和动态修改参数的方法:
// 监控
getRejectedExecutionHandler()
getCorePoolSize()
getMaximumPoolSize()
getKeepAliveTime(TimeUnit unit)
getQueue()
getPoolSize()
getLargestPoolSize()
getTaskCount()
// 动态配置
setRejectedExecutionHandler(RejectedExecutionHandler handler)
setCorePoolSize(int corePoolSize)
setMaximumPoolSize(int maximumPoolSize)
setKeepAliveTime(long time, TimeUnit unit)
我们可以给系统增加一个线程池监控、配置的页面。从而根据实际情况,随时调整线程池的各项配置(动态配置完,线程池会马上生效)。
至此,基础部分就结束了,后半部分是深入解析。主要是通过源码,更进一步理解线程池。
建议继续看一下,因为尽管我已经尽量通俗详细的解释,其实还是有可能会让人产生误解的地方。而且有很多细节是看了源码后才能更深刻的理解它的准确含义。