前言
上一篇文章我们解决了父子线程数据传递的问题
但是文章所用线程池设置的拒绝策略会导致在任务过多的情况下取出的用户信息依然为空,
这个是为什么呢?该怎么解决呢?下面我们可以通过源码来分析一下原理
原理
首先我们先来回顾下自定义线程池时的拒绝策略
当线程池的任务队列满了之后(一般都是有界队列), 并且线程数也已经达到了最大线程数,此时就会执行拒绝策略
目前JDK提供了四种拒绝策略
- AbortPolicy: 丢弃任务,并抛出 RejectedExecutionException 异常。
- CallerRunsPolicy: 该任务被线程池拒绝,由调用 execute方法的线程执行该任务。
- DiscardOldestPolicy: 抛弃队列最前面的任务,然后重新尝试执行任务。
- DiscardPolicy: 丢弃任务,不抛出异常。
在上篇文章中,我们使用的是CallerRunsPolicy策略, 使用这种策略会导致主线程会去执行线程池的任务,会发生什么呢?
//自定义的TaskDecorator
static class ContextCopyingDecorator implements TaskDecorator {
@Nonnull
@Override
public Runnable decorate(@Nonnull Runnable runnable) {
//这行代码是由主线程执行
SecurityContext securityContext = SecurityContextHolder.getContext();
return () -> {
try {
SecurityContextHolder.setContext(securityContext);
runnable.run();
} finally {
//这行代码是由当前线程执行
SecurityContextHolder.clearContext();
}
};
}
也就是说,如果主线程执行了一次线程池中的任务后,会清除掉主线程中的用户信息,然后下次从主线程里面取用户信息的时候,取出来的securityContext 就是null
那如果想解决这个问题的话,有两个方法
- 自定义一个拒绝策略
- 线程池的队列容量和最大线程数根据业务调整的更大一些
为了让大家的理解更透彻,下面带大家看看线程池的excute方法的源码
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();//c表示的是当前正在运行的线程的数量 ctl是一个AtomicInteger 32位 其中3位存储线程池状态 29位存储线程数量
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)//如果创建的线程数是0
addWorker(null, false);//第一个参数为null 说明只为了新建一个线程
}
else if (!addWorker(command, false))//插入队列不成功的话 且当前线程数小于最大线程数则新建线程
reject(command);//还是超过最大线程则执行拒绝策略
}
看下新建线程的addWorker方法
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (;;) {//CAS给线程数加1
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 ||//大于最大线程数 此时core为true
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
if (compareAndIncrementWorkerCount(c)) //CAS修改线程数+1 成功则退出CAS
break retry;
c = ctl.get(); // Re-read ctl
if (runStateOf(c) != rs)//线程池状态发生改变的话则回到外层循环
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}
//下面的操作主要是新建线程放入worker池中
可能有朋友问了,从队列里面取任务执行的源码在哪里呢?大家可以看下Worker的run方法,如下
/** Delegates main run loop to outer runWorker */
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) {
//下面省略
在getTask()方法里面就会有从队列取任务的过程,这里就不给大家展示了,有兴趣的可以自己去看看
至于拒绝策略的执行源码就更直观了,我给大家放一个CallerRunsPolicy策略的源码方法
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
r.run();//这个是主线程
}
}
总结
线程池任务执行顺序
-
首先检测线程池运行状态,如果不是running,则直接拒绝。
-
如果workCount < corePoolSize,则创建并启动一个线程线程来执行提交任务。
-
如果workCount >= corePoolSize,且线程池阻塞队列未满,则将任务添加到阻塞队列中。
-
如果workCount >= corePoolSize && workCount < maximumPoolSize,并且线程池内的阻塞队列已满,则创建并启动一个线程来执行新提交的任务。
-
如果workCount >= maximumPoolSize ,并且阻塞队列已满,则根据拒绝策略来处理改任务,默认是直接抛出异常
如何设置线程核心数,最大线程数
先提前告诉大家,不要纠结设置多少线程。没有标准答案,一定要结合场景,带着目标,通过测试去找到一个最合适的线程数。
可能还有同学可能会有疑问:我们系统也没啥压力,不需要那么合适的线程数,只是一个简单的异步场景,不影响系统其他功能就可以
很正常,很多的内部业务系统,并不需要啥性能,稳定好用符合需求就可以了。那么按照下面来就可以
//这个是获取系统核心数的方法
n = Runtime.getRuntime().availableProcessors()//获取逻辑核心数,如6核心12线程,那么返回的是12
CPU密集型(计算密集型):系统的I/O读写效率高于CPU效率,大部分的情况是CPU有许多运算需要处理,使用率很高。但I/O执行很快。此时核心线程数为n, 最大核心线程数设置为2n
I/O密集型:系统的CPU性能比磁盘读写效能要高很多,大多数情况是CPU在等I/O的读写操作,此时CPU的使用率并不高,此时核心线程数设置为2n, 最大核心线程数设置为4n
最大线程数的意义其实不大,这里仅供大家参考
队列建议选择有界队列,防止系统资源耗尽,队列容量根据自己的业务需求来设定
拒绝策略的选择,如果内置的能满足需求就用内置的,如果有定制需求可以自定义实现拒绝策略