【面试题】线程池

1. 线程池的优势

  1. 线程复用,降低资源消耗。减少创建销毁线程的消耗;
  2. 提高响应速度,任务来的时候直接不需要等待线程的创建;

2. 线程池的4种创建方式Executors

  1. newFixedThreadPool(N) 执行长期任务,性能好很多
    创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
  2. newSingleThreadExecutor() 一个任务一个任务的执行
    如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
  3. newCachedThreadPool() 执行很多短期异步的小程序
class Main{

    public static void main(String[] args) throws Exception{
//        ExecutorService executorService = Executors.newFixedThreadPool(5);  //一池5个线程
//        ExecutorService executorService = Executors.newSingleThreadExecutor();  //一池1个线程
        ExecutorService executorService = Executors.newCachedThreadPool();  //一池多个线程

        try{
            //模拟10个任务,
            for (int i = 1; i <= 10; i++) {
                executorService.execute(()->{
                    System.out.println(Thread.currentThread().getName()+"办理业务");
                });
            }
        }catch (Exception e){

        }finally {
            executorService.shutdown();
        }
    }
}
  1. Executors.newScheduledThreadPool(N)。需要定期反复执行,例如,每秒刷新证券价格。
ScheduledExecutorService ses = Executors.newScheduledThreadPool(4);
// 1秒后执行一次性任务:
ses.schedule(new Task("one-time"), 1, TimeUnit.SECONDS);
// 2秒后开始执行定时任务,每3秒执行:
ses.scheduleAtFixedRate(new Task("fixed-rate"), 2, 3, TimeUnit.SECONDS);
// 2秒后开始执行定时任务,以3秒为间隔执行:
ses.scheduleWithFixedDelay(new Task("fixed-delay"), 2, 3, TimeUnit.SECONDS);

注意FixedRate和FixedDelay的区别。FixedRate是指任务总是以固定时间间隔触发,不管任务执行多长时间:
在这里插入图片描述
而FixedDelay是指,上一次任务执行完毕后,等待固定的时间间隔,再执行下一次任务:
在这里插入图片描述
因此,使用ScheduledThreadPool时,我们要根据需要选择执行一次、FixedRate执行还是FixedDelay执行。

2.1 线程池在开发场景中怎么使用

生产中,我们使用自定义的线程池。
FixedThreadPool 和 SingleThreadExecutor : 允许请求的队列长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致 OOM。
CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM。

2.2 推荐使用 ThreadPoolExecutor 构造函数创建线程池

