JAVA ThreadPoolExecutor线程池

深入分析ThreadPoolExecutor的内部机制,包括Worker线程、任务队列、状态管理、任务提交流程及线程池关闭过程。探讨如何合理配置线程池参数以优化CPU密集型和I/O密集型任务。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

部分资料来自网络

https://www.cnblogs.com/fixzd/p/9253203.html

ThreadPoolExecutor源码剖析_threadpoolexecutor源码分析_niulx111的博客-优快云博客

设计

ThreadPoolExecutor中存在内部类Worker,线程池通过CAS控制Worker数量, HashSet<Worker>保存这些Worker对象。Worker类实现Runnable接口,Worker对象就是实际的任务执行者,同时也代表一个独立Thread线程对象。Worker=Thread, 每一个Worker对象在实例化时通过ThreadFactory创建一个自己专属的Thread线程对象。这个专属线程start启动后执行这个Worker对象的run方法。Worker的run方法执行从BlockingQueue<Runnable>中取得任务,并执行任务runnable.run方法。通过Queue实现业务线程到线程池线程的切换,线程池工作线程的挂起与唤醒依靠BlockingQueue完成。

BlockingQueue阻塞队列

ArrayBlockingQueue:数组实现的有界队列。生产者消费者共用一把锁。

LinkedBlockingQueue:链表实现的有/无界队列。生产者消费者分别使用两把锁。性能好于ArrayBlockingQueue。

DelayQueue:延迟队列。

PriorityBlockingQueue:优先级队列。

注意

1 、不要用无限队列,应为可能造成大量任务堆积,内存无法释放

2 、死锁。例如:核心线程10,队列100。全部核心线程都执行任务A,A等待任务B执行唤醒,而任务B全部在队列中。此时就会死锁。

任务投递流程

投递核心线程---->放入队列---->创建非核心线程---->拒绝策略

状态

状态由整形变量表示,由大到小依次是: 

RUNNING:接受新的任务,处理队列任务; 
SHUTDOWN:不在接受新的任务,处理队列任务; 
STOP:不在接受新任务,不处理队列任务,中断正在执行的任务线程。shutDownNow(); 
TIDYING:所有的任务已经结束,任务线程为0,线程池转换到TIDYING; 
TERMINATED:停止,线程池已结束,且无任务在执行,即terminated()方法执行完。

submit方法执行逻辑

任务提交到线程池后先交给核心线程,若无空闲则放入队列中,若队列已满则尝试增加线程数到最大值。若以上都不能成功,则启动拒绝策略。

 

public Future<?> submit(Runnable task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<Void> ftask = newTaskFor(task, null);
        execute(ftask);
        return ftask;
}

public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
          /*
         *  3步操作
         *
         * 1. 如果当前运行的线程数<核心线程数,创建一个新的线程执行任务,调用addWorker方法原子性地检查
         *    运行状态和线程数,通过返回false防止不需要的时候添加线程
         * 2. 如果一个任务能够成功的入队,仍然需要双重检查,因为我们添加了一个线程(有可能这个线程在上次检查后就已经死亡了)
         *    或者进入此方法的时候调用了shutdown,所以需要重新检查线程池的状态,如果必要的话,当停止的时候要回滚入队操作,
         *    或者当线程池为空的话创建一个新的线程
         * 3. 如果不能入队,尝试着开启一个新的线程,如果开启失败,说明线程池已经是shutdown状态或饱和了,所以拒绝执行该任务
*/
        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) { //工作线程线程数少于核心数
            if (addWorker(command, true))   //增加工作线程到HashSet<Worker>
                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);   //增加工作线程到HashSet<Worker>
        }
        else if (!addWorker(command, false)) //增加工作线程到HashSet<Worker>
            reject(command); //失败 拒绝
}

addWorker方法

添加工作线程。

第一步:    判断线程池状态,for+cas 增加线程个数;

第二步: 在第一步成功后实例化Workder(Thread,Runable),持锁将新workder放入Set中,启动workder中线程t.start()。

第一部分主要是两个循环,外层循环主要是判断线程池状态。内层循环判断当前线程数量,CAS增加数量。

