当面试官问线程池时,你应该知道些什么?

Java面试中,线程池也算是一个高频的问题,其实就JDK源码来看线程池这一块的实现代码应该算是写的清晰易懂的,通过这篇文章,我们就来盘点一下线程池的知识点。

本文基于JDK1.8源码进行分析

首先看下线程池构造函数:

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        //忽略赋值与校验逻辑
    }

构造参数比较多,一个一个说下:

  • corePoolSize线程池中的核心线程数
  • maximumPoolSize线程池中的最大线程数
  • keepAliveTime线程池中的线程存活时间(准确来说应该是没有任务执行时的回收时间,后面会分析)
  • unit时间单位
  • workQueue来不及执行的任务存放的阻塞队列
  • threadFactory新建woker线程(注意不是我们提交的任务)是进行一些属性设置,比如线程名,优先级等等,有默认实现。
  • handler 任务拒绝策略,当运行线程数已达到maximumPoolSize,队列也已经装满时会调用该参数拒绝任务,有默认实现。

当我们向线程池提交任务时,通常使用execute方法,接下来就先从该方法开始分析。

在分析execute代码之前,需要先说明下,我们都知道线程池是维护了一批线程来处理用户提交的任务,达到线程复用的目的,线程池维护的这批线程被封装成了Worker

public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        //JDK8的源码中,线程池本身的状态跟worker数量使用同一个变量ctl来维护
        int c = ctl.get();
        //通过位运算得出当然线程池中的worker数量与构造参数corePoolSize进行比较
        if (workerCountOf(c) < corePoolSize) {
            //如果小于corePoolSize,则直接新增一个worker,并把当然用户提交的任务command作为参数,如果成功则返回。
            if (addWorker(command, true))
                return;
            //如果失败,则获取最新的线程池数据
            c = ctl.get();
        }
        //如果线程池仍在运行,则把任务放到阻塞队列中等待执行。
        if (isRunning(c) && workQueue.offer(command)) {
            //这里的recheck思路是为了处理并发问题
            int recheck = ctl.get();
            //当任务成功放入队列时,如果recheck发现线程池已经不再运行了则从队列中把任务删除
            if (! isRunning(recheck) && remove(command))
                //删除成功以后,会调用构造参数传入的拒绝策略。
                reject(command);
             //如果worker的数量为0(此时队列中可能有任务没有执行),则新建一个worker(由于此时新建woker的目的是执行队列中堆积的任务,
             //因此入参没有执行任务,详细逻辑后面会详细分析addWorker方法)。
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        //如果前面的新增woker,放入队列都失败,则会继续新增worker,此时线程池的状态是woker数量达到corePoolSize,阻塞队列任务已满
        //只能基于maximumPoolSize参数新建woker
        else if (!addWorker(command, false))
            //如果基于maximumPoolSize新建woker失败,此时是线程池中线程数已达到上限,队列已满,则调用构造参数中传入的拒绝策略
            reject(command);
    }

源码里我增加了很多注释,需要多读几遍才能完全理解,总结一下用户向线程池提交任务以后,线程池的执行逻辑:

  • 如果当前woker数量小于corePoolSize,则新建一个woker并把当前任务分配给该woker线程,成功则返回。
  • 如果第一步失败,则尝试把任务放入阻塞队列,如果成功则返回。
  • 如果第二步失败,则判断如果当前woker数量小于maximumPoolSize,则新建一个woker并把当前任务分配给该woker线程,成功则返回。
  • 如果第三步失败,则调用拒绝策略处理该任务。

从execute的源码可以看出addWorker方法是重中之重,马上来看下它的实现。

addWorker方法:

private boolean addWorker(Runnable firstTask, boolean core) {
        //这里有一段基于CAS+死循环实现的关于线程池状态,线程数量的校验与更新逻辑就先忽略了,重点看主流程。
        //...

        boolean workerStarted = false;
        boolean workerAdded = false;
        Worker w = null;
        try {
             //把指定任务作为参数新建一个worker线程
            w = new Worker(firstTask);
            //这里是重点,咋一看,一定以为w.thread就是我们传入的firstTask
            //其实是通过线程池构造函数参数threadFactory生成的woker对象
            //也就是说这个变量t就是代表woker线程。绝对不是用户提交的线程任务firstTask!!!
            final Thread t = w.thread;
            if (t != null) {
                final ReentrantLock mainLock = this.mainLock;
                mainLock.lock();
                try {
                    //加锁之后仍旧是判断线程池状态等一些校验逻辑。
                    int rs = runStateOf(ctl.get());
                    if (rs < SHUTDOWN ||
                        (rs == SHUTDOWN && firstTask == null)) {
                        if (t.isAlive())
                            throw new IllegalThreadStateException();
                        //把新建的woker线程放入集合保存,这里使用的是HashSet
                        workers.add(w);
                        int s = workers.size();
                        if (s > largestPoolSize)
                            largestPoolSize = s;
                        workerAdded = true;
                    }
                } finally {
                    mainLock.unlock();
                }
                if (workerAdded) {
                    //然后启动woker线程
                    //这里再强调一遍上面说的逻辑,该变量t代表woker线程,也就是会调用woker的run方法
                    t.start();
                    workerStarted = true;
                }
            }
        } finally {
            if (! workerStarted)
                //如果woker启动失败,则进行一些善后工作,比如说修改当前woker数量等等
                addWorkerFailed(w);
        }
        return workerStarted;
    }

