从一个反直觉的案例来深入剖析线程池的源码部分

本文从一个反直觉的案例来深入剖析线程池的源码部分

如果你完全不了解线程池的构造参数或者执行任务的主要流程,可以先阅读前两篇文章,阅读本文默认你已掌握线程池的构造以及使用,同时本文章并不做详细的源码解读,而关注于整体流程,避免被过多的细节冲击而掌握不到主题

线程池的执行流程

首先我要在这里放一张图,如果你之前看过其他线程池的文章那我相信你对这个流程已经熟悉的不能再熟悉了,但我为什么还要放这张图,这是因为我将结合一个反直觉的代码例子让你对这张图有深刻了解

flowchart TD A[开始] --> B{线程池是否已关闭} B -->|是| C[拒绝任务] B -->|否| D{当前线程数小于核心线程数} D -->|是| E[创建新线程执行任务] D -->|否| F{任务加入队列} F --> G{队列是否已满} G -->|否| H[等待线程处理任务] G -->|是| I{当前线程数小于最大线程数} I -->|是| J[创建新线程执行任务] I -->|否| K[拒绝任务]

一个反直觉的案例

使用自定义线程池执行100个任务,平均分成10批执行,执行下一批前主线程将阻塞等待上一批全部完成

java 复制代码
​
    public static void main(String[] args) {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                10, // 核心线程
                20, // 最大线程
                60, // 保活时间
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(1),
                new TestRejectedExecutionHandler()
        );
        
        // 执行10组,每组10个线程,打印出线程名
        IntStream.range(0, 10).forEach(i -> {
            List<CompletableFuture<Integer>> c = IntStream.range(0, 10).mapToObj(index ->
                    CompletableFuture.supplyAsync(() -> {
                        log.info("任务线程:{} 运行结束,当前任务批次:{}", Thread.currentThread().getName(),i+1);
                        return index;
                    }, executor)).collect(Collectors.toList());
            // 等待一批任务全部结束后休眠两秒再进行下一批任务执行
            c.forEach(CompletableFuture::join);
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        executor.shutdown();
    }
​
public class TestRejectedExecutionHandler implements RejectedExecutionHandler {
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        System.out.println("触发了拒绝策略");
    }
}
​

会不会触发拒绝策略?

我相信有一部分人一定想说,哎 这里十个核心线程,每批就执行10个任务,当然不会触发拒绝策略咯。

没错,这是符合人的直觉的,但是事实并非如此,你可以copy至你的代码执行看看结果,那一定会打印出"触发了拒绝策略" 为了解释这个问题,请一定要仔细的看上面的流程图!

我这里的批次任务从1开始,而不是0方便理解。

关键在于你误认为第二批以及后面批次的任务没有入队而是直接交给了Worker执行,实际是第一批任务执行完后创建了10个核心线程,那么第二批及以后的任务是一定会入队的(核心线程数量不小于配置的数量)。

所以当你的入队的任务超出了核心线程的取出速度同时队列容量最大只有1 那么就很容易满足创建非核心线程的条件(核心线程满了,队列也满了)多执行几批次后线程数超过了最大值那么就触发了拒绝策略

这个问题的本质在于线程池中的workers取任务慢于每批次任务入队,导致队列满了再去创建非核心线程,最终超过了20最大线程数导致拒绝策略触发

题外思考,只改变一个线程池的构造参数,你有多少种办法让这10批任务全部执行完而不触发拒绝策略呢?

源码讲解

什么是Worker?

Worker 你可以理解为包装后的线程,线程池的池就是Worker的池,在代码中的池是private final HashSet<Worker> workers = new HashSet<Worker>();

关键属性

java 复制代码
final Thread thread;
/** Initial task to run.  Possibly null. */
Runnable firstTask;
  1. thread 是通过线程工厂构造的 this.thread = getThreadFactory().newThread(this); 这个thread 是final修饰也就是和worker是绑定的,不要忘记这里的thread的任务是worker,也就是worker本身也是一个可执行的任务,不过这个任务可以自己run自己而不需要外部thread调用,因为内部持有了thread。
  2. firstTask 从名字上来看为什么叫第一个任务,那是因为这个Runnable 只有在一次创建Worker时才会赋值为传入的任务,假如Worker后续被复用了后续执行的任务是直接从阻塞队列中拿的,这个firstTask 也没有用了,这也解释了为什么Worker中明明包含了任务和线程但不叫Task的原因,别人的Runnable 只是第一次临时使用