第二部分说明CAS成功了也就是说线程个数加一了,但是现在任务还没开始执行,这里使用全局的独占锁来控制Hashset<Worker> workers里面添加任务,其实也可以使用并发安全的set,但是性能没有独占锁好(这个从注释中知道的)。这里需要注意的是要在获取锁后重新检查线程池的状态(双重检查),这是因为其他线程可能在本方法获取锁前改变了线程池的状态,比如调用了shutdown方法。添加成功则启动任务执行。 

private boolean addWorker(Runnable firstTask, boolean core) {
    retry:
    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);

        // 检查当前线程池状态是否是SHUTDOWN、STOP、TIDYING或者TERMINATED
        // 且!(当前状态为SHUTDOWN、且传入的任务为null,且队列不为null)
        // 条件都成立则返回false
        if (rs >= SHUTDOWN &&
            ! (rs == SHUTDOWN &&
               firstTask == null &&
               ! workQueue.isEmpty()))
            return false;
        //循环
        for (;;) {
            int wc = workerCountOf(c);
            //如果当前的线程数量超过最大容量或者大于(根据传入的core决定是核心线程数还是最大线程数)核心线程数 || 最大线程数,则返回false
            if (wc >= CAPACITY ||
                wc >= (core ? corePoolSize : maximumPoolSize))
                return false;
            //CAS增加c,成功则跳出retry
            if (compareAndIncrementWorkerCount(c))
                break retry;
            //CAS失败执行下面方法,查看当前线程数是否变化,变化则继续retry循环,没变化则继续内部循环
            c = ctl.get();  // Re-read ctl
            if (runStateOf(c) != rs)
                continue retry;
        }
    }
    //CAS更新线程数成功后创建一个线程worker
    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 {               
                //重新检查线程池状态
                //避免ThreadFactory退出故障或者在锁获取前线程池被关闭
                int rs = runStateOf(ctl.get());

                if (rs < SHUTDOWN ||
                    (rs == SHUTDOWN && firstTask == null)) {
                    if (t.isAlive()) // 先检查线程是否是可启动的
                        throw new IllegalThreadStateException();
                    workers.add(w);
                    int s = workers.size();
                    if (s > largestPoolSize)
                        largestPoolSize = s;
                    workerAdded = true;
                }
            } finally {
                mainLock.unlock();
            }
            //判断worker是否添加成功,成功则启动线程,然后将workerStarted设置为true
            if (workerAdded) {
                //启动线程worker.thread 
                t.start();
                workerStarted = true;
            }
        }
    } finally {
        //判断线程有没有启动成功,没有则调用addWorkerFailed方法
        if (! workerStarted)
            addWorkerFailed(w);
    }
    return workerStarted;
}

Worker对象

每个Worker对象持有一个私有Thread对象。Worker是定义在ThreadPoolExecutor中的finnal类,其中继承了AbstractQueuedSynchronizer类和实现Runnable接口,其中的run方法如下。线程启动时调用了其run方法。

public void run() {
    runWorker(this);
}

runWorker方法

worker.Thread会循环从queue中取得任务执行其run()方法。

worker线程终止:

1 若执行任务代码时捕获异常则向上抛出,终止循环。

2 getTask()返回null,终止while循环。既队列中无任务或线程总数超标。

最终进入finally{}执行worker.Thread退出,当前worker.Thread不再执行循环,自然终止。

final void runWorker(Worker w) {
    Thread wt = Thread.currentThread(); //workerThread
    Runnable task = w.firstTask;
    w.firstTask = null;
    w.unlock();
    boolean completedAbruptly = true;
    try {
        //循环获取任务,getTask()返回null时不再循环,线程自然终止。
        //getTask()从Queue中取任务,会被阻塞
        while (task != null || (task = getTask()) != null) {
            w.lock();
            // 当线程池是处于STOP状态或者TIDYING、TERMINATED状态时,设置当前线程处于中断状态。防止线程池调用了shutdownnow方法必须中断所有的线程
            // 如果不是,当前线程就处于RUNNING或者SHUTDOWN状态,确保当前线程不处于中断状态
            // 重新检查当前线程池的状态是否大于等于STOP状态
            if ((runStateAtLeast(ctl.get(), STOP) ||
                 (Thread.interrupted() && runStateAtLeast(ctl.get(), STOP))) &&
                !wt.isInterrupted())
                //当前线程打中断标志
                wt.interrupt();

            try {
                //提供给继承类使用做一些统计之类的事情,在线程运行前调用
                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(task, thrown);
                }
            } finally {
                task = null;
                //统计当前worker完成了多少个任务
                w.completedTasks++;
                w.unlock();
            }
        }
        completedAbruptly = false;
    } finally {
        //当前线程不再执行while循环或throw Exception时,会执行线程退出操作
        //workers中清除当前worker,统计整个线程池完成的任务个数之类的工作
        //最终当前线程不再循环,代码走到尽头自然终止。
        processWorkerExit(w, completedAbruptly);
    }
}

