线程池源码深度剖析

目录

 为什么要有线程池

 ThreadPoolExecutor应用方式

线程池的核心参数

工作线程本质就是一个Worker对象。再究其底层,其实就是thread.start启动的线程。 

线程池的执行流程 (excute()方法的执行过程)

线程池的状态

线程池中的核心属性ctl

线程池的状态变化

线程池的状态

线程池中常用的两个函数

 状态转化

shutdown()

​编辑 

shutdownNow()

tryTerminate() 内部调用

terminated()

excute()源码分析 (线程池的执行流程)

addWorker()源码剖析 (线程池如何添加工作线程?)

工作线程的本质是什么?

runWorker()源码剖析 (工作线程如何执行的任务?)

getTask()源码剖析 (工作线程如何获取新任务?)

线程池参数到底如何设置

  • 为什么要有线程池?
  • Java是实现和管理线程池有哪些方式? 请简单举例如何使用。
  • 为什么很多公司不允许使用Executors去创建线程池? 那么推荐怎么使用呢?
  • ThreadPoolExecutor有哪些核心的配置参数? 请简要说明
  • ThreadPoolExecutor可以创建哪三种线程池呢?
  • 当队列满了并且worker的数量达到maxSize的时候,会怎么样?
  • 说说ThreadPoolExecutor有哪些RejectedExecutionHandler策略? 默认是什么策略?
  • 简要说下线程池的任务执行机制? execute –> addWorker (增加工作线程)–>runworker (getTask) (工作线程执行任务)
  • 线程池中任务是如何提交的?
  • 线程池中任务是如何关闭的?
  • 在配置线程池的时候需要考虑哪些配置因素?
  • 如何监控线程池的状态?

 为什么要有线程池

减少性能开销:如果每次执行异步任务,都去构建一个全新的线程执行,但是任务执行完毕后,线程还要销毁。可以利用线程池,实现资源复用,线程处理完任务,不会立即销毁,而是等待新任务的到来。类似连接池。

可以充分发挥CPU性能:如果每次来任务都直接new,没有办法去把控线程的个数。如果太多了,上下文频繁的切换,性能有损耗。如果太少了,CPU资源没有发挥出来。线程池可以指定好工作线程的个数,充分发挥CPU资源。

监控的效果:如果直接new的话,无法查看线程的个数或者工作的状态。但是采用了线程池之后,本身帮咱们记录很多信息,工作线程个数,有多少个任务在排队,每一个工作线程处理了多少个任务,整个线程池处理了多少个任务

 ThreadPoolExecutor应用方式

手动创建ThreadPoolExecutor

1、只需要构建好ThreadPoolExecutor对象即可,传入指定的7个参数。

2、在执行Runnable任务时,可以直接调用execute方法执行。

3、在执行Callable任务时,需要有返回结果,直接调用submit方法执行。

线程池的核心参数

工作线程本质就是一个Worker对象。再究其底层,其实就是thread.start启动的线程。 

线程池的执行流程 (excute()方法的执行过程)

线程池的状态

线程池中的核心属性ctl

解释一个AtomicInteger是个什么鬼?

1、可以简单的看成,AtomicInteger就是一个int数值。

2、因为线程池中存在并发修改ctl的情况。AtomicInteger内部是基于CAS的方式,对ctl属性做修改。

Ps:CAS其实就是比较和交换,Compare And Swap。基于CPU支持的原语,来保证的原子性。保证修改某一个属性的时候,是原子性的。

ctl在线程池中,维护这两个信息:

  • 工作线程的个数。
  • 线程池的状态。

一个int类型,怎么维护这两个信息?线程池内部,将这个本质是int类型的ctl,拆分为32个bit位。

线程池的状态变化

线程池的状态

线程池中常用的两个函数

 状态转化

线程池需要执行任务时,需要始终保证running状态

shutdown()
shutdownNow()

tryTerminate() 内部调用

terminated()

重写该方法,线程池销毁之后需要执行什么操作

excute()源码分析 (线程池的执行流程)

线程池中的工作线程就是一个个的work对象

工作线程本质就是一个Worker对象。再究其底层,其实就是thread.start启动的线程。

