Java线程池面试实战

java多线程原理图

面试官:我们使用线程池创建线程,说说为什么使用线程池创建?不用线程池创建可以吗?创建四个线程处理可不可以呢?

我:当然可以呀,但是使用线程池更合适,这样方便管理资源的创建与释放,阿里巴巴开发规范第一条规定:

【强制】线程资源必须通过线程池提供,不允许在应用中自行显示的创建;

说明:使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,
解决资源不足的问题。
如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者"过度切换"的问题。
                                                            ——《阿里巴巴研发手册》

我:就好比去餐厅吃饭,服务员总是把盘子提前洗好,而不是等你到达餐厅才去洗盘子,盘子好像线程池里的线程,打饭就是要处理的任务。

面试官:那你说说线程池的相关联的关系吗?

 Executor非常简单,就定义了线程最简单最本质的事,去处理提交任务,

public interface Executor {

    /**
     * Executes the given command at some time in the future.  The command
     * may execute in a new thread, in a pooled thread, or in the calling
     * thread, at the discretion of the {@code Executor} implementation.
     *
     * @param command the runnable task
     * @throws RejectedExecutionException if this task cannot be
     * accepted for execution
     * @throws NullPointerException if command is null
     */
    void execute(Runnable command);
}

 ExcutorService也是个接口,告诉实现它的线程池必须提供一些管理线程的方法:

AbstractExecutorService是普通的线程池执行器;

ScheduledThreadPoolExecutorService是定时任务线程池;

面试官:你知道线程池都有哪些核心参数吗?

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)
corePoolSize  //核心线程数
maxPoolSize  //最大线程数
long keepAliveTime //线程存活时间,超时了没有人调用,就会释放
TimeUnit unit  //存活时间单位
new BlockQueue<>() //阻塞队列
Executors.defaultThreadFactory() //线程工厂
policy()  //拒绝策略

面试官:那你平时是如何管理线程池的呢?

我:新建一个线程池管理器,比如ThreadPoolManager,初始化一个私有变量的Map集合,按照线程池的作用给它命名,把线程名称定义成常量和创建好的线程池放到管理器的Map集合中;

面试官:除了你自己用ThreadPoolExecutor创建线程池,还有别的方式吗?

我:用jdk 1.8 的java.util.concurent包里提供的Executors也可以用来创建线程池 ;

面试官:Executors定义了哪几种?

我:(1)newSingleThreadExecutos 单线程线程池,也就是线程池只有一个任务,这个我偶尔用一用;

(2)newFixedThreadPool(int nThreads) 固定大小线程的线程池

(3)newCachedThreadPool() 无界线程池,这个就是无论多少任务,都创建线程来运行,所以队列相当于没用。

面试官 : 你上面讲日常开发自己 用 ThreadPoolExecutor 创建线程池,为什么不用 Executors  提供的。

我: 第一是 Executors 提供的线程池使用场景很有限,一般场景很难用到,第二他们也都是通过 ThreadPoolExecutor 创建的线程池,我直接用 ThreadPoolExecutor 创建线程池,可以理解原理,灵活度更高。

参考阿里开发手册规约:

【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,
这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明:Executors 返回的线程池对象的弊端如下:
1)FixedThreadPool 和 SingleThreadPool:
允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
2)CachedThreadPool:
允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
                                                            ——《阿里巴巴研发手册》

面试官 : 前面你代码里有任务入队的操作,你一般自定义线程池,用的什么队列?

我: 这个要看实际应用的。

  1. 有的任务在早上 8 点和晚上 6 点都是高峰期,因此有任务尖刺,用 LinkedBlockingQueue, 这个是无界队列,不限制任务大小的。
  2. 对于重要性没那么高,非强依赖的任务用的 ArrayBlockingQueue,这个是指定大小的,如果任务超出,会创建非核心线程执行任务。

面试官 : 那你怎么保证任务队列的可用性呢?

