线程池学习入门
我们经常听说线程池,各大厂的面试题中也经常出现线程池的面试题目,所以我们今天开始学习线程池是什么、线程池设计思路以及线程池的简单使用。只有搞懂这些内容,我们才可以从容的面对各大厂关于线程池的面试题目以及日常开发过程中对线程池使用和问题的处理。
搞清楚线程池的原理我们从以下两个方面展开:
- 线程池是什么
- 线程池设计思路
线程池是什么
先不说线程池,我们日常开发过程中也会听到其他关于池的说法,比如说数据库连接池(国内比较出名的Alibaba的Druid、hikariCP、dbcp、c3p0等等)、redis连接池、HTPP连接池以及生活中常见的水池。
以上的各种池都是池化技术的体现,以数据库连接池举例来说,数据库连接池技术的核心思想是:连接复用,通过建立一个数据库连接池以及一套连接使用、分配、管理策略,使得该连接池中的连接可以得到高效、安全的复用,同时避免数据库连接频繁建立、关闭的开销。
类比数据库连接池,线程池的设计思想:线程复用,通过建立一个线程连接池以及一套线程使用、分配、管理策略,使得线程池中的线程可以高效、安全的复用,同时避免线程频繁创建的开销。
以上类比可能比够严谨,但是我觉得已经能够诠释线程池是什么了。那么我们接下来就好好了解下线程池的设计思路(下面开始所有讨论内容都是针对JDK 1.7中线程池的实现类ThreadPoolExecutor)。
线程池设计思路
通过简单的类比,我们会发现线程池的核心元素:线程。那么线程池内部的核心逻辑便是围绕根据线程的生命周期展示的:线程池能容纳多少线程、线程池是否能无限大、线程池的线程不够用怎么办、最最最重要的线程的创建和销毁。
线程池能容纳多少线程
线程池能容纳多少线程,理论上来说要看服务器的性能,一般来说服务器的性能越高线程池能容纳的线程也就越多,服务器的性能也能充分的理用。通过juc包下的Executors源码阅读我们可以知道线程池能容纳的最大线程数为2^31 - 1。
线程池是否能无限大
通过线程池的容量的了解,我们直到线程池并不是无限大的,线程池能容纳的最大的线程数为:2^31 - 1,所以线程池并不能无限大,只能说是上限容量满足业务场景是绝对够用的(21亿线程绝对够用了)。
线程池的线程不够用怎么办
21亿线程那绝对是够用的,但是线程数越大也意味着服务器的资源也得跟上,这是存在风险的。一般线程池中我们会指定一定数量的常驻线程,这些常驻线程用来满足常规的业务场景,但是如果这个时候服务的流量上升,常驻线程都用完了怎么办呢?
我们经常看新闻说某某某公司出事了,这时候临时工这个词很常见。线程池中也可以请临时工,但是线程中还需要给临时工指定工作的时间,这个工作时间是指线程空闲多久后就不再运行。
对于线程不够用的情况,线程池的策略里面给了两种选择:线程池扩容以及拒绝执行任务。
- 扩容,简单来说就是常驻线程都在运行,并且线程池中运行的线程数量还没有到达线程池的上限,这里就涉及到线程池的最大的线程数,当然这个数最大不能超过2^31 - 1。
- 拒绝执行任务,简单来说就是临时工也没了,再提交任务给线程池执行,线程池没有可用资源,这个情况下就只能拒绝本次任务了。
线程的创建和销毁
上面几个点我们说了线程池的容量,但始终忽视了线程池的核心对象线程,这里我们围绕线程的创建和销毁这两个点展开。
不过多说线程的创建,而是从规范和统一的角度来说线程的创建。针对规范和统一线程池创建的时候会要求提供一个线程工厂类,用来定义创建的线程的规格,简单来说就是提交的线程应该找什么样子:线程怎么命名的、是不是在线程组之内以及是不是后台运行任务等等。
前文中提到了两个概念:临时工以及临时工空闲时间。临时工我们还是可以理解为一个普通的线程,在线程的创建上和刚才说的没什么不一样的地方。我们重点说一下临时工空闲时间,这里就牵扯到线程池中一个大家都容易问的问题:临时工线程是怎样在规定时间内自动销毁的。
要理解这个问题,我们首先需要明确以下几块内容:
- 线程的销毁是什么
- 线程池是如何保证线程常驻线程不销毁
我们知道线程是一次性的,只要线程运行完指定的任务,便会退出,这就是我们刚才提到的一个概念:线程的销毁。既然线程是一次性的,线程池是如何复用这些线程的呢?答案是不让线程退出。不让线程退出,那很简单只要执行一个死循环就可以了,像下面这样:
while (true) {
// 线程执行的任务
doTask();
}
那问题又来了,如果执行死循环那岂不是没法执行其他的任务了?这里我们就需要一个循环获取任务并执行的机制,并在没有任务执行时挂起线程或者说没有任务超过临时工线程空闲时间便销毁线程。ThreadPoolExecutor是怎么做的呢,它是通过一个阻塞队列实现上述机制,看下面的源码:
- 线程执行任务
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 pool is stopping, ensure thread is interrupted;
// if not, ensure thread is not interrupted. This
// requires a recheck in second case to deal with
// shutdownNow race while clearing interrupt
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
try {
beforeExecute(wt, task);
Throwable thrown = null;
try {
task.run();
} catch (RuntimeException x) {
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
thrown = x; throw new Error(x);
} finally {
afterExecute(task, thrown);
}
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}
- 循环获取任务
private Runnable getTask() {
boolean timedOut = false; // Did the last poll() time out?
retry:
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// Check if queue empty only if necessary.
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount();
return null;
}
boolean timed; // Are workers subject to culling?
for (;;) {
int wc = workerCountOf(c);
timed = allowCoreThreadTimeOut || wc > corePoolSize;
if (wc <= maximumPoolSize && ! (timedOut && timed))
break;
if (compareAndDecrementWorkerCount(c))
return null;
c = ctl.get(); // Re-read ctl
if (runStateOf(c) != rs)
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
try {
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
上述代码中有两段很关键的代码,这两段代码实现了上面说的有任务执行,没有任务阻塞线程同时在空闲指定时间后销毁线程的机制,我们看下面的列表:
- while (task != null || (task = getTask()) != null)
- Runnable r = timed ? workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : workQueue.take();
第一段很好理解,有任务的时候执行,没有任务的时候销毁线程以及任务获取的时候没有任务便阻塞线程。第二段就更好理解了,在指定的时候内拉取阻塞队列中的任务,如果在规定时间内有任务拿到便返回,没有任务拿到便阻塞。如果poll超过了空闲时间则返回null,再综合第一段代码看是不是非常好理解了。
这里我们思考一个问题:线程池中需要区分线程是常驻线程还是临时工线程嘛?答案是不需要区分。
原理总结
通过上述的拆分,我们能概括线程池中几个重要的概念:
- 常驻线程数量
- 最大线程数量
- 临时工线程空闲时间
- 线程工厂
- 任务拒绝执行
- 阻塞队列
我们回到ThreadPoolExecutor源码,看看这个类的几个构造方法:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
threadFactory, defaultHandler);
}
我们会发现在Executors类中提供的几个线程池的构造工厂方法中,都是根据上述的构造方法提供出来的,不同的便是参数的差异。我们再来一一对比下我们刚才提到的几个概念:
- corePoolSize:常驻线程数量
- maximumPoolSize:最大线程数量
- keepAliveTime:临时工线程空闲时间
- threadFactory:线程工厂
- defaultHandler:任务拒绝执行
- workQueue:阻塞队列
相信到这里小伙伴们对线程池都有个大概的了解,如果有问题或者有错误的地方,欢迎大家留言指出(PS,我的个人博客:享客)