getTask方法

从队列中取得任务,可以通过阻塞队列控制线程的挂起/唤醒。配置了keepAliveTime则queue取任务时等待keepAliveTime时间,没有配置则非阻塞从queue取任务。

队列中无任务或线程总数超标时返回null。返回null代码就会退出while循环,执行runWorker()中的processWorkerExit(),当前线程生命走到尽头。

private Runnable getTask() {
    boolean timedOut = false; // Did the last poll() time out?
    //循环
    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);

        //线程线程池状态和队列是否为空
        if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
            //减线程数
            decrementWorkerCount();
            return null;
        }
        //线程数量
        int wc = workerCountOf(c);
        
        //判断当前线程在阻塞队列获取任务时,是否需要定时唤醒
        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

        //(当前线程数是否大于最大线程数或者)
        //且(线程数大于1或者任务队列为空)
        //这里有个问题(timed && timedOut)timedOut = false,好像(timed && timedOut)一直都是false吧
        if ((wc > maximumPoolSize || (timed && timedOut))
            && (wc > 1 || workQueue.isEmpty())) {
            if (compareAndDecrementWorkerCount(c))
                return null; //CAS减少Worker数,返回null。当前线程会自然终止。
            continue;
        }

        try {
            //获取任务 阻塞线程
            Runnable r = timed ?
                //如果timed==true,线程阻塞等待一定时间
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                //否则 阻塞取任务
                workQueue.take();
            if (r != null)
                return r;
            //超时没有取到任务,继续for
            timedOut = true;
        } catch (InterruptedException retry) {
            timedOut = false;
        }
    }
}

关闭线程池

可以通过调用线程池的shutdown()shutdownNow()方法来关闭线程池。他们的原理是遍历线程池中的工作线程,然后遍历Worker线程调用interrupt方法来中断,所以代码中没有处理interrupt中断状态的任务可能永远无法停止。只要调用了这两个关闭方法的一个,isShutdown()就会返回true。当所有的任务都关闭后,才表示线程池关闭成功,这是调用isTerminated()方法会返回true。shutdown方法只是发出了停止信号,等所有任务执行完毕会关闭线程池;而shutdownNow则是立即停止所有任务,返回未执行任务List<Runable>(但是已执行未完成被终止的任务不包含),调用isTerminated()方法会返回true。

isTerminated当调用shutdown()方法后,并且所有提交的任务完成后返回为true;
isTerminated当调用shutdownNow()方法后,成功停止后返回为true;

线程数配置

分布式时要考虑多机生产消费能力和通讯消耗。 

最低线程数 : 依据生产者的每秒平均生产能力、单一线程每秒的平均消费能力。

最高线程数 : 依据生产者的每秒可能的峰值生产能力 、单一线程每秒的可能的最低/平均消费能力。 

缓冲线程数:  依据情况而定

1)   CPU密集型任务的线程池配置

因为cpu执行速度非常快,往往切换线程上下文环境所耗费的时间比执行代码花费的时间更长,所以CPU密集型任务的线程数应该尽量保持和CPU核数一致,以减少线程切换带来的性能损耗。

2)   IO密集型任务的线程池配置

IO密集型任务的主要性能瓶颈在于等待IO结果,而CPU利用率比较低,当遇到任务中存在从DB中读取数据、从缓存中读取数据时、远程通信等,就可以认为是IO密集型任务。一般而言业务系统配置线程池基本上都会采用上边的模型配置。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值