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

2020-12-16  本文已影响0人  Java柚子

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      备注:简书   免费领取~

上一篇下一篇

猜你喜欢

热点阅读