深入浅出:JAVA线程池
前言
最近研究java并发编程相关的一些知识,顺便看了下线程池,总结了一些大家认知上可能有误的部分分享分享,接下来我们就撸着源码,简单分享一下,如有问题,评论区一起讨论学习!
一、线程池基本参数介绍:
因为是深入浅出,所以这里就从最基础的知识开始说起吧。我们都知道要使用线程池,绕不开ThreadPoolExecutor,这里我们简单看看通用的构造函数:
1.1:jdk最基础的参数:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
针对基础薄弱的同学咱们简单解释一下里面参数实际含义及影响:
corePoolSize:线程池的基本大小,即是咱们这个线程被实例化的时候,池子里就有可供直接使用的线程数量。
maximumPoolSize:线程池中允许的最大线程数。也就是说,线程池中的当前线程数目不会超过该值。
keepAliveTime:线程空闲时间,当线程空闲时间达到keepAliveTime时,线程会退出,直到线程数量=corePoolSize。
unit:空闲时间单位:无非就是时分秒毫秒一类的。
workQueue:阻塞队列:然后阻塞队列其实也分有界阻塞队列和无界阻塞队列。
1.1.1:有界队列介绍:
就是有固定大小的队列。比如设定了固定大小的 LinkedBlockingQueue,又或者大小为 0,只是在生产者和消费者中做中转用的 SynchronousQueue:
ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。
LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列。
SynchronousQueue:一个不存储元素的阻塞队列。
1.1.2:无界队列介绍:
指的是没有设置固定大小的队列。这些队列的特点是可以直接入列,直到溢出。当然现实几乎不会有到这么大的容量(超过Integer.MAX_VALUE),所以从使用者的体验上,就相当于 “无界”。
PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。
DelayQueue:一个使用优先级队列实现的延迟无界阻塞队列。
LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
1.1.3:最容易出问题的地方警告:
警告一:红标注意:这里提前说一个误区,是不是一旦我的可用线程为0了,也就是池子里面没有可用线程了,就会去实例化新的线程放到我的池子里供后面任务使用呢?不是的!不是的!不是的!真正的应该这样的:
如果池子里面核心线程都被使用了,如果还有后续任务来,首先这些任务到我的阻塞队列里面去!阻塞队列满了,之后才会实例化新的线程,去从阻塞队列里面,取任务去做执行,直到我的线程数目=maximumPoolSize,这个时候,如果还有源源不断的任务过来,阻塞队列又满了,就会触发相关的线程池拒绝策略(直达相关线程池拒绝策略配置)。
警告二:这里有一个有意思的问题:我的线程池如果是无界队列,那么是不是真的无界,我设置的maximumPoolSize就相当于没有用。
这其实是一个极致问题:首先,无界并不是真的无界,它也有最大值,就是Integer.MAX_VALUE,但是这个值非常大,生产中真有那么多任务过来打到你一台服务器上,想不OOM都难。但是确实,一般用到了无界队列,设置的maximumPoolSize就相当于没有用了。
1.1.4:源码深度解析:
这里我们看看jdk源码:
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
/*
* Proceed in 3 steps:
*
* 1. If fewer than corePoolSize threads are running, try to
* start a new thread with the given command as its first
* task. The call to addWorker atomically checks runState and
* workerCount, and so prevents false alarms that would add
* threads when it shouldn't, by returning false.
*
* 2. If a task can be successfully queued, then we still need
* to double-check whether we should have added a thread
* (because existing ones died since last checking) or that
* the pool shut down since entry into this method. So we
* recheck state and if necessary roll back the enqueuing if
* stopped, or start a new thread if there are none.
*
* 3. If we cannot queue task, then we try to add a new
* thread. If it fails, we know we are shut down or saturated
* and so reject the task.
*/
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);
}
在任务执行的时候,其实调用的是这个execute方法,然后这里会对执行中任务数量和我们核心线程数量做对比,
第一个if就是:如果执行中任务数量<核心线程数,就会执行addWorker这个方法,总的来说addWorker这个方法也就是添加一个任务实例到HashSet<Worker>中;
第二个if就是:如果worker数量超过核心线程数,任务直接进入队列;看到没有!!这里是进入队列,而不是直接对线程池扩容!
第三个else if (!addWorker(command, false))表达的就是:如果线程池不是运行状态,或者任务进入队列失败,则尝试创建worker执行任务。
咱们看到,这里上上下下都用到了addWorker这个方法,那么这个方法是干嘛的?看源码:
我们分2部分讲
private boolean addWorker(Runnable firstTask, boolean core) {
//第一部分
retry:
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;
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;
}
在这里面细心的同学又看到了ctl,这个ctl到底是什么呢?我们深入研究下这个ctl,再继续分析他的执行步骤,注意,这里我们开始分析出现很多次的ctl到底是啥?可以干什么用?
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
这里的ctl是一个原子操作类,为什么需要AtomicInteger原子操作类?这里引用别人的一篇文章有详细介绍:原子操作类AtomicInteger详解_分享传递价值-CSDN博客_atomicinteger
我们看到源码:
// runState is stored in the high-order bits
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;
所以这里,ctl就是利用低29位表示线程池中线程数,通过高3位表示线程池的运行状态的一个int变量。
1、RUNNING:-1 << COUNT_BITS,即高3位为111,该状态的线程池会接收新任务,并处理阻塞队列中的任务;
2、SHUTDOWN: 0 << COUNT_BITS,即高3位为000,该状态的线程池不会接收新任务,但会处理阻塞队列中的任务;
3、STOP : 1 << COUNT_BITS,即高3位为001,该状态的线程不会接收新任务,也不会处理阻塞队列中的任务,而且会中断正在运行的任务;
4、TIDYING : 2 << COUNT_BITS,即高3位为010, 所有的任务都已经终止;
5、TERMINATED: 3 << COUNT_BITS,即高3位为011, terminated()
方法已经执行完成
这里我们先记下他们之间大小关系,这里后续的逻辑会用到:
RUNNING < SHUTDOWN < STOP < TIDYING < TERMINATED
然后我们回到主线上来:
retry:
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// Check if queue empty only if necessary.
// 这里就是之前那个线程池状态关系的逻辑了,也就是保证只有线程池状态为运行中才往下走,
//其他情况都是返回一个false
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
//这里内部开始自旋,这里逻辑大致就是worker数量超过容量,直接返回false;
//如果没有大于,则开始CAS增加worker数量,增加成功则break retry;开始下面的逻辑
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
}
}
接下来就是上面的自旋结束开始接下来实例化任务并安全的把任务加到 workers 里面,并且启动任务执行
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
//通过任务实例化worker对象
w = new Worker(firstTask);
final Thread t = w.thread;
if (t != null) {
//这里串行化添加woker,因此加锁;为了就是保证在并发
//情况下,我添加worker到我的池子中不会超过我池子本身大小
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();
//添加worker到线程池中,并更新现在largestPoolSize。
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;
1.2:jdk进阶的参数
然后我们继续点this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,Executors.defaultThreadFactory(), defaultHandler);我们又会发现其实远不止如此:
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;
}
我们看到了:
ThreadFactory:需要创建新线程的对象。使用线程工厂就无需再手工编写对 new Thread的调用了,从而允许应用程序使用特殊的线程子类、属性等等。
RejectedExecutionHandler:拒绝策略。
1.2.1:线程池的4种拒绝策略 这个是可以自己根据业务选择配置的:
1.ThreadPoolExecutor.AbortPolicy
线程池的默认拒绝策略为AbortPolicy,即丢弃任务并抛出RejectedExecutionException异常
意思就是,如果阻塞队列满了,然后也到达最大线程数了,但是任务源源不断的继续来,这种策略把后面提交的请求不放入队列也不会直接消费,而是抛出一个异常,这样会导致任务的丢失,但是对开发是有感知的。
我们来看看源码:
public static class AbortPolicy implements RejectedExecutionHandler {
/**
* Creates an {@code AbortPolicy}.
*/
public AbortPolicy() { }
/**
* Always throws RejectedExecutionException.
*
* @param r the runnable task requested to be executed
* @param e the executor attempting to execute this task
* @throws RejectedExecutionException always
*/
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
throw new RejectedExecutionException("Task " + r.toString() +
" rejected from " +
e.toString());
}
}
2.ThreadPoolExecutor.DiscardPolicy
丢弃任务,但是不抛出异常。如果线程队列已满,则后续提交的任务都会被丢弃,且是丢弃。
意思就是,如果阻塞队列满了,然后也到达最大线程数了,但是任务源源不断的继续来,这种策略就会把后续进来的任务都直接丢弃掉,不做执行,这样对开发没有任何感知,会导致被丢弃的任务无法再次被执行。
3.ThreadPoolExecutor.DiscardOldestPolicy
丢弃队列最前面的任务,然后重新提交被拒绝的任务
抛弃旧的任务,会导致被丢弃的任务无法再次被执行
4.ThreadPoolExecutor.CallerRunsPolicy
由调用线程处理该任务(不会丢弃任务,最后所有的任务都执行了,并不会抛出异常)
这个就是主线程直接执行该任务,执行完之后尝试添加下一个任务到线程池中,可以有效降低向线程池内添加任务的速度。
1.3:深入spring的参数:
在这里很多同学就以为基础的配置结束了,其实不是,我们spring的线程池还针对jdk的线程池做了一些装饰,我们看到ThreadPoolTaskExecutor。
进initializeExecutor这个方法里面去,这里我们发现,spring针对初始化我们的线程池做了一些自己的业务:
@Override
protected ExecutorService initializeExecutor(
ThreadFactory threadFactory, RejectedExecutionHandler rejectedExecutionHandler) {
BlockingQueue<Runnable> queue = createQueue(this.queueCapacity);
ThreadPoolExecutor executor;
if (this.taskDecorator != null) {
executor = new ThreadPoolExecutor(
this.corePoolSize, this.maxPoolSize, this.keepAliveSeconds, TimeUnit.SECONDS,
queue, threadFactory, rejectedExecutionHandler) {
@Override
public void execute(Runnable command) {
super.execute(taskDecorator.decorate(command));
}
};
}
else {
executor = new ThreadPoolExecutor(
this.corePoolSize, this.maxPoolSize, this.keepAliveSeconds, TimeUnit.SECONDS,
queue, threadFactory, rejectedExecutionHandler);
}
if (this.allowCoreThreadTimeOut) {
executor.allowCoreThreadTimeOut(true);
}
this.threadPoolExecutor = executor;
return executor;
}
1.3.1:taskDecorator:
这里有同学就注意到taskDecorator了,是的,taskDecoratory也可以理解成线程池的一个参数。
taskDecorator:在初始化线程池的时候,有一个判断就是,如果taskDecorator不为空,(这其实是一个处理子线程与主线程间数据传递的装饰接口,因为子线程没法带上主线程的一些上下文,所以我们就用到了taskDecorator,比如基于rpc协议调,开多线程用其他下层服务方法的时候,我们就需要它去做上下文封装透传,这个我们放后面来说具体怎么用,先把基础搞定)我们在使用这个线程的时候,还会去调用继承这个装饰器的方法做透传。
这里稍微补充一下吧,如果你想使用这个taskDecorator,应该如何使用:
直接上代码:
package com.lufax.wmc.stock.task;
import org.slf4j.MDC;
import org.springframework.core.task.TaskDecorator;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import java.util.Map;
public class ContextTaskDecorator implements TaskDecorator {
@Override
public Runnable decorate(Runnable runnable) {
try {
//获取请求上下文
RequestAttributes context = RequestContextHolder.currentRequestAttributes();
Map<String,String> previous = MDC.getCopyOfContextMap();
return () -> {
try {
//对需要传递的参数封装
RequestContextHolder.setRequestAttributes(context);
MDC.setContextMap(previous);
runnable.run();
} finally {
RequestContextHolder.resetRequestAttributes();
MDC.clear();
}
};
} catch (IllegalStateException e) {
return runnable;
}
}
}
然后在线程池配置这里:
commonThreadPool.setTaskDecorator(new ContextTaskDecorator());
注意线程池中有的线程是一直存在一直被复用的,所以线程执行完成后需要在TaskDecorator的finally方法中移除传递的上下文对象,否则就存在内存泄漏的问题。
1.3.2:allowCoreThreadTimeOut:
allowCoreThreadTimeOut:是否允许核心线程数消亡。所以如果有人问你,线程池中核心线程是否能被销毁,记住!是可以的!而且大大的可以!那么它啥时候消亡呢?
这个就是你设置的保活时间,一旦核心线程空置到达了保活时间,就会消亡,那么消亡了会对我们spring线程池有什么影响吗?有的!这个,我们看第二章详细介绍。
到这里,我们线程池基础配置参数都介绍完毕了。