按着下面线程池原理图写的,核心线程:2,最大线程:5,队列长度:3,默认的拒绝策略。

 public static void main(String[] args){
        ExecutorService executorService = new ThreadPoolExecutor(3,5,2,TimeUnit.SECONDS,
                new LinkedBlockingQueue<Runnable>(3), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
        try{
            //模拟10个任务,
            for (int i = 1; i <= 10; i++) {
                executorService.execute(()->{
                    System.out.println(Thread.currentThread().getName()+"办理业务");
                });
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            executorService.shutdown();
        }
    }

2.3 execute() vs submit()

execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;
submit()方法用于提交需要返回值的任务。线程池会返回一个 Future 类型的对象,通过这个 Future 对象可以判断任务是否执行成功,并且可以通过 Future 的 get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用 get(long timeout,TimeUnit unit)方法的话,如果在timeout时间内任务还没有执行完,就会抛出 java.util.concurrent.TimeoutException

2.4 shutdown() VS shutdownNow()

shutdown() :关闭线程池,线程池的状态变为 SHUTDOWN。线程池不再接受新任务了,但是队列里的任务得执行完毕。
shutdownNow() :关闭线程池,线程的状态变为 STOP。线程池会终止当前正在运行的任务,并停止处理排队的任务并返回正在等待执行的 List。

2.5 isTerminated() VS isShutdown()

isShutDown() 当调用 shutdown() 方法后返回为 true。
isTerminated() 当调用 shutdown() 方法后,并且所有提交的任务完成后返回为 true

3. 线程池的7大参数

1.corePoolSize,线程池中常驻核心线程数.
2.maximumPoolSize,线程池中可同时执行的最大线程数。
3.keepAliveTime,多余存活线程的存活时间,当线程池中线程数超出corePoolSize,当空闲时间超出keepAliveTime,多余线程会销毁直到只剩下corePoolSize个核心线程.
4.TimeUnit unit,keepAliveTime单位。
5.BlockingQueue workQueue,任务队列,被提交但是未被立马执行的任务。
6.ThreadFactory threadFactory,生成线程的线程工厂。
7.RejectedExecutionHandler handler,拒绝策略。

4. 线程池原理

在这里插入图片描述
在这里插入图片描述

5. 线程池拒绝策略

AbortPolicy策略:该策略会直接抛出异常,阻止系统正常工作。
CallerRunsPolicy 策略:只要线程池未关闭,该策略直接在调用者线程中,运行当前的被丢弃的任务。
DiscardOleddestPolicy策略: 该策略将丢弃最老的一个请求,也就是即将被执行的任务,并尝试再次提交当前任务。
DiscardPolicy策略:该策略默默的丢弃无法处理的任务,不予任何处理。

6. 面试题

在这里插入图片描述

6.1 线程池的线程是如何做到复用的

线程池中的线程在循环中尝试取任务执行,这一步会被阻塞,如果设置了allowCoreThreadTimeOut为true,则线程池中的所有线程都会在keepAliveTime时间超时后还未取到任务而退出。或者线程池已经STOP,那么所有线程都会被中断,然后退出。

6.2 核心线程数和最大线程数和工作线程个数的关系是什么呢?

在这里插入图片描述
工作线程的个数可能从0到最大线程数之间变化,当执行一段时间之后可能维持在核心线程数(corePoolSize),但也不是绝对的,取决于核心线程是否允许被超时回收。

6.3 存活时间keepAliveTime

上面我们说了,当工作线程数达到corePoolSize时,线程池会将新接收到的任务放在阻塞队列中,而阻塞队列又分为两种情况:一种是有界的队列,一种是无界的队列。
如果是无界队列,那么当核心线程都在忙时,所有新提交的任务都会被存放在该无界队列中,这时最大线程数将变得没有意义,因为阻塞队列不会存在被装满的情况。
如果是有界队列,那么当阻塞队列中装满了等待执行的任务,这时再有新任务提交时,线程池就需要创建新的临时线程来处理,相当于增派人手来处理任务。

但是创建的临时线程是有存活时间的,不可能让它们一直都存活着,当阻塞队列中的任务被执行完毕,并且又没有那么多新任务被提交时,临时线程就需要被回收销毁,而在被回收销毁之前等待的这段时间,就是非核心线程的存活时间,也就是keepAliveTime属性。

6.4 什么是非核心线程呢?是不是先创建的线程就是核心线程,后创建的就是非核心线程呢?

其实核心线程跟创建的先后没有关系,而是跟工作线程的个数有关,如果当前工作线程的个数大于核心线程数,那么所有的线程都可能是非核心线程,都有被回收的可能。

6.5 怎么保证核心线程不会被回收呢?还是跟工作线程的个数有关,每一个线程在取任务的时候,线程池会比较当前的工作线程个数与核心线程数。

一个线程执行完一个任务后,会去阻塞队列里面取新的任务,在取到任务之前,它就是一个闲置的线程。
取任务的方法有两种,一种是通过take()方法一直阻塞直到取出任务,另一种是通过poll(keepAliveTime, timeUnit)方法在一定时间内取出任务或者超时,如果超时这个线程就会被回收,请注意核心线程一般不会被回收。
1.如果工作线程数小于当前的核心线程数,则使用第一种方法取任务,也就是没有超时回收,这时所有的工作线程都是核心线程,它们不会被回收。
2.如果工作线程数大于核心线程数,则使用第二种方法取任务,一旦超时就回收,所以并没有绝对的核心线程,只要这个线程没有在存活时间内取到任务去执行就会被回收。
核心线程一般不会被回收,但是也不是绝对的,如果我们设置了允许核心线程超时被回收的话,那么就没有核心线程这种说法了,所有的线程都会通过poll(keepAliveTime, timeUnit)来获取任务,一旦超时获取不到任务,就会被回收,一般很少会这样来使用,除非该线程池需要处理的任务非常少,并且频率也不高,不需要将核心线程一直维持着。

7. 线程池合理线程数

CPU密集型(任务需要大量运算,没有阻塞):CPU核数 + 1
IO密集型(任务线程并不是一直在执行任务,则应尽可能多的配置线程):CPU核数 * 2

7.1 如何判断是 CPU 密集任务还是 IO 密集任务?

CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内存中对大量数据进行排序。但凡涉及到网络读取,文件读取这类都是 IO 密集型,这类任务的特点是 CPU 计算耗费时间相比于等待 IO 操作完成的时间来说很少,大部分时间都花在了等待 IO 操作完成上。

8.线程池状态

在这里插入图片描述
运行状态(RUNNING):此状态下,线程池可以接受新的任务,也可以处理阻塞队列中的任务。执行shutdown()方法可进入待关闭(SHUTDOWN)状态,执行shutdownNow()方法可进入停止(STOP)状态。
停止状态(STOP):此状态下,线程池不接受新任务,也不处理阻塞队列中的任务,反而会尝试结束执行中的任务。当工作线程数为0时,进入整理(TIDYING)状态。
待关闭状态(SHUTDOWN):此状态下,线程池不再接受新的任务,继续处理阻塞队列中的任务。当阻塞队列中的任务为空,且工作线程数为0的时候,进入整理(TIDYING)状态。
整理状态(TIDYING):此状态下,所有任务都已经执行完毕,且没有工作线程。执行terminated()方法进入终止(TERMINATED)状态。
终止状态(TERMINATED):此状态下,线程池完全终止,并完成了所有资源的释放。

9. 使用线程池注意事项

很多人在实际项目中都会有类似这样的问题:我的项目中多个业务需要用到线程池,是为每个线程池都定义一个还是说定义一个公共的线程池呢?

一般建议是不同的业务使用不同的线程池,配置线程池的时候根据当前业务的情况对当前线程池进行配置,因为不同的业务的并发以及对资源的使用情况都不同,重心优化系统性能瓶颈相关的业务。
在这里插入图片描述
上面的代码可能会存在死锁的情况,为什么呢?画个图给大家捋一捋。

试想这样一种极端情况:假如我们线程池的核心线程数为 n,父任务(扣费任务)数量为 n,父任务下面有两个子任务(扣费任务下的子任务),其中一个已经执行完成,另外一个被放在了任务队列中。由于父任务把线程池核心线程资源用完,所以子任务因为无法获取到线程资源无法正常执行,一直被阻塞在队列中。父任务等待子任务执行完成,而子任务等待父任务释放线程池资源,这也就造成了 “死锁”
在这里插入图片描述
解决方法也很简单,就是新增加一个用于执行子任务的线程池专门为其服务。

10. 加餐:Callable+ThreadPoolExecutor示例代码

MyCallable.java

import java.util.concurrent.Callable;

public class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        Thread.sleep(1000);
        //返回执行当前 Callable 的线程名字
        return Thread.currentThread().getName();
    }
}