我: 分几个方面:

  1. 我的线程池管理器,会有一个定时任务,定时检测 Map 中线程池当前任务队列的状态,会设置一个 waterThreshold(水位线),超出水位线会有告警;
  2. 日常大促演练,会对线程池做压测,如果发生超水位情况,还会对线程按线程名做降级,动态调整核心线程数和队列,当然还有限流、降级等其他有段保障。

面试官 : 那你怎么合理拆分线程池,核心任务数和任务队列大小的呢?

我: 这个是个老生常谈的问题。

【推荐】 了解每个服务大致的平均耗时,可以通过独立线程池配置,
将较慢的服务与主线程池隔离开,不致于各服务线程同归于尽。
                                        ——《阿里巴巴研发手册》
  1. 按照任务的类型,对任务做拆分,分成不同的线程池,分别命名;
  2. 区分任务的类型,是 CPU 密集型还是 IO 密集型,CPU 可以设置约为 CPU 核心数,上下文切换少,io 密集型可以设置的大一些。
  3. 大体估算一个,然后做压测,评估,另外线程池有个变量也可以参考意义:largestPoolSize,线程池达到过的最大线程任务,比如你刚开始可以把线程数设置的足够大,压测过后看这个参数达到的最大数值,同时参考系统的性能指标,cou、io、mem 等。
  4. 这里还有个公式借鉴:最佳线程数目 = ((线程等待时间+线程 CPU 时间)/线程 CPU 时间 )* CPU 数目
  5. 也有开源的辅助测算线程池的合理线程数。

面试官 : 那拒绝策略呢?了解吗

我: 拒绝策略就是当任务太多,超过 maximumPoolSize 了,只能拒绝。

面试官 : 详细讲讲

我: 拒绝的时候可以指定拒绝策略,也可以自己实现,JDK 默认提供了四种拒绝策略.

  • AbortPolicy

默认拒绝策略, 直接抛 RejectedExecutionException

  • DiscardPolicy

任务直接丢弃,不抛出异常

  • CallerRunsPolicy

由调用者来执行被拒绝的任务,比如主线程调用线程池的 submit 提交任务,但是任务被拒绝,则主线程直接执行。

但是线程池如果已经被关闭了,任务就被丢弃了。

public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    //线程池没关闭
    if (!e.isShutdown()) {
    //直接 run,没有让线程池来执行
      r.run();
    }
}
  • DiscardOldestPolicy

丢弃队列里等的最久的任务,然后尝试执行被拒绝的任务。

但是线程池如果已经被关闭了,任务就被丢弃了。

public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    if (!e.isShutdown()) {
      //丢弃队列头部任务
      e.getQueue().poll();
      //线程池尝试执行任务
      e.execute(r);
    }
}

 

//执行任务
public void execute(Runnable command) {
    if (command == null) {
        throw new NullPointerException();
    }
    // ctl 存储了两部分信息,当前线程池的线程数、线程池的运行状态
    int c = ctl.get();
    // 1、判断线程池的任务数是否小于核心线程数
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command,true)) // 2、创建核心线程执行任务,参数true表示创建的核心线程
            return;
        c = ctl.get();
    }
    // 3、如果线程池的线程数 >= 核心线程数,把任务添加到任务队列中
    // workerQueue:BlockingQueue<Runnable> workQueue;
    // offer:添加任务到队列中去
    if (isRuning(c) && workerQueue.offer(command)) {
        int recheck = ctl.get();
        if (! isRunning(recheck) && remove(command)) 
            reject(command);
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    // 4、如果队列满了,就创建非核心线程执行任务,false表示创建的线程为非核心线程
    else if (!addWorker(command, false))
        // addWorker()内部会判断,线程池的线程数 < 最大线程数,如果大等于了,就执行拒绝策略了
        reject(command);
}

面试官 : 那这几种拒绝策略,你选哪一种?

这个问题留给大家来思考???

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

sunshineAndAlways

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值