线程池面试问题

本文详细探讨了线程池的概念、使用原因及其工作原理,包括核心线程、最大线程、任务队列和拒绝策略。介绍了不同类型的线程池如SingleThreadExecutor、FixedThreadPool、CachedThreadPool和ScheduledThreadPool,以及阿里巴巴推荐使用自定义线程池的原因。线程池的提交任务方式、关闭策略、线程数量选择、使用阻塞队列的优势和线程池状态管理等关键知识点也得到了阐述。

一、什么是线程池?

线程池是一种多线程处理形式,处理过程中将任务提交到线程池,任务的执行交由线程池来管理。
如果每个请求都创建一个线程去处理,那么服务器的资源很快就会被耗尽,使用线程池可以减少创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。

二、为什么使用线程池?

创建线程和销毁线程的花销是比较大的,这些时间有可能比处理业务的时间还要长。这样频繁的创建线程和销毁线程,再加上业务工作线程,消耗系统资源的时间,可能导致系统资源不足。(我们可以把创建和销毁的线程的过程去掉)

三、线程池工作原理

1、当线程池中有任务需要执行时,线程池会判断如果线程数量没有超过核心数量就会新建线程进行任务执行
2、如果线程池中的线程数量已经超过核心线程数,这时候任务就会被放入任务队列中排队等待执行
3、如果任务队列超过最大队列数,并且线程池没有达到最大线程数,就会新建线程来执行任务;
4、如果超过了最大线程数,就会执行拒绝执行策略
总结:提交顺序:核心线程池 > 队列 > 非核心线程池 执行顺序:核心线程池 > 非核心线程池 > 队列

四、线程池有哪些?

线程池参数:corePoolSize(核心线程数)、maximumPoolSize(最大线程数)、keepAliveTime(存活时间)、util(时间单位)、workQueue(队列)、threadFactory(创建线程的线程工厂)、handler(拒绝策略)
1、newSingleThreadExecutor(单线程化的线程池)

new ThreadPoolExecutor(1, 1,
                        0L, TimeUnit.MILLISECONDS,
                        new LinkedBlockingQueue<Runnable>())

描述:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
使用场景:用于串行执行任务的场景,每个任务必须按顺序执行,不需要并发执行。
2、newFixedThreadPool(定长线程池)

return new ThreadPoolExecutor(nThreads, nThreads,
                              0L, TimeUnit.MILLISECONDS,
                              new LinkedBlockingQueue<Runnable>())

描述:创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
使用场景:用于负载比较重的服务器,为了资源的合理利用,需要限制当前线程数量。
3、newCachedThreadPool(缓存线程池)

new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                              60L, TimeUnit.SECONDS,
                              new SynchronousQueue<Runnable>(),
                              threadFactory)

描述:创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
使用场景:用于并发执行大量短期的小任务,或者是负载较轻的服务器。
4、newScheduledThreadPool(定长定时任务线程池)

new ThreadPoolExecutor(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
      new DelayedWorkQueue(), threadFactory)

描述:创建一个定长线程池,支持定时及周期性任务执行。
使用场景:用于需要多个后台线程执行周期任务,同时需要限制线程数量的场景。
5、阿里推荐不使用以上四个,使用自定义线程池

public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
                 BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) {
    super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
}

推荐原因
(1)ThreadPoolExecutor和Executors比较
使用线程池可以减少创建和销毁线程上所花的时间以及系统资源的开销,然后之所以不用Executors自定义线程池,Executors的很多的参数设置并不合理,用ThreadPoolExecutor是为了规范线程池的使用,还有让其他人更好懂线程池的运行规则。
(2)ThreadPoolExecutor和上面四个比较
newCachedThreadPool:最大线程数是Integer.MAX_VALUE, 并且任务队列是SynchronousQueue。 也就是说这个线程池对任务来着不拒,线程
不够用就创建一个, 感觉就像一个豪横的富豪。 这就是问题所在了, 如果同一时刻应用的来了大量的任务, 这个线程池很容易就创建过多的线程,
而创建线程又是一个很耗性能的事情, 这就容易导致应用卡顿或者直接内存溢出(OOM))
newFixedThreadPool: 线程数量被定死了, 所以线程数量是不会超出的,但是它的任务队列是无界的LinkedBlockingQueue, 对于加进来的任务处
理不过来就会存入任务队列中, 并且无限制的存入队列。 这个线程池感觉就是家里有地, 无论来多少货都往里面装。这个线程池如果使用不当很容易导
致内存溢出(OOM)
newSingleThreadExecutor:这个线程池只有一个线程, 比newFixedThreadPool还穷, 但是任务队列和上面一样, 没有限制, 很容易就使用不当
很容易发生内存溢出(OOM)
newScheduledThreadPool:这个是定时任务的线程池, 没有定义线程创建数量的上线, 同时任务队列也没有定义上限, 如果前一次定时任务还
没有完成, 后一个定时任务的运行时间到了, 它也会运行, 线程不够就创建。 这样如果定时任务运行的时间过长, 就会导致前后两个定时任务同时执
行,如果他们之间有锁,还有可能出现死锁, 此时灾难就发生了。