// 任务投递给线程池之后的处理
// command:投递过来的任务
public void execute(Runnable command) {
    // 如果投递的任务为null,直接甩异常。
    if (command == null)
        throw new NullPointerException();
    // 拿到核心属性ctl。
    int c = ctl.get();
    // workerCountOf:在获取工作线程个数
    // 现在的工作线程数 小于 核心线程数
    if (workerCountOf(c) < corePoolSize) {
        // 基于addWorker方法,创建工作线程处理任务command,传递的true,代表构建核心线程
        // addWorker,返回true,创建工作线程成功,反之失败。
        if (addWorker(command, true))
            // 代表创建线程成功,任务已经交给线程池了。return结束了。
            return;
        // 代表创建线程失败了,有情况,重新获取ctl。做后续判断
        c = ctl.get();
    }
    // 任务交给核心线程处理失败了,代码走到这。
    // 线程池状态是RUNNING么?如果是,就尝试将任务扔到工作队列
    // 如果任务成功的投递到了工作队列排队,返回true,反之返回false
    // 返回true,进到if,execute方法结束
    if (isRunning(c) && workQueue.offer(command)) {
        // 代码到这,说明任务已经添加到工作队列
        // 再次获取ctl
        int recheck = ctl.get();
        // 再次判断,线程池状态是不是RUNNING
        // 如果状态不是RUNNING,走remove方法,将任务从工作队列中移除。
        // 移除任务操作,成功返回true,反之false
        if (! isRunning(recheck) && remove(command))
            // 说明任务移除成功,给任务甩一波拒绝策略。
            reject(command);
        // 状态是RUNNING,或者,没拒绝
        // 查看工作线程个数是不是0个。
        else if (workerCountOf(recheck) == 0)
            // 创建一个非核心线程,处理这个任务,毕竟任务饥饿。
            addWorker(null, false);
    }
    // 到这,说明任务没扔到工作队列。
    // 创建非核心线程,处理任务。
    else if (!addWorker(command, false))
        // 如果创建非核心线程失败了,执行拒绝策略
        reject(command);
}

addWorker()源码剖析 (线程池如何添加工作线程?)

1、判断线程池状态是否对劲

2、判断工作线程个数是否对劲

3、对ctl + 1

4、new Worker对象,扔到HashSet里。

5、启动Worker对象里的Thread。就执行了Worker里的run方法。

6、如果启动失败,要做补偿操作。

// 添加工作线程。
// firstTask,是任务
// core,true=核心,false=非核心
private boolean addWorker(Runnable firstTask, boolean core) {
    // =======================做校验,线程池状态,工作线程个数====================================
    retry:
    for (;;) {
        // 线程池状态
        // 获取ctl。
        int c = ctl.get();
        // 基于ctl获取线程池状态
        int rs = runStateOf(c);

        // rs >= SHUTDOWN,如果满足,代表状态不是RUNNING。不是RUNNING不能接收新任务
        if (rs >= SHUTDOWN &&
            // 线程池状态是SHUTDOWN,没有传递任务,并且工作队列不为空
            // 满足上述要求,代表当前要构建的工作线程,是要处理工作队列中的任务的。
            // 如果工作队列有任务,但是没有工作线程在处理。任务饥饿。就会做addWorker(null,false)
            !(rs == SHUTDOWN && firstTask == null && ! workQueue.isEmpty()))
            // 到这,代表当前状态不对劲,不能构建工作线程
            return false;

        for (;;) {
            // 工作线程个数
            // 到这说明状态ok。
            // 拿到工作线程个数。
            int wc = workerCountOf(c);
            // CAPACITY:占用了29个bit位的数值,代表线程池能支撑的最大工作线程个数
            // wc >= CAPACITY:如果满足,代表不能再创建工作线程了,超过最大值了。
            if (wc >= CAPACITY ||
                // 根据core判断,是否超过了核心线程数,或者最大线程数。
                wc >= (core ? corePoolSize : maximumPoolSize))
                // 工作线程数,达到阈值了,进到if,直接结束。
                return false;
            // 状态没问题,个数没问题。
            // 对ctl + 1,如果成功,返回true,如果失败,返回false
            if (compareAndIncrementWorkerCount(c))
                // ctl + 1成功,跳出外层循环,准备走添加工作线程,并启动工作线程
                break retry;
            // 到这,CAS失败了,其他线程把ctl + 1了,ctl肯定改变了。
            // 重新获取ctl
            c = ctl.get();
            // 线程池状态改变了么?
            if (runStateOf(c) != rs)
                // 跳出一次外层for循环
                continue retry;
            // 到这,说明状态没变,走内层for循环,判断工作线程个数就可以了。
        }
    }

  
    // =======================添加工作线程,并启动工作线程====================================
    // 两个标识,默认都是false
    boolean workerStarted = false;
    boolean workerAdded = false;
    // Worker就是工作线程,先声明出来。
    Worker w = null;
    try {
        // new 一个工作线程。会基于咱们自己提供的线程工厂,构建出Thread
        w = new Worker(firstTask);
        // 拿到线程工作构建的thread
        final Thread t = w.thread;
        // thread对象不是null,继续往下走。
        if (t != null) {
            // 拿线程池里的lock锁,加锁。 目的是为了确保HashSet操作的线程安全。
            final ReentrantLock mainLock = this.mainLock;
            mainLock.lock();
            try {
                // 获取线程池的状态
                int rs = runStateOf(ctl.get());
                // 如果是RUNNING,一切正常,接着往下走
                if (rs == RUNNING ||
                    // 解决任务饥饿的情况,代表正常,接着往下走
                    (rs == SHUTDOWN && firstTask == null)) {
                    // 一个Thread对象,连续两次执行start方法,会发生什么?
                    // 如果你的线程工厂,构建的Thread已经start了,直接扔你异常。
                    if (t.isAlive()) 
                        throw new IllegalThreadStateException();
                    // Worker对象扔到HashSet集合里。
                    workers.add(w);
                    // 获取工作线程个数
                    int s = workers.size();
                    // largestPoolSize记录当前线程池中工作线程个数的最大值记录
                    if (s > largestPoolSize)
                        // 如果现在的工作线程数,大于之前的记录,覆盖一下。
                        largestPoolSize = s;
                    // 代表工作线程添加成功。
                    workerAdded = true;
                }
            } finally {
                mainLock.unlock();
            }
            // 添加成功了咩?
            if (workerAdded) {
                // 成功就,启动线程
                t.start();
                // 代表工作线程启动成功。
                workerStarted = true;
            }
        }
    } finally {
        // 如果到这 workerStarted是false,代表工作线程启动失败。
        if (! workerStarted)
            // ctl + 1,   workers.add(w)
            // 从workers里移除worker,然后ctl - 1
            addWorkerFailed(w);
    }
    // 返回结果。
    return workerStarted;
}