addWorker方法主要做的工作就是新建一个Woker线程,加入到woker集合中,然后启动该线程,那么接下来的重点就是Woker类的run方法了。

worker执行方法:

//Woker类实现了Runnable接口
public void run() {
            runWorker(this);
        }

//最终woker执行逻辑走到了这里
final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        //task就是Woker构造函数入参指定的任务,即用户提交的任务
        Runnable task = w.firstTask;
        w.firstTask = null;
        w.unlock();
        boolean completedAbruptly = true;
        try {
            //一般情况下,task都不会为空(特殊情况上面注释中也说明了),因此会直接进入循环体中
            //这里getTask方法是要重点说明的,它的实现跟我们构造参数设置存活时间有关
            //我们都知道构造参数设置的时间代表了线程池中的线程,即woker线程的存活时间,如果到期则回收woker线程,这个逻辑的实现就在getTask中。
            //来不及执行的任务,线程池会放入一个阻塞队列,getTask方法就是去阻塞队列中取任务,用户设置的存活时间,就是
            //从这个阻塞队列中取任务等待的最大时间,如果getTask返回null,意思就是woker等待了指定时间仍然没有
            //取到任务,此时就会跳过循环体,进入woker线程的销毁逻辑。
            while (task != null || (task = getTask()) != null) {
                w.lock();
                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 {
                    //这里设为null,也就是循环体再执行的时候会调用getTask方法
                    task = null;
                    w.completedTasks++;
                    w.unlock();
                }
            }
            completedAbruptly = false;
        } finally {
            //当指定任务执行完成,阻塞队列中也取不到可执行任务时,会进入这里,做一些善后工作,比如在corePoolSize跟maximumPoolSize之间的woker会进行回收
            processWorkerExit(w, completedAbruptly);
        }
    }

woker线程的执行流程就是首先执行初始化时分配给的任务,执行完成以后会尝试从阻塞队列中获取可执行的任务,如果指定时间内仍然没有任务可以执行,则进入销毁逻辑。

注:这里只会回收corePoolSize与maximumPoolSize直接的那部分woker

理解了整个线程池的运行原理以后,再来看下JDK默认提供的线程池类型就会一目了然了:

