文章目录
序言
在平时写代码的时候,我们经常会听到各种池,比如内存池、对象池,还有本文要介绍的——线程池。"池"通常意味着存储了一些比较昂贵的资源,并对它们进行统一管理。在线程池中,线程就是·昂贵的资源·,通过线程池执行任务,我们能做到复用线程资源,节省内存开销,对多个线程进行统一调度,比如说限制它们的最大数量。那么,1个线程池需要有什么功能,Java线程池是如何实现的,在使用它的时候有哪些需要注意的地方呢?本文将会一一道来
有趣的两个问题
有经验的程序员可能经常使用线程池,这里有两个我遇到的两个有趣的问题,希望能引起你阅读本文的兴趣
- 如何实现线程的复用,每个线程执行完之后状态就会变为TERMINATED,不能再继续执行,如何避免这个限制,让线程可以一直执行,达到复用的目的呢?
- 线程池通常具有扩容和缩容机制,缩容即一段时间内线程没有执行代码就会销毁自己,应该如何实现这个功能呢,是通过定时器不断轮询吗?
最简单的线程池
在序言中我们提到了我们提到了可以通过线程池执行任务,见下图(概念图)
通过这个图我们可以知道线程池的功能——它有一个线程集合,当外部提交任务时,线程池可以从线程集合里获取线程让它去执行,但是这个功能还不够,比如说如果现在池子里已经没有线程了,怎么办?线程池需要将这些任务存储起来,当池子里有空闲线程的时候再执行,我们可以简单的修改图1-1
一个最简单的线程池有这些内容就够了,通过自己维护的线程集合去执行外部提交的任务
实现一个线程池demo
结合上述简易线程池的功能,我们直接先写出demo框架
class KKThreadPool {
// 初始化线程集合
KKThreadPool(int poolSize) {
for (int i = 0; i < poolSize; ++i) {
Thread thread = new Thread();
threadList.add(thread);
}
}
// 待执行任务
private List<Runnable> codeList = new ArrayList<>();
// 线程集合
private List<Thread> thread = new ArrayList<>();
// 添加任务
public void sumbit(Runnable runnable) {
}
// 调用线程执行代码快
public void start() {
}
}
线程执行,其实就是在执行它的target字段对应的任务,所以我们想复用线程,就需要不断把它的target字段设置为我们要运行的任务,除此之外,线程执行完之后,它的状态会变成TERMINATED,会导致后续无法继续执行,所以我们需要重置这个状态为NEW
将框架填充完整后,代码如下,约100行左右
class KKThreadPool {
// 初始化代码集合
KKThreadPool(int poolSize) {
for (int i = 0; i < poolSize; ++i) {
Thread thread = new Thread();
threadList.add(thread);
// 记录Thread的元信息,重置状态时使用
resetFieldMetaMap.put(thread, new ResetFieldMeta(thread));
}
}
// 待执行任务
private List<Runnable> codeList = new ArrayList<>();
// 线程集合
private List<Thread> threadList = new ArrayList<>();
// 线程池是否已经开始执行任务
private boolean started;
// 线程池组
private final ThreadGroup threadGroup = Thread.currentThread().getThreadGroup();
// 线程——线程元信息Map
private Map<Thread, ResetFieldMeta> resetFieldMetaMap = new HashMap<>();
// 提交任务
public void sumbit(Runnable code) {
codeList.add(code);
if (!started) {
start();
started = true;
}
}
public void start() {
new Thread(() -> {
while (true) {
try {
Runnable code = null;
// 从待执行的任务中选一个
if (codeList.size() > 0) code = codeList.get(0);
if (code != null) {
for (Thread thread : threadList) {
synchronized (thread) {
// 选择一个线程状态为NEW或者TERMINATED的,用来执行任务
if (thread.getState() == Thread.State.NEW || thread.getState() == Thread.State.TERMINATED) {
// 设置target runnable
resetFieldMetaMap.get(thread).resetThreadStatus();
resetFieldMetaMap.get(thread).setTarget(code);
thread.start();
break;
}
}
}
}
codeList.remove(code);
} catch (Exception e) {}
}
}).start();
}
class ResetFieldMeta {
Thread thread;
Field threadGroupField;
Field targetField;
Field threadStatusField;
public ResetFieldMeta(Thread thread) {
this.thread = thread;
try {
threadGroupField = thread.getClass().getDeclaredField("group");
threadGroupField.setAccessible(true);
targetField = thread.getClass().getDeclaredField("target");
targetField.setAccessible(true);
threadStatusField = thread.getClass().getDeclaredField("threadStatus");
threadStatusField.setAccessible(true);
} catch (Exception e) {}
}
void resetThreadStatus() {
try {
threadGroupField.set(thread, threadGroup);
targetField.set(thread, null);
threadStatusField.set(thread, 0);
} catch (Exception e) {
}
}
void setTarget(Runnable target) {
try {
targetField.set(thread, target);
} catch (Exception e) {}
}
}
public static void main(String[] args) {
KKThreadPool threadPool = new KKThreadPool(5);
Runnable runnable = () -> System.out.println(Thread.currentThread().getName() + " hello world");
for (int i = 0; i < 100; ++i) {
threadPool.sumbit(runnable);
}
while (true);
}
}
/**Output
Thread-0 hello world
Thread-2 hello world
Thread-1 hello world
Thread-3 hello world
Thread-4 hello world
Thread-0 hello world
Thread-1 hello world
Thread-2 hello world
Thread-0 hello world
Thread-1 hello world
Thread-2 hello world
Thread-0 hello world
Thread-1 hello world
Thread-2 hello world
...
*/
demo存在的问题
虽然我们实现了一个简易线程池,但是它实在过于简陋,有很多明显的问题需要解决,比如
-
需要一个单独的线程去轮询当前有没有任务待执行,这样做无效耗费CPU资源
-
线程池集合是在最开始的构造函数中创建的,有可能会造成线程资源浪费。比如一共只有三个任务需要执行,这里创建了10个线程
-
存储任务的列表是没有界限的,如果提交任务的速度远远大于执行任务的速度,会造成任务积累,甚至导致OOM
-
通过反射的方式重置线程状态来实现线程复用,更像是旁门左道,代码实现不够优雅
-
线程无法携带个性化信息,比如设置线程名称,给线程携带更多信息
Java线程池是如何实现的
结合最开始的线程池基本能力,以及后来demo中存在的问题,我们对理想的线程池有了更多要求
-
能够通过自己维护的线程,执行外部提交的任务,且这部分线程根据任务的提交数量/速率调整数量
a. 线程随着任务的提交创建,且有最大数量限制(不限制,线程池就没有意义了,来一个任务建一个线程,还用线程池干啥,对吧)
b. 如果一段时间没有任务提交了,线程可以自己销毁,但是我们可以不全部销毁,留下几个平时用,避免来新任务了还需要创建新的,在内存/CPU(创建线程需要消耗CPU资源)之间取得平衡
-
通过一种非反射的方式,实现线程的复用
-
可以定制任务列表的最大数量,如果超过数量限制,我们可以自定义拒绝策略
Java的线程池ThreadPoolExecutor完美解决了上述问题
-
它的线程随着任务的提交进行创建。用户可以定制核心线程数,最大线程数,线程最长存活时间,将线程资源控制在一定范围内
-
它没有采用单独启动线程去轮询的方式执行任务。而是将任务存在队列中,让创建的线程调用workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) 方法获取待执行任务,这样获取不到时线程会陷入阻塞,节省CPU资源,同时这里的超时时间刚好是线程最长存活时间,所以可以同时check当前线程数是不是大于了核心线程数,且在一段时间内都没有执行任务,决定是否要销毁自己
-
用户可以定制任务存储使用的队列类型
-
用户可以设置线程工厂,进而定制自己的线程
接下来我们从源码入手,分析ThreadPoolExecutor的实现,ThreadPoolExecutor源码有千行左右,逐行介绍容易丢掉重点,所以我选择了几个关键的实现,通过这几个部分将全部代码串联起来
成员变量
ThreadPoolExecutor的成员变量比较少,而且每个都很关键,下面我们逐一介绍
// 线程池状态 + 线程池实际线程数量,ThreadPoolExecutor将int类型分为两部分,高三位代表状态,低29位表示数量
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
// 任务集合,每个Runnable都是一个任务, 用户通过设置该字段控制队列大小,比如配置workQueue=new ArrayBlockingQueue<Runnable>(1);表示队列最多只允许有1个元素
private final BlockingQueue<Runnable> workQueue;
// 线程集合
private final HashSet<Worker> workers = new HashSet<Worker>();
// 可重入锁, 保证一个代码块在某一个时刻只有1个线程在执行,比如说获取线程池数量的时候,要结合线程池状态&实际数量进行返回,可以看ThreadPoolExecutor#getPoolSize方法
private final ReentrantLock mainLock = new ReentrantLock();
// 在线程池中用来实现唤醒有等待时间的线程,比如A线程等待线程池到结束状态X时间,当B线程将线程池置为结束状态后,需要立刻唤醒A线程
private final Condition termination = mainLock.newCondition();
// 线程池中曾经最多出现过多少线程
private int largestPoolSize;
// 线程池一共完成了多少任务
private long completedTaskCount;
// 线程工厂,用户可以使用它定制自己的线程
private volatile ThreadFactory threadFactory;
// 当到达workQueue的上限时,如何处理被拒绝的任务,可以通过配置handler实现
private volatile RejectedExecutionHandler handler;
// 线程池最大核心线程数
private volatile int corePoolSize;
// 线程池最大线程数
private volatile int maximumPoolSize;
// 是否允许核心线程销毁
private volatile boolean allowCoreThreadTimeOut;
// 线程在不执行任务时最多可存活多久
private volatile long keepAliveTime;
新增任务会发生什么
新增任务一共有3种结果,分别对应三个函数
-
立刻创建新线程执行任务,ThreadPoolExecutor#addWorker
-
将任务放到队列中,等待执行, isRunning© && workQueue.offer(command)
-
拒绝任务, ThreadPoolExecutor#reject
提交任务时,对应着ThreadPoolExecutor#execute方法
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
// 查看当前的线程数量是否小于核心线程数,如果是则直接创建线程,且立刻执行
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
// 当前线程已经大于核心线程数,将任务添加到队列中
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
// 任务添加到队列失败,则创建线程(非核心),且立刻执行
else if (!addWorker(command, false))
reject(command);
}
创建新线程,并把它添加到线程集合,对应着addWorker方法
**
* 整个addWorker方法分为两个大的部分,第一部分是增加workerCount数字大小,增加成功之后再新创建线程
* 第一部分是无锁操作,通过自旋 + CAS实现
* 第二部分是使用了成员变量中的mainLock加锁实现的
* 这样做可以保证最多只会有maximumPoolSize个线程等待锁,其余的线程会直接返回添加失败
*/
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
// 校验线程池状态,状态合法再校验corePoolSize/maximumPoolSize,都满足条件后新增workerCount
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// Check if queue empty only if necessary.
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
for (;;) {
int wc = workerCountOf(c);
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
if (compareAndIncrementWorkerCount(c))
break retry;
c = ctl.get(); // Re-read ctl
if (runStateOf(c) != rs)
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
w = new Worker(firstTask);
final Thread t = w.thread;
// 先创建线程,然后加锁添加到线程集合中,这样做可以避免并发访问workers导致的问题,比如#interruptWorkers#方法还在遍历workers,这时候就不能添加线程
if (t != null) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// Recheck while holding lock.
// Back out on ThreadFactory failure or if
// shut down before lock acquired.
int rs = runStateOf(ctl.get());
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
workers.add(w);
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
mainLock.unlock();
}
// 添加完成后,立刻开始执行
if (workerAdded) {
t.start();
workerStarted = true;
}
}
} finally {
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}
最后,介绍下拒绝任务的方法
final void reject(Runnable command) {
// 调用成员变量中的handler的rejectedExecution方法实现的,让用户可以自己定制拒绝策略
handler.rejectedExecution(command, this);
}
线程自己如何获取待执行任务
在最开始的时候,我们提到了每个线程执行完之后状态就会变为TERMINATED,不能再继续执行,避开这个限制的方法其实很简单,就是让线程永远结束不了,所以Java线程池中线程的run方法其实会不断从任务队列里获取任务,除了任务抛出异常和销毁自己时,它不会终止轮询。不过它获取任务是阻塞获取的,这样可以避免无效的CPU使用
Worker类继承了Runnable,将它自己作为参数创建了Thread。除此之外,Worker还继承了AQS同步器,它在线程池中的使用比较少,只有执行任务和中断线程两处有使用
private final class Worker
extends AbstractQueuedSynchronizer
implements Runnable
{
Worker(Runnable firstTask) {
// AQS 的实现就是将state作为竞争资源 + 队列实现,这里直接置为state置为-1,说明只使用CAS可以,没有增删操作
setState(-1); // inhibit interrupts until runWorker
this.firstTask = firstTask;
// 通过成员变量,用户配置的线程工程创建线程,同时这里的runnable就是自身
this.thread = getThreadFactory().newThread(this);
}
public void run() {
// 这里的runWorker方法不断轮询任务队列
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 {
// 这里的getTask就是会不断从任务队列里查询是否有任务,如果没有任务,getTask会阻塞
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
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
afterExecute(task, thrown);
}
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
}
// 标记任务抛出异常
completedAbruptly = false;
} finally {
// completedAbruptly为false的时候,processWorkerExit方法会尝试重新创建一个线程
processWorkerExit(w, completedAbruptly);
}
}
// 轮询任务队列,获取待执行的任务
private Runnable getTask() {
boolean timedOut = false; // Did the last poll() time out?
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;
}
int wc = workerCountOf(c);
// Are workers subject to culling?
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
// 这里会检查该线程是否需要销毁了,条件如下
// 1. 线程数大于1 或者 当前没有需要执行的任务
// 2. 线程数已经大于了最大线程数 并且 (已经这个线程已经等了最大存活时间,还是没有等到任务,除此之外(当前线程数大于了核心线程数,或者核心线程数本身也允许被销毁)
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c))
return null;
continue;
}
try {
// 这里是设置了超时时间的阻塞获取,如果超过时间后还没有获取到,将timeOut置为false
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
动态调整线程池核心线程数大小会发生什么
Java的线程池是可以动态调整线程池中线程数量的,根据之前我们讲解的两个小节,我们思考下如果新设置的线程数量大于或者小于当前线程池核心线程数,需要怎么调整呢
-
如果新的值大于当前的核心线程数,调用addWorker方法新增
-
如果新的值小于当前的核心线程数,将corePoolSize设置为新的值,这样已有的线程在执行上面提到的getTask方法时,可能会返回null,然后线程数就会将自己从线程集合里移除,达到销毁线程的目的
public void setCorePoolSize(int corePoolSize) {
if (corePoolSize < 0)
throw new IllegalArgumentException();
int delta = corePoolSize - this.corePoolSize;
// 设置为新的值,corePoolSize是volatile,保证其他线程会读到这个值
this.corePoolSize = corePoolSize;
if (workerCountOf(ctl.get()) > corePoolSize)
// 中断当前还在等任务的线程,让他们检查当前线程数与corePoolSize的关系,然后销毁自己(可能,而非一定)
interruptIdleWorkers();
else if (delta > 0) {
// 调用addWorker新增,如果这里的workQueue为空,会导致新增线程失败
while (k-- > 0 && addWorker(null, true)) {
if (workQueue.isEmpty())
break;
}
}
}
使用线程池时有哪些注意事项
-
永远使用有界队列,否则早晚会有一直添加任务,导致OOM的场景(墨菲定律——你觉得不会发生的,一定会发生)
-
核心线程数的计算,根据我自己的经验,假设期望线程池能接受的最大QPS为A,单个任务执行的平均时间是Xms,则线程数为A / (1000/X),考虑到cpu切换的时间,可以适当增大A的值,但是不要设置太大,不然可能会突破A
-
提交任务到线程池后,任务执行时会丢失【提交任务的线程】的ThreadLocal信息,这时我们可以对任务进行包装,将主线程的信息当作成员变量进行保存,让执行任务时,设置为当前线程的ThreadLocal信息,比如
class WrapperRunnable implements Runnable {
private final ThreadLocal<Object> context = getCurrentThreadContext();
@Overide
public void run() {
// 将threadlocal信息改为提交任务的线程的,并记录当前线程的threadlocal信息
ThreadLocal<Object> backup = backupAndSet(this.context);
try {
// 执行任务
super.run();
} catch (Exception e) {
// 执行完任务后,将threadlocal改为执行任务的线程的信息
backupAndSet(backup);
}
}
}
那些常见的面试问题
-
线程池有哪些参数
-
核心线程是否可以销毁
-
关于Worker线程你知道多少
-
any other quetion…
关于我
IT打工人小胡,正在持续学习Java、Mysql、Redis、MQ等知识,任何问题,欢迎文章下评论