工作线程的本质是什么?

工作线程本质就是一个Worker对象。再究其底层,其实就是thread.start启动的线程。

// 省略了部分代码
// Worker实现的Runnable,Worker重新了run方法,run方法就是任务
private final class Worker implements Runnable{
    // thread对象
    final Thread thread;
    // 传递过来的第一个任务
    Runnable firstTask;
    // 当前工作线程,每完成一个任务,就+1
    volatile long completedTasks;

    // new Worker用的有参构造
    Worker(Runnable firstTask) {
        this.firstTask = firstTask;
        this.thread = getThreadFactory().newThread(this);
    }

    // Worker任务。thread对象执行了start方法走,走的是这个run方法。
    public void run() {
        // 工作线程执行任务,走的就是这个方法、
        runWorker(this);
    }
}

runWorker()源码剖析 (工作线程如何执行的任务?)

当线程启动后,会基于Worker对象里的run方法中的runWorker方法执行任务。

1、优先执行Worker携带的firstTask。

2、任务执行前后,会有勾子函数。

3、任务就是基于task.run执行的。

4、如果线程要凉凉,会执行补偿操作,ctl - 1,记录任务完成数,将worker从workers中移除。

// 省略了部分代码。
// 代码走的这,代表已经是另外一个线程了!!!!!这个线程是thread.start启动的。
// 已经是线程池里的工作线程在干活了!!!
final void runWorker(Worker w) {
    // 拿到当前工作线程。
    Thread wt = Thread.currentThread();
    // task是第一个要执行的任务
    Runnable task = w.firstTask;
    // 把worker里的firstTask置为空   help gc
    w.firstTask = null;
    // 一个怪怪的属性!
    boolean completedAbruptly = true;
    try {
        // 第一次循环,如果firstTask不为null,先执行firstTask
        // 第二次循环,task必然为null,就需要走getTask再次获取新的任务执行。
        while (task != null || (task = getTask()) != null) {
            try {
                // 任务前的勾子函数
                beforeExecute(wt, task);
                Throwable thrown = null;
                try {
                    // 执行任务
                    task.run();
                } catch (Throwable x) {
                    thrown = x; 
                    throw new Error(x);
                } finally {
                    // 任务后的勾子函数
                    afterExecute(task, thrown);
                }
            } finally {
                // 任务设置为null
                task = null;
                // 当前工作线程处理的任务个数 + 1
                w.completedTasks++;
            }
        }
        // 如果执行任务出现了异常,这行代码走么?
        completedAbruptly = false;
    } finally {
        // 如果任务出现异常,需要走这个方法。
        // 工作线程凉凉之前,做的处理
        processWorkerExit(w, completedAbruptly);
    }
}

Ps:如果工作线程在执行任务时,异常抛出来了,这个工作线程会怎么样?
如果task.run将异常抛出来了,会抛到runWorker方法,runWorker方法会将异常抛给run方法。
run方法异常结束。run方法结束,线程凉凉。
如果任务是基于FutureTask执行的,异常会被FutureTask捕获,线程就不会结束。

// 工作线程凉凉之前,做的操作
private void processWorkerExit(Worker w, boolean completedAbruptly) {
    // 如果没对ctl - 1,在这做个-1操作。
    if (completedAbruptly) 
        decrementWorkerCount();

    // 加锁,记录当前工作线程干了多少活,同时将worker对象从Workers中移除。
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        completedTaskCount += w.completedTasks;
        workers.remove(w);
    } finally {
        mainLock.unlock();
    }
}