public static ExecutorService newFixedThreadPool(int nThreads) {
        //corePoolSize跟maximumPoolSize值一样,同时传入一个无界阻塞队列
        //根据上面分析的woker回收逻辑,该线程池的线程会维持在指定线程数,不会进行回收
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
public static ExecutorService newSingleThreadExecutor() {
        //线程池中只有一个线程进行任务执行,其他的都放入阻塞队列
        //外面包装的FinalizableDelegatedExecutorService类实现了finalize方法,在JVM垃圾回收的时候会关闭线程池
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }
public static ExecutorService newCachedThreadPool() {
        //这个线程池corePoolSize为0,maximumPoolSize为Integer.MAX_VALUE,意思也就是说来一个任务就创建一个woker,回收时间是60s
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

最后再说说初始化线程池时线程数的选择:

  • 如果任务是IO密集型,一般线程数需要设置2倍CPU数以上,以此来尽量利用CPU资源。
  • 如果任务是CPU密集型,一般线程数量只需要设置CPU数加1即可,更多的线程数也只能增加上下文切换,不能增加CPU利用率。

上述只是一个基本思想,如果真的需要精确的控制,还是需要上线以后观察线程池中线程数量跟队列的情况来定。

作者:凌风郎少
链接:https://www.jianshu.com/p/5df6e38e4362
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

原文地址:https://www.cnblogs.com/GarfieldEr007/p/10230873.html

时间: 2024-10-01 05:05:34

当面试官问线程池时,你应该知道些什么?的相关文章

面试官问我,使用Dubbo有没有遇到一些坑?我笑了。

前言 17年的时候,因为一时冲动没把持住(当然最近也有粉丝叫我再冲动一把再更新一波),结合面试题写了一个系列的Dubbo源码解析.目前公众号大部分粉丝都是之前的粉丝,这里不过多介绍. 面试官问我,使用Dubbo有没有遇到一些坑?我笑了.根据我的面试经验而言,能在简历上写上原理.源码等关键词的,是非常具备核心竞争力的.上周和一个公众号粉丝交流面试情况如下 面试官问我,使用Dubbo有没有遇到一些坑?我笑了.面试的时候,把源码一波分析,令面试官虎躯一震!在一阵前戏过后,以为接下来无非就是身体的一顿抽

在使用线程池时应特别注意对ThreadLocal的使用

使用ThreadLocal并且有线程池时要特别注意,ThreadLocal是以线程为key的,而线程池里面的线程是会被重新利用的,所以如果有使用线程池并且使用ThreadLocal来保存状态信息时要特别注意要每次初始化,否则同一个线程会用到之前线程的状态信息,代码见 http://www.macrohuang.com/blog/?p=34 其他:http://blog.csdn.net/comliu/article/details/3186778http://www.blogjava.net/j

面试官问:如何让其他部门重视数据?该怎么答?

本文转自知乎 作者:接地气的陈老师 ----------------------------------------------------- 有同学问:老师,面试官问了我一个这样的问题:"管销售的领导一直不关心数据,而且人家那个部门也一直业绩排名第一,每年的指标都能达标,要怎样让这个领导重视起来呢?"我该怎么回答???? 答:如果不是面试的话,我建议直接放弃这哥们吧.回顾一下我们讲过的业务部门分类(如下图) 天天做用户画像,有多少同学对企业内部数据分析部门的用户进行过画像?哈哈 业务

面试官问现在工资是多少,该怎么回答?

面试,是一个推销自己的过程,先谈能力,后聊价格,顺序不能乱.如何谈薪资,能够做到对候选人最有利,是一门学问,也需要大量的实践.今天,我介绍两个谈薪资的要点给大家,稍加练习,便足以应对大部分的场面. 这个世界,从来都不是公平和客观的,面试也一样.用人单位对候选人的定价,从不看真实价值,往往只根据候选人当前的薪资来定,并且默认最多只会增幅0-20%(没错,不少公司在某些情况下一分钱都不会涨),而候选人对跳槽涨薪的预期一般是30%起.天然的,在谈判开始前,双方对薪资涨幅的预期,就存在巨大鸿沟,如何跨越

大厂面试官问你META-INF/spring.factories要怎么实现自动扫描、自动装配?

大厂面试官问你META-INF/spring.factories要怎么实现自动扫描.自动装配? 很多程序员想面试进互联网大厂,但是也有很多人不知道进入大厂需要具备哪些条件,以及面试官会问哪些问题,这里今天就给大家分享一下,如果大厂面试官问你META-INF/spring.factories要怎么实现自动扫描.自动装配,你需要怎么回答? 程序员应聘面试经验技巧和注意事项你知道哪些? 1.基础很重要,不要生疏了. 2.要关注技术前沿. 3.小公司比较看重知识的广度,大公司更看重知识的深度. 4.良好

面试又被问线程池原理?教你如何反击

前言 在阿里巴巴Java开发手册中有这么两段话,如下图所示可以看到提到的两点,第一要求不能显示的创建线程,也就是new Thread的这种形式,需要使用线程池对线程进行管理,第二不允许使用官方提供的四种线程池,而是需要通过自行创建的方式去创建线程池,更加理解线程池的允许规则本文就基于JDK1.8的代码,对线程池源码进行解析,带大家能够更好的理解线程池的概念以及其运行规则,如有错误,请大家指出 一.ThreadPoolExecutor源码 1.构造函数 先从构造函数看起: public Threa

去百度面试,想知道面试官问哪些问题,看这篇文章

https://mp.weixin.qq.com/s?__biz=MzI0NjM3NjI1NQ==&mid=2247487861&idx=1&sn=34316976cdb0aa2e4df3aa6f1f5cc31d&chksm=e9416325de36ea33200d9944fff37381de11c8d4dc8aeb0cc4b83c6201e56b8ee2bc8c2798cc&mpshare=1&scene=1&srcid=1102ElP5k7MBT

大厂面试官问你知道final、finally、finalize有什么区别?

前言Java程序员面试,基础真的很重要.基础这东西,各个公司都很看重,尤其是大公司,他们看中人的潜力,他们舍得花精力去培养,所以基础是重中之重.之前很多人问我,项目经历少怎么办,那就去打牢基础,当你的基础好的发指的时候,你的其他东西都不重要了. Java 语言有很多看起来很相似,但是用途却完全不同的语言要素,这些内容往往容易成为面试官考察你知识掌握程度的切入点.今天,我要问你的是一个经典的 Java 基础题目,谈谈 final.finally. finalize 有什么不同?典型回答final

当面试官问你:如何进行性能优化?

问题背景 在开发好页面后,如何让页面更快更好的运行,是区分一个程序猿技术水平和视野的一个重要指标.所以面试时,面试官总会问你一个问题,如何进行性能优化呢? 性能优化是什么 从前端的角度来说,性能优化可以分为两个方向.从用户角度来看,一个是页面加载的很快,另一个是页面使用起来很流畅.因此,对性能优化的探索,我们可以分为页面加载时间跟页面运行效率两个方向来进行研究 从浏览器打开到页面渲染完成,花费了多少时间 浏览器解析->查询缓存->dns查询->建立链接->服务器处理请求->服