线程池用法及原理

一、线程池的用法

简单用法,这里写一个简单的demo

java 复制代码
// 线程池
public static void executePool() throws ExecutionException, InterruptedException {

    // 手动创建线程池
    ThreadPoolExecutor executor = new ThreadPoolExecutor(
            4,
            4,
            100, // 非核心线程空闲时间
            TimeUnit.MILLISECONDS, // 超时单位
            new LinkedBlockingQueue<>(1000),
            new ThreadFactory() { // 线程工厂
                @Override
                public Thread newThread(Runnable r) {
                    Thread thread = new Thread(r);
                    return thread;
                }
            },
            new ThreadPoolExecutor.AbortPolicy() // 拒绝策略
    );

    // 执行任务,没有返回值的任务
    for (int i = 0; i < 5; i++) {
        executor.execute(() -> {
            System.out.println("没有返回值!!!" + Thread.currentThread());
        });
    }
    
    // 执行任务,有返回值的任务
    String result = executor.submit(() -> {
        return "100";
    }).get();
    System.out.println(result);

}

线程池线程大小的设置可以从两方面来说

CPU密集、IO 密集

CPU 密集:核心线程数:CPU 核心数 + 1 【CPU 一直在执行指令,进行大量计算】

IO 密集:核心线程数:CPU 内核数 * 2 【CPU 一直在做 IO 调用】

一般业务上,不会按这个走,业务上有不同的任务,不同的任务用不同的线程池,可能一个服务里面有多个线程池

二、线程池的核心属性

java 复制代码
// AtomicInteger ,就是一个int,用 CAS 来保证原子性
// 高三位表示线程池状态 
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

// 29
private static final int COUNT_BITS = Integer.SIZE - 3;

// 工作线程最大个数
private static final int COUNT_MASK = (1 << COUNT_BITS) - 1;

// 线程池的几个状态
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;

// 获取线程池状态
private static int runStateOf(int c)     { return c & ~COUNT_MASK; }
// 获取工作线程个数
private static int workerCountOf(int c)  { return c & COUNT_MASK; }

三、线程池状态流转

四、excute方法

线程池处理任务的流程,当新任务来了是先放队列还是开辟新的线程处理,都在这个逻辑里

scss 复制代码
public void execute(Runnable command) {
    // 执行的任务为空,抛异常
    if (command == null){
        throw new NullPointerException();
    }
    // 拿到 ctl
    int c = ctl.get();
    // 通过c获取工作线程数 判断 是否小于 核心线程数
    if (workerCountOf(c) < corePoolSize) {
        // addWorker() 第二个参数 含义:true : 核心线程 false : 代表非核心线程
        if (addWorker(command, true)){
            // 添加核心线程执行任务成功 return
            return;
        }
        // 添加失败 重新获取 ctl 的值
        c = ctl.get();
    }
    // 判断线程池状态 < Shutdown 
    // 线程状态为 Running状态的同时,将任务添加进队列 
    if (isRunning(c) && workQueue.offer(command)) {
        // doublecheck 再次获取 ctl值
        int recheck = ctl.get();
        // 线程池状态非 Running状态 从工作队列移除掉任务
        if (!isRunning(recheck) && remove(command)){
            // 走一波拒绝策略
            reject(command);
        }else if (workerCountOf(recheck) == 0){ // 线程池状态是 Running -> 线程池的工作线程数为 0 进这个if分支
            // 进这个if还有一个说法:可以将核心线程数设置为0,所有的工作线程都是非核心线程
            // 情况二:核心线程数可以通过keepAlive 来销毁,如果此时核心线程刚好被销毁,工作线程可能为 0
            // 添加一个非核心线程的空任务 去处理工作队列中的任务
            addWorker(null, false);
        }
    }else if (!addWorker(command, false)){ // 启动非核心线程去处理工作队列里的任务失败 ---> 线程满了
        // 根据上下条件判断:什么时候进行这个if的条件判断 要么线程池状态不是 Running , 要么 任务添加到队列失败

        // 走一波拒绝策略
        reject(command);
    }
}

五、addWorker添加工作线程并处理任务

从这个方法就可以看出,一个线程就是一个 Worker

这个方法分为两大部分:

1、CAS 自增 ctl 工作线程数

2、步骤一成功:new 一个 Worker 处理任务