getTask()源码剖析 (工作线程如何获取新任务?)

如果工作线程完成了firstTask,后续的任务,就需要基于getTask从工作队列中获取。

1、判断线程池状态是否正常。

2、判断工作线程个数,但是第一次循环进不去

3、在工作队列的位置,尝试获取任务,可能是poll,可能是take。

4、如果拿到任务,回到runWorker执行

5、如果没拿到任务,重新走for循环,在判断工作线程个数时,可能就会进入到if中,结束当前线程。

// getTask方法获取新任务执行。
private Runnable getTask() {
    // timeOut,刚开始肯定是false。
    boolean timedOut = false; 

    // 死循环。
    for (;;) {
        // 这两行看不懂面壁。。
        int c = ctl.get();
        int rs = runStateOf(c);

        // 判断线程池状态
        // 状态是SHUTDOWN,并且队列没任务。
        if (rs == SHUTDOWN && workQueue.isEmpty()) 
        // 状态是STOP,啥也不管,直接破产。
        if (rs >= STOP)
        if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
            // ctl - 1,工作线程准备凉凉……
            decrementWorkerCount();
            // 返回null,代表没任务了。
            return null;
        }

        // 判断工作线程个数问题
        // 拿到工作线程个数
        int wc = workerCountOf(c);
        // allowCoreThreadTimeOut:核心线程是否允许超时,默认是false,如果为true,
                                                                    基于最大空闲时间走
        // wc > corePoolSize:如果满足,代表现在有非核心线程在。
        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

        // wc > maximumPoolSize:不会满足,理解就是一个健壮性判断,妹啥用。。。
        // timed可以在第一次循环时为true,但是timeOut必然是false。第一次循环进不来,先不看!
        // 第二次循环,timeOut可能为true了,第一个判断为null
        if ((wc > maximumPoolSize || (timed && timedOut))
            // wc > 1,至少是两个,即便工作有任务,也不会出现任务饥饿问题。
            // workQueue.isEmpty(),工作队列为空,肯定没有任务饥饿问题。
            && (wc > 1 || workQueue.isEmpty())) {
            // CAS的方式,将ctl - 1,只要-1成功,直接返回null
            if (compareAndDecrementWorkerCount(c))
                return null;
            // 如果CAS失败,说明有并发,重新走for循环。
            continue;
        }


        try {
            // timed为true,走工作队列的poll方法,拿任务,等keepAliveTime时间,超过了,返回null
            // timed为false,都是核心线程,执行take方法,死等,没任务就在这死等。等到有任务
            Runnable r = timed ?
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                workQueue.take();
            // 如果r不为null,拿到任务了,回到runWorker继续执行任务
            if (r != null)
                return r;
            // 到这,说明等了一会,没拿到任务,先把timedOut设置为true
            timedOut = true;
        } catch (InterruptedException retry) {
            timedOut = false;
        }
    }
}

线程池参数到底如何设置?

核心线程数,这个是最重要的。

第一个维度,要考虑这个线程池处理的任务类型是什么,是IO密集还是CPU密集。

CPU密集:需要CPU一直分配给这个线程时间片,程序一直执行。

IO密集:查询数据库,查询三方服务,涉及到磁盘IO或者网络IO比较多的情况。

第二个维度,需要考虑你服务器硬件的情况,CPU是多少核心的。

如果任务是CPU密集,核心线程数,设置到跟CPU核心数差不多即可。±1差不多。

如果任务是IO密集,IO密集的情况可能不太一样的,有的是去Redis查询,挺快,有的是去MySQL查询,有点慢,IO密集情况不用,核心线程数都会受到影响。核心线程数需要设置的比较多。

阻塞队列

阻塞队列和一般的队列在设计上有一个重要的区别

阻塞队列能够在特定条件下阻塞操作的线程,如当队列满时阻塞生产者线程,或当队列空时阻塞消费者线程。这种阻塞机制使得阻塞队列在多线程环境中更为适合,特别是在线程池的任务调度中,阻塞队列可以保证线程间的协调和同步,从而避免任务丢失或争抢资源导致的错误。

  • 当队列为空时:如果有线程尝试从队列中取元素,阻塞队列会阻塞该线程,直到有其他线程向队列中放入新的元素,才会解除阻塞,允许线程继续操作。

  • 当队列已满时:如果有线程尝试向队列中放入元素,阻塞队列会阻塞该线程,直到队列中有空位(例如消费者线程消费了队列中的元素),然后解除阻塞并允许放入元素。

LinkedBlockingQueue和ArrayBlockingQueue区别

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值