美文网首页
一篇文章搞清楚Java线程池!知识点整理及经典面试题剖析,必看!

一篇文章搞清楚Java线程池!知识点整理及经典面试题剖析,必看!

作者: Java柚子 | 来源:发表于2020-12-16 15:01 被阅读0次

    1、什么是线程池

    线程池的基本思想是一种对象池,在程序启动时就开辟一块内存空间,里面存放了众多(未死亡)的线程,池中线程执行调度由池管理器来处理。当有线程任务时,从池中取一个,执行完成后线程对象归池,这样可以避免反复创建线程对象所带来的性能开销,节省了系统的资源。

    记得点赞收藏加关注哦 ,我这里也准备了很多面试热门知识点和大厂面试题,希望对大家有帮助!有需要的朋友

    可以加q群:580763979      备注:简书   免费领取~

    2、使用线程池的好处

    减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。

    运用线程池能有效的控制线程最大并发数,可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。

    对线程进行一些简单的管理,比如:延时执行、定时循环执行的策略等,运用线程池都能进行很好的实现

    3、线程池的主要组件

    一个线程池包括以下四个基本组成部分:

    线程池管理器(ThreadPool):用于创建并管理线程池,包括 创建线程池,销毁线程池,添加新任务;

    工作线程(WorkThread):线程池中线程,在没有任务时处于等待状态,可以循环的执行任务;

    任务接口(Task):每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等;

    任务队列(taskQueue):用于存放没有处理的任务。提供一种缓冲机制。

    4、线程池参数设置

    参数的设置跟系统的负载有直接的关系,下面为系统负载的相关参数:

    tasks,每秒需要处理的的任务数(针对系统需求)

    threadtasks,每个线程每钞可处理任务数(针对线程本身)

    responsetime,系统允许任务最大的响应时间,比如每个任务的响应时间不得超过2秒。

    corePoolSize

    系统每秒有tasks个任务需要处理理,则每个线程每钞可处理threadtasks个任务。,则需要的线程数为:tasks/threadtasks,即tasks/threadtasks个线程数。

    假设系统每秒任务数为100 ~ 1000,每个线程每钞可处理10个任务,则需要100 / 10至1000 / 10,即10 ~ 100个线程。那么corePoolSize应该设置为大于10,具体数字最好根据8020原则,因为系统每秒任务数为100 ~ 1000,即80%情况下系统每秒任务数小于1000 * 20% = 200,则corePoolSize可设置为200 / 10 = 20。

    queueCapacity

    任务队列的长度要根据核心线程数,以及系统对任务响应时间的要求有关。队列长度可以设置为 所有核心线程每秒处理任务数 * 每个任务响应时间 = 每秒任务总响应时间 ,即(corePoolSize*threadtasks)responsetime: (2010)*2=400,即队列长度可设置为400。

    maxPoolSize

    当系统负载达到最大值时,核心线程数已无法按时处理完所有任务,这时就需要增加线程。每秒200个任务需要20个线程,那么当每秒达到1000个任务时,则需要(tasks - queueCapacity)/ threadtasks 即(1000-400)/10,即60个线程,可将maxPoolSize设置为60。

    队列长度设置过大,会导致任务响应时间过长,切忌以下写法:

    LinkedBlockingQueue queue = new LinkedBlockingQueue();

    这实际上是将队列长度设置为Integer.MAX_VALUE,将会导致线程数量永远为corePoolSize,再也不会增加,当任务数量陡增时,任务响应时间也将随之陡增。

    keepAliveTime

    当负载降低时,可减少线程数量,当线程的空闲时间超过keepAliveTime,会自动释放线程资源。默认情况下线程池停止多余的线程并最少会保持corePoolSize个线程。

    allowCoreThreadTimeout

    默认情况下核心线程不会退出,可通过将该参数设置为true,让核心线程也退出。

    一般说来,大家认为线程池的大小经验值应该这样设置:(其中N为CPU的个数)

    如果是CPU密集型应用,则线程池大小设置为N+1

    如果是IO密集型应用,则线程池大小设置为2N+1

    5、线程池的五种状态

    线程池的初始化状态是RUNNING,能够接收新任务,以及对已添加的任务进行处理。

    线程池处在SHUTDOWN状态时,不接收新任务,但能处理已添加的任务。 调用线程池的shutdown()接口时,线程池由RUNNING -> SHUTDOWN。

    线程池处在STOP状态时,不接收新任务,不处理已添加的任务,并且会中断正在处理的任务。 调用线程池的shutdownNow()接口时,线程池由(RUNNING or SHUTDOWN ) -> STOP。

    当所有的任务已终止,ctl记录的”任务数量”为0,线程池会变为TIDYING状态。当线程池变为TIDYING状态时,会执行钩子函数terminated()。terminated()在ThreadPoolExecutor类中是空的,若用户想在线程池变为TIDYING时,进行相应的处理;可以通过重载terminated()函数来实现。

    当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由 SHUTDOWN -> TIDYING。

    当线程池在STOP状态下,线程池中执行的任务为空时,就会由STOP -> TIDYING。 线程池彻底终止,就变成TERMINATED状态。线程池处在TIDYING状态时,执行完terminated()之后,就会由 TIDYING -> TERMINATED。

    6、关闭线程池

    线程池提供两种关闭线程池方法:shutDown()和shutdownNow()

    shutDown()

    当线程池调用该方法时,线程池的状态则立刻变成SHUTDOWN状态。此时,则不能再往线程池中添加任何任务,否则将会抛出RejectedExecutionException异常。但是,此时线程池不会立刻退出,直到添加到线程池中的任务都已经处理完成,才会退出。

    shutdownNow()

    根据JDK文档描述,大致意思是:执行该方法,线程池的状态立刻变成STOP状态,并试图停止所有正在执行的线程,不再处理还在池队列中等待的任务,当然,它会返回那些未执行的任务。

    它试图终止线程的方法是通过调用Thread.interrupt()方法来实现的,但是大家知道,这种方法的作用有限,如果线程中没有sleep、wait、Condition、定时锁等应用, interrupt()方法是无法中断当前的线程的。所以,ShutdownNow()并不代表线程池就一定立即就能退出,它可能必须要等待所有正在执行的任务都执行完成了才能退出。

    7、各种场景下怎么设置线程数

    高并发、任务执行时间短的业务怎样使用线程池?

    线程池线程数可以设置为CPU核数+1,减少线程上下文的切换

    并发不高、任务执行时间长的业务怎样使用线程池?

    这个需要判断执行时间是耗在哪个地方

    假如是业务时间长集中在IO操作上,也就是IO密集型的任务,因为IO操作并不占用CPU,所以不要让所有的CPU闲下来,可以适当加大线程池中的线程数目(2 * CPU核数),让CPU处理更多的业务。

    假如是业务时间长集中在计算操作上,也就是CPU密集型任务,和(1)CPU核数+1 一样吧,线程池中的线程数设置得少一些,减少线程上下文的切换

    并发高、业务执行时间长的业务怎样使用线程池?

    解决这种类型任务的关键不在于线程池而在于整体架构的设计

    8、为什么不推荐使用JUC的线程池?

    这样的处理方式更加明确线程池的运行规则,规避资源耗尽的风险

    newFixedThreadPool和newSingleThreadExecutor

    上面两个主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM

    newCachedThreadPool和newScheduledThreadPool

    上面两个主要问题是最大线程数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM。

    9、问题

    非核心线程延迟死亡,如何实现?

    通过阻塞队列poll(),让线程阻塞等待一段时间,如果没有取到任务,则线程死亡

    线程池为什么能维持线程不释放,随时运行各种任务?

    for (;;) {

                try {

                    Runnable r = timed ?

                        workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :

                        workQueue.take();

                    if (r != null)

                        return r;

                    timedOut = true;

                } catch (InterruptedException retry) {

                    timedOut = false;

                }

            }

        }

    在死循环中工作队列workQueue会一直去拿任务:

    核心线程的会一直卡在 workQueue.take()方法,让线程一直等待,直到获取到任务,然后返回。

    非核心线程会 workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) ,如果超时还没有拿到,下一次循环判断compareAndDecrementWorkerCount就会返回null,Worker对象的run()方法循环体的判断为null,任务结束,然后线程被系统回收。

    通过阻塞队列take(),让线程一直等待,直到获取到任务

    如何释放核心线程?

    将allowCoreThreadTimeOut设置为true。可用下面代码实验

    {

        // 允许释放核心线程,等待时间为100毫秒

        es.allowCoreThreadTimeOut(true);

        for(......){

            // 向线程池里添加任务,任务内容为打印当前线程池线程数

            Thread.currentThread().sleep(200);

        }

    }

    线程数会一直为1。 如果allowCoreThreadTimeOut为false,线程数会逐渐达到饱和,然后大家一起阻塞等待。

    非核心线程能成为核心线程吗?

    线程池不区分核心线程于非核心线程,只是根据当前线程池容量状态做不同的处理来进行调整,因此看起来像是有核心线程于非核心线程,实际上是满足线程池期望达到的并发状态。

    Runnable在线程池里如何执行?

    线程执行Worker,Worker不断从阻塞队列里获取任务来执行。。

    总结

    我这里准备了一线大厂面试资料和超多超硬核的PDF技术文档,以及我为大家精心准备的多套简历模板(不断更新中),希望大家都能找到心仪的工作!

    有需要的朋友可以加q群:580763979      备注:简书   免费领取~

    相关文章

      网友评论

          本文标题:一篇文章搞清楚Java线程池!知识点整理及经典面试题剖析,必看!

          本文链接:https://www.haomeiwen.com/subject/jxongktx.html