java 复制代码
/**
* firstTask : 任务
* core : 是否是核心线程
*/
private boolean addWorker(Runnable firstTask, boolean core) {
    // 下面这个两层的for循环 --> 给ctl线程数 + 1
    retry:
    for (int c = ctl.get();;) { // 外层循环 --> 校验线程池状态
        // 边界校验  线程池状态不是runing 且 [线程池状态至少是 STOP  或者 传进来的任务不是null 或者 任务队列是空]
        if (runStateAtLeast(c, SHUTDOWN)
            && (runStateAtLeast(c, STOP)
                || firstTask != null
                || workQueue.isEmpty())){
                     // 不满足创建新线程的条件 ,直接return
                     return false;
                }

        for (;;) { // 内层循环:尝试增加工作线程数

            // 工作线程数是否超过阈值(核心线程数 / 最大线程数)
            if (workerCountOf(c)
                >= ((core ? corePoolSize : maximumPoolSize) & COUNT_MASK)){
                    // 线程数已达上限,不能再新增
                    return false;
                }
            // CAS 工作线程数自增
            if (compareAndIncrementWorkerCount(c))
                // 跳出外层循环
                break retry;
            
            // CAS 失败,重新获取ctl
            c = ctl.get(); 
            if (runStateAtLeast(c, SHUTDOWN)) // 线程池状态至少是 ShutDown
                continue retry; // 继续下一次外层循环,重新进行校验

            // 否则 重试 CAS 自增线程数
        }
    }

    // 这一部分 :启动线程处理任务
    boolean workerStarted = false; // 工作线程是否启动
    boolean workerAdded = false; // Worker是否成功加入线程池的管理集合
    Worker w = null; // 工作线程默认为null
    try {
        // new 一个工作线程出来
        w = new Worker(firstTask);
        // 拿到thread
        final Thread t = w.thread;
        if (t != null) { // 除非自己声明的线程工厂返回的是 null ,否则一定不为 null
            // 获取线程池的全局锁
            final ReentrantLock mainLock = this.mainLock;
            // 之所以要获取锁资源:因为在启动这个线程时,避免线程池状态发生变化 --> 线程池调用shutDown方法时也会获取这个锁
            mainLock.lock();
            try {
                // 重新获取ctl
                int c = ctl.get();
                // 判断线程池状态
                // 场景1:线程池处于RUNNING状态 → 无条件允许
                // 场景2:线程池状态<STOP(即SHUTDOWN)且无初始任务 → 允许创建Worker处理队列残留任务
                if (isRunning(c) || (runStateLessThan(c, STOP) && firstTask == null)) {

                    // 如果thread不是 new 已经被启动 抛异常
                    if (t.getState() != Thread.State.NEW){
                        throw new IllegalThreadStateException();
                    }
                    // 将工作线程添加到HashSet中  统一管理  
                    workers.add(w);
                    // 工作线程添加成功
                    workerAdded = true;
                    // 获取工作线程个数,判断是否需要修改最大工作线程数记录【监控用】
                    int s = workers.size();
                    if (s > largestPoolSize){
                        largestPoolSize = s;
                    }
                }
            } finally {
                // finally 里解锁
                mainLock.unlock();
            }
            if (workerAdded) { // 判断工作线程是否添加成功
                // 启动线程 实际是调用t.start 来启动线程
                container.start(t);
                // 线程启动成功
                workerStarted = true;
            }
        }
    } finally {
        // 判断工作线程是否启动成功
        if (! workerStarted){
            // 启动失败,走这 新增工作线程失败
            addWorkerFailed(w);
        }
    }
    return workerStarted;
}



// Worker 继承了 AQS ,构造器如下
Worker(Runnable firstTask) {
    setState(-1); 
    this.firstTask = firstTask;
    // 线程工厂获取新线程
    this.thread = getThreadFactory().newThread(this);
}

shutDown的时候也会获取全局锁

添加工作线程时,之所以要加锁,就是为了防止并发时,线程池的状态被修改

六、runWorker方法

在addWorker时,在最终启动线程时,会调用t.start()方法,会执行 Worker里的run()方法,因为 Worker实现了 Runnable接口,重写了run方法,最终会走到worker里的run方法。