CallableDemo.java

public class CallableDemo {

    private static final int CORE_POOL_SIZE = 5;
    private static final int MAX_POOL_SIZE = 10;
    private static final int QUEUE_CAPACITY = 100;
    private static final Long KEEP_ALIVE_TIME = 1L;

    public static void main(String[] args) {

        //使用阿里巴巴推荐的创建线程池的方式
        //通过ThreadPoolExecutor构造函数自定义参数创建
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                CORE_POOL_SIZE,
                MAX_POOL_SIZE,
                KEEP_ALIVE_TIME,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(QUEUE_CAPACITY),
                new ThreadPoolExecutor.CallerRunsPolicy());

        List<Future<String>> futureList = new ArrayList<>();
        Callable<String> callable = new MyCallable();
        for (int i = 0; i < 10; i++) {
            //提交任务到线程池
            Future<String> future = executor.submit(callable);
            //将返回值 future 添加到 list,我们可以通过 future 获得 执行 Callable 得到的返回值
            futureList.add(future);
        }
        for (Future<String> fut : futureList) {
            try {
                System.out.println(new Date() + "::" + fut.get());
            } catch (InterruptedException | ExecutionException e) {
                e.printStackTrace();
            }
        }
        //关闭线程池
        executor.shutdown();
    }
}

输出

Wed Nov 13 13:40:41 CST 2019::pool-1-thread-1
Wed Nov 13 13:40:42 CST 2019::pool-1-thread-2
Wed Nov 13 13:40:42 CST 2019::pool-1-thread-3
Wed Nov 13 13:40:42 CST 2019::pool-1-thread-4
Wed Nov 13 13:40:42 CST 2019::pool-1-thread-5
Wed Nov 13 13:40:42 CST 2019::pool-1-thread-3
Wed Nov 13 13:40:43 CST 2019::pool-1-thread-2
Wed Nov 13 13:40:43 CST 2019::pool-1-thread-1
Wed Nov 13 13:40:43 CST 2019::pool-1-thread-4
Wed Nov 13 13:40:43 CST 2019::pool-1-thread-5

视频讲解:https://www.bilibili.com/video/BV1vE411D7KE?p=13&spm_id_from=333.788.top_right_bar_window_history.content.click&vd_source=b901ef0e9ed712b24882863596eab0ca
参考:https://blog.csdn.net/zhaohong_bo/article/details/89303522
https://snailclimb.gitee.io/javaguide/#/./docs/java/concurrent/java-thread-pool-summary
ScheduledThreadPool:https://www.liaoxuefeng.com/wiki/1252599548343744/1306581130018849

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
回答:Java线程池是Java中的一个重点知识,并且在Java的工作中经常会遇到,因此在面试中也是必问的面试目。以下是一些常见的Java线程池面试: 1. 谈谈什么是线程池? 2. 为什么要使用线程池? 3. 你们哪些地方会使用到线程池? 4. 线程池有哪些作用? 5. 线程池的创建方式有哪些? 6. 线程池底层是如何实现复用的? 7. ThreadPoolExecutor核心参数有哪些? 8. 线程池创建的线程会一直在运行状态吗? 9. 为什么阿里巴巴不建议使用Executors? 10. 线程池的底层实现原理是什么? 11. 线程池队列满了,任务会丢失吗? 12. 线程池的拒绝策略类型有哪些? 13. 线程池如何合理配置参数? 这些问涵盖了线程池的基本概念、使用场景、实现原理以及相关的配置和策略等方面的知识。了解这些问能够帮助面试者更好地理解和应用Java线程池。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [java线程池面试有哪些?java线程池常见面试](https://blog.csdn.net/muli525/article/details/123553744)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 50%"] - *3* [(一)【Java精选面试线程池底层实现原理(含答案)](https://blog.csdn.net/qq_30999361/article/details/124924343)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值