为什么实现了AQS?

  1. 这里我简单讲一下AQS 是一个同步阻塞队列,这里的同步阻塞对象不是Worker而是操作Worker的当前Thread,也就是说Worker实现了AQS是为了让多线程操作Worker不引发并发问题的解决方式
  2. 这里再分析一下Worker作为临界资源它当然不是可共享的,也就是说它需要实现AQS的排他锁,除此之外实现排他锁的还有ReentrantLock,实现共享锁的有CountDownLatch,Semaphore,本文并不多做解释AQS。
java 复制代码
// 独占锁实现
public void lock()        { acquire(1); }
public boolean tryLock()  { return tryAcquire(1); }
public void unlock()      { release(1); }
public boolean isLocked() { return isHeldExclusively(); }

worker是怎么获取任务的?

如果你使用过阻塞队列来实现一个生成消费模型,那么Doug Lea(作者)也是这么做的,下面是一个极简的伪代码,省去了很多其他逻辑,只是让你大致了解.

scss 复制代码
runWorker(){
    while((task = getTask()) != null ){
        // 运行任务
        task.run()
    }
}
​
getTask (){
   // 阻塞直到元素获取返回
   return queue.take()
}

runWorker 执行的位置是 addWorker 方法,当一个Worker创建好后就可以从队列中取任务执行了。所以addWorker 并不是简单的添加worker至workers集合中而是让这个worker开始工作。同时一定要注意 runWorker, getTask 这些方法的调用线程是Worker本身,获取任务中的阻塞操作也是阻塞的worker本身而不是调用线程池的工作线程!

线程池源码分析

状态管理

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0)); ctl的高三位为线程池的状态为,后29位为线程池的worker数量,使用了原子类来保证操作时线程安全,同时2^29-1 的最大worker数对于系统来说远远满足了使用情况

下面的状态值依次递增,状态值占三位,在实际获取判断中低29位补齐0

  1. Running 运行状态 高三位 111 是负数

    1. 线程池初始化后的状态
    2. 此时线程池接收任务,并处理任务
  2. Shutdown 结束 状态 高三位 000

    1. 线程池调用shutdown方法后进入此状态
    2. 已经在队列中的任务会继续执行,但是不会再接收新的任务了
    3. 任务队列为空后将进入整理状态(tidying)
  3. Stop 停止状态 高三位 001

    1. 线程池调用shutdownNow方法后进入此状态
    2. 线程池将立刻停止所有的任务,包括正在执行任务和队列中等待的任务
  4. Tidying 整理状态 高三位 010

    1. SHUTDOWN 转换而来:队列为空且所有 Worker 线程退出。从 STOP 转换而来:所有 Worker 线程退出。
    2. 过渡状态,表示线程池正在清理资源。
    3. 所有任务已终止,Worker 线程数为 0。
    4. 最后执行一个钩子方法terminated
  5. Terminated 终止 状态 高三位 011

    1. 钩子方法执行完毕后进入此状态
    2. 线程池已销毁
状态源码分析
java 复制代码
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
// worker数量的占位 共占29位
private static final int COUNT_BITS = Integer.SIZE - 3;
// worker的最大容量,也就是 2^29-1
private static final int CAPACITY   = (1 << COUNT_BITS) - 1;
​
// xxxx为后29位,每位值为0
// 111xxxx 十进制是 -536870912
private static final int RUNNING    = -1 << COUNT_BITS;
// 000xxx 十进制是 0
private static final int SHUTDOWN   =  0 << COUNT_BITS;
// 001xxx 十进制是 536870912
private static final int STOP       =  1 << COUNT_BITS;
// 010xxx 十进制是 1073741824
private static final int TIDYING    =  2 << COUNT_BITS;
// 111xxx 十进制是 1610612736
private static final int TERMINATED =  3 << COUNT_BITS;

此外还提供了三个方法来快速获取状态