csharp 复制代码
public void run() {
    // 调用runWorker
    runWorker(this);
}
scss 复制代码
// worker传进来的是this, 当前对象
final void runWorker(Worker w) {
    // 拿到当前线程
    Thread wt = Thread.currentThread();
    // 拿到当前携带的第一个任务
    Runnable task = w.firstTask;
    // 将worker中的任务清空,避免重复执行
    w.firstTask = null;
    w.unlock(); // 解锁
    // 是否异常完成,默认为true 表示 任务执行异常
    boolean completedAbruptly = true; 
    try {
        // 这是一个循环,task不为空 或者 能从队列里拿到任务,就一直执行
        while (task != null || (task = getTask()) != null) {
            // 加锁开始执行 --> 防止线程池停止时中断执行的任务
            w.lock();
            // 再来一个校验 : 线程池状态至少是 Stop ,但是当前线程还没被中断  就得执行中断
            // 线程池 STop 状态,停止执行任何任务
            if ((runStateAtLeast(ctl.get(), STOP) || (Thread.interrupted() && runStateAtLeast(ctl.get(), STOP))) && !wt.isInterrupted())
                // 当前线程要中断
                wt.interrupt();
            try {
                // 前置钩子 --> 让自己实现
                beforeExecute(wt, task);
                try {
                    // 执行任务
                    task.run();

                    // 后置钩子 --> 让自己实现
                    afterExecute(task, null);
                } catch (Throwable ex) {
                    afterExecute(task, ex);
                    throw ex;
                }
            } finally {
                task = null;
                // 当前任务完成,就++ 统计完成了多少个任务
                w.completedTasks++;
                // 解锁
                w.unlock();
            }
        }
        // 是否出现异常完成 --> false 正常完成
        completedAbruptly = false;
    } finally {
        // 执行线程退出
        processWorkerExit(w, completedAbruptly);
    }
}

线程池线程复用的关键也在这块:worker启动之后,会一直从队列里取任务,直到没有任务可取才会被销毁。

七、getTask()取任务

再接着分析getTask从工作队列里取任务是怎么取的

java 复制代码
private Runnable getTask() {
    // 是否超时
    boolean timedOut = false; 

    for (;;) {
        // 获取ctl值
        int c = ctl.get();

        // 线程池状态至少为 ShutDown 并且队列已经空了
        if (runStateAtLeast(c, SHUTDOWN)
            && (runStateAtLeast(c, STOP) || workQueue.isEmpty())) {
            // 对工作线程个数减一
            decrementWorkerCount();
            return null;
        }
        // 获取工作线程个数
        int wc = workerCountOf(c);

        // 可超时回收标记:如果工作线程个数 大于 核心线程数 允许超时  --> 说明是非核心线程
        // allowCoreThreadTimeOut --> 默认false 允许核心线程数超时退出
        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

        // 条件一:工作线程数 大于 最大线程数 或者 非核心线程超时了
        // 条件二:工作线程数大于 1 但是 队列空了
        // 两个条件同时满足
        if ((wc > maximumPoolSize || (timed && timedOut)) && (wc > 1 || workQueue.isEmpty())) {
            // 对工作线程数减一 直接return
            if (compareAndDecrementWorkerCount(c))
                return null;
            // CAS 失败,continue自旋
            continue;
        }

        try {
            // 根据time来判断是否可超时回收
            // 如果可以超时回收 ,就自己从队列里拉,等待keepAliveTime后仍无任务返回null
            // 不可超时回收,调用take,阻塞等待直到拿到任务(核心线程默认行为)
            Runnable r = timed ? workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : workQueue.take();
            if (r != null){
                // 从队列里拿到任务 返回
                return r;
            }
            timedOut = true; // 超时没有拿到,标记为true 下次循环触发退出校验
        } catch (InterruptedException retry) {
            // 捕获中断异常,中断不算超时,重置标记,重新循环尝试获取任务
            timedOut = false;
        }
    }
}

这个方法整体分为三大块:

一:线程池状态判断 和 任务队列是否为空判断 --> 提前校验

二:判断是否可以超时回收:获取工作线程个数,判断是否是核心线程,核心线程不可超时回收,非核心线程可以超时回收

三:根据是否可以超时回收:非核心线程用poll拉 标记keepAliveTime ,到了时间还没拿到任务返回null。核心线程用take,阻塞等待,直到拿到任务

八、processWorkerExit执行线程退出