五、线程池拒绝策略

使用原因:当请求任务不断的过来,而系统此时又处理不过来的时候,我们需要采取的策略是拒绝服务。
ThreadPoolExecutor中包含四种处理策略:
AbortPolicy策略:该策略会直接抛出异常,阻止系统正常工作。(线程池默认此策略)
CallerRunsPolicy 策略:只要线程池未关闭,该策略直接在调用者线程中,运行当前的被丢弃的任务。
DiscardOleddestPolicy策略: 该策略将丢弃最老的一个请求,也就是即将被执行的任务,并尝试再次提交当前任务。
DiscardPolicy策略:该策略默默的丢弃无法处理的任务,不予任何处理。
除了JDK默认提供的四种拒绝策略,我们可以根据自己的业务需求去自定义拒绝策略,自定义的方式很简单,直接实现RejectedExecutionHandler接口即可。

六、线程池的提交任务的方式

ExecutorService 提供了两种提交任务的方法:
execute():提交不需要返回值的任务
submit():提交需要返回值的任务
void execute(Runnable command);
//execute() 的参数是一个 Runnable,也没有返回值。因此提交后无法判断该任务是否被线程池执行成功

ExecutorService executor = Executors.newCachedThreadPool();
executor.execute(new Runnable() {
    @Override
    public void run() {
        //do something
    }
});

//通过Funture 对象,通过它我们可以判断任务是否执行成功,Funture 对象,通过它我们可以判断任务是否执行成功

<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);

七、关闭线程池方式

线程池即使不执行任务也会占用一些资源,所以在我们要退出任务时最好关闭线程池。
shutdown():线程池即使不执行任务也会占用一些资源,所以在我们要退出任务时最好关闭线程池。
shutdownNow():将线程池设置为 STOP,然后尝试停止 所有线程,并返回等待执行任务的列表。
共同点:都是通过遍历线程池中的线程,逐个调用 Thread.interrup() 来中断线程,所以一些无法响应中断的任务可能永远无法停止(比如
Runnable)。
不同点:shutdown() 只结束未执行的任务;shutdownNow() 结束全部。

八、线程池的线程数量选择

任务为IO密集型(IO:100%, CPU:85%)
线程数 = 2 * CPU核心数
CPU消耗不多,IO比较耗时,需要使用大的线程池
比如:mysql数据库、文件读写
任务为CPU密集型(CPU:100%,IO:85%)
线程数 = CPU核心数 + 1
CPU使用率很高,IO消耗量较少,如果开过多的线程,造成CPU过度切换,需要使用较小的线程池
比如:加解密、压缩文件、计算

九、为什么线程池使用阻塞队列(BlockingQueue)

阻塞队列可以保证任务队列中没有任务时,阻塞获取任务的线程,使该线程进入wait状态,释放cpu资源。
当队列中有任务时才唤醒对应线程从队列中取出消息进行执行。
使得线程不至于一直占用cpu资源
线程执行完任务后,通过循环再次从任务队列中取出任务进行执行
阻塞队列和非阻塞队列区别

十、线程池状态

RUNNING:线程池的初始化状态,可以添加待执行的任务。
SHUTDOWN:线程池处于待关闭状态,不接收新任务仅处理已经接收的任务。
STOP:线程池立即关闭,不接收新的任务,放弃缓存队列中的任务并且中断正在处理的任务。
TIDYING:线程池自主整理状态,调用 terminated() 方法进行线程池整理。
TERMINATED:线程池终止状态。

十一、线程池中线程复用原理

线程池将线程和任务进行解耦,线程是线程,任务是任务,摆脱了之前通过 Thread 创建线程时的一个线程必须对应一个任务的限制
在线程池中,同一个线程可以从阻塞队列中不断获取新任务来执行,其核心原理在于线程池对 Thread 进行了封装
不是每次执行任务都会调用 Thread.start() 来创建新线程,而是让每个线程去执行一个“循环任务”,在这个“循环任务”中不停的检查是否有任务需要被执行
如果有则直接执行,也就是调用任务中的 run 方法,将 run 方法当成一个普通的方法执行
通过这种方式将只使用固定的线程就将所有任务的 run 方法串联起来。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值