| 方法 | 作用 | 位操作 | 示例输入 | 示例输出 |
|--------------------|----------|-----------------|--------------------|--------------------|-----|
| runStateOf(c) | 提取线程池状态 | c & ~CAPACITY | 0b111...00000101 | 0b111...00000000 |
| workerCountOf(c) | 提取工作线程数 | c & CAPACITY | 0b111...00000101 | 5 |
| ctlOf(rs, wc) | 合并状态和线程数 | `rs | wc` | rs=0, wc= 5 | 5 |

线程池是如何管理非核心线程以及实现非核心线程的保活机制呢, 我们需要观察worker执行流程才能知道
java 复制代码
// 启动worker
final void runWorker(Worker w) {
    try{
        // getTask阻塞worker直到队列有任务或者符合线程失活条件返回null
         while(task = getTask()) != null){
             xxx
             // worker执行任务
             task.run();
             xxx
        }
    }finally{
        // worker退出
        processWorkerExit(w, completedAbruptly);
    }
    
 }
​

上面是一段简化后的主流程代码,我的注释中写道 如果线程失活了则返回null,所以我们需要看getTask()的流程

java 复制代码
​
private Runnable getTask() {
        boolean timedOut = false; // 上一次任务获取是否超过keepAliveTime设定的时间?
​
        for (;;) {
            int c = ctl.get();
            int rs = runStateOf(c);
​
            // 安全检查,如果线程池处于停止及以上的状态,或者任务队列为空则提前结束
            if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
                decrementWorkerCount();
                return null;
            }
​
            int wc = workerCountOf(c);
​
            // 这里的timed代表是否应该开启检测下次任务获取是否会超过keepAliveTime设定的时间?
            boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
            // (timed && timedOut): 如果开启了检测,同时检测还通过了,那么就对workers数量进行cas-1
            // wc > maximumPoolSize 当前线程数大于最大线程数也需要对workers数量进行cas-1
            if ((wc > maximumPoolSize || (timed && timedOut))
                && (wc > 1 || workQueue.isEmpty())) {
                if (compareAndDecrementWorkerCount(c))
                    // 走到这里就代表一个worker失活了,则退出getTask,同时在finally块该worker清除
                    return null;
                continue;
            }
​
            try {
                Runnable r = timed ?
                    // 开启检测,如果这里超过了keepAliveTime 还没拿到任务(代表当前worker空闲了这么久)那么r = null,后面的 timedOut = true; 就会执行,下一次循环就会对workers数量进行cas-1
                    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                    workQueue.take();
                if (r != null)
                    return r;
                timedOut = true;
            } catch (InterruptedException retry) {
                timedOut = false;
            }
        }
    }

从这里能够看出来其实线程池里的核心线程与非核心线程并没有根本上的区别,对于是否回收该worker判断依据是在keepAliveTime内有没有拿到task,以及当前线程池线程数量是否超过了核心线程数

最后getTask 返回null 后 执行 processWorkerExit 清除了该worker

后续还有更多关于线程池的源码剖析,碍于篇幅关系后续整理,同时还有关于线程池的生产问题必坑总结

相关推荐
yngsqq2 小时前
c# —— StringBuilder 类
java·开发语言
Asthenia04123 小时前
浏览器缓存机制深度解析:电商场景下的性能优化实践
后端
星星点点洲3 小时前
【操作幂等和数据一致性】保障业务在MySQL和COS对象存储的一致
java·mysql
xiaolingting3 小时前
JVM层面的JAVA类和实例(Klass-OOP)
java·jvm·oop·klass·instanceklass·class对象
风口上的猪20153 小时前
thingboard告警信息格式美化
java·服务器·前端
专注API从业者4 小时前
分布式电商系统中的API网关架构设计
大数据·数据仓库·分布式·架构
databook4 小时前
『Python底层原理』--Python对象系统探秘
后端·python
追光少年33224 小时前
迭代器模式
java·迭代器模式
超爱吃士力架5 小时前
MySQL 中的回表是什么?
java·后端·面试
扣丁梦想家5 小时前
设计模式教程:装饰器模式(Decorator Pattern)
java·前端·装饰器模式