scss 复制代码
 // w --> worker工作线程
 // completedAbruptly --> false正常完成任务 true非正常完成任务
 private void processWorkerExit(Worker w, boolean completedAbruptly) {
        // 判断是否正常完成任务
        if (completedAbruptly){
            // 非正常完成任务 工作线程数减一
            decrementWorkerCount();
        } 
            
        // 获取全局锁
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            // 统计累计完成的任务数
            completedTaskCount += w.completedTasks;
            // 从hashSet中移除掉worker 
            workers.remove(w);
        } finally {
            mainLock.unlock();
        }

        // 尝试终止线程池,线程池状态是否发生变化
        tryTerminate();

        int c = ctl.get();
        // 线程池的状态小于 Stop 
        if (runStateLessThan(c, STOP)) {
            // 任务正常完成
            if (!completedAbruptly) {
                // 判断 队列是否为空,如果队列不为空,判断工作线程是否大于等于 1 如果还有工作线程直接return
                int min = allowCoreThreadTimeOut ? 0 : corePoolSize; // 如果核心线程可以超时,那么整个线程池就没有线程可用
                if (min == 0 && ! workQueue.isEmpty())
                    min = 1;
                if (workerCountOf(c) >= min) // 如果还有核心线程在阻塞,直接return
                    return; 
            }
            // 如果工作线程为空
            // 添加一个非核心空任务的线程来处理队列里的任务
            addWorker(null, false);
        }
}

线程退出这里分为两大部分:

部分一:从维护的全局 HashSet里移除掉worker + 尝试终止线程池

部分二:线程池的状态如果是 Running/ShutDown,判断任务队列是否为空,不为空,核心线程在阻塞着,return,用核心线程处理,工作线程数为空,添加一个空任务的非核心线程去处理队列里的任务

九、拒绝策略

就这四个拒绝策略

9.1、AbortPolicy

抛出异常

java 复制代码
   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());
        }
    }

9.2、DiscardPolicy

什么也不做,也不抛出异常

java 复制代码
   public static class DiscardPolicy implements RejectedExecutionHandler {
        /**
         * Creates a {@code DiscardPolicy}.
         */
        public DiscardPolicy() { }

        /**
         * Does nothing, which has the effect of discarding task r.
         *
         * @param r the runnable task requested to be executed
         * @param e the executor attempting to execute this task
         */
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        }
    }

9.3、DiscardOldestPolicy

从队列里边扔出一个,把当前任务给执行掉

java 复制代码
public static class DiscardOldestPolicy implements RejectedExecutionHandler {
        /**
         * Creates a {@code DiscardOldestPolicy} for the given executor.
         */
        public DiscardOldestPolicy() { }

        /**
         * Obtains and ignores the next task that the executor
         * would otherwise execute, if one is immediately available,
         * and then retries execution of task r, unless the executor
         * is shut down, in which case task r is instead discarded.
         *
         * @param r the runnable task requested to be executed
         * @param e the executor attempting to execute this task
         */
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (!e.isShutdown()) {
                e.getQueue().poll();
                e.execute(r);
            }
        }
    }

9.4、CallerRunsPolicy

谁提交谁执行

java 复制代码
public static class CallerRunsPolicy implements RejectedExecutionHandler {
        /**
         * Creates a {@code CallerRunsPolicy}.
         */
        public CallerRunsPolicy() { }

        /**
         * Executes task r in the caller's thread, unless the executor
         * has been shut down, in which case the task is discarded.
         *
         * @param r the runnable task requested to be executed
         * @param e the executor attempting to execute this task
         */
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (!e.isShutdown()) {
                r.run();
            }
        }
    }

十、总结

线程池的源码看完还是很混乱,里面有太多的线程池状态判断以及 Double Check,影响主思路的梳理,现在写一下核心流程:

相关推荐
用户2190326527352 小时前
Spring Boot + Redis 注解极简教程:5分钟搞定CRUD操作
java·后端
计算机学姐2 小时前
基于php的旅游景点预约门票管理系统
开发语言·后端·mysql·php·phpstorm
用户908324602732 小时前
SpringBoot集成DeepSeek
后端
无限大62 小时前
为什么"云计算"能改变世界?——从本地计算到云端服务
后端
哈哈老师啊2 小时前
Springboot校园订餐管理系统k2pr7(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
数据库·spring boot·后端
喵叔哟2 小时前
10.消息队列集成
后端·服务发现
残花月伴2 小时前
天机学堂-day4(高并发优化方案)
java·spring boot·后端
tonydf3 小时前
在Blazor项目里构造一个覆盖面广泛的权限组件
后端
阿杰AJie3 小时前
Docker 常用镜像启动参数对照表
后端