简介
本专栏结合笔者面试的一些高频率题目,详细分析其原理、场景、底层机制、以及实际场景等。为了节约篇幅,我们注重分析一些原理、较为底层的东西,对于一些概念性的知识,不多赘述。也希望通过这个专栏,让大家在面试的过程中能够灵活应对。早日收到满意的offer
关于线程池的一些概念,如线程池是什么、为什么要使用线程池,不多赘述。
1、为什么不推荐使用Executors
线程池的创建方式有很多种,这也是面试最常问的问题,归根结底,面试官还是为了考察线程池的参数含义。不推荐使用 Executors的原因是默认参数不友好:
- FixedThreadPool和SingleThreadPool,队列长度为Integer.MAX_VALUE
- CachedThreadPool和ScheduledThreadPool:线程数量为Integer.MAX_VALUE
2、线程池核心参数分析
- corePoolSize:核心线程数
- maximumPoolSize:最大线程数
- workQueue:任务队列
- RejectedExecutionHandler 拒绝策略
- keepAliveTime:空闲时间
- timeUnit:时间单位
- threadFactory:线程工厂
针对参数会问到的是任务队列和拒绝策略,考察对java集合、拒绝策略的处理。
队列名称 | 简单分析 |
---|---|
ArrayBlockingQueue | 数组,ReentrantLock+Condition,生产者、消费者共用一把锁 |
LinkedBlockingQueue | 链表,ReentrantLock+Condition,生产者、消费者的锁各自独立,吞吐量高于ArrayBlockingQueue |
SynchronousQueue | 不存储元素,插入操作必须等到另一个线程调用移除操作,吞吐量高于LinkedBlockingQueue |
PriorityBlockingQueue | 带优先级 |
JDK自带策略有四种,但是对任务的处理都不友好,我们需要自定义拒绝策略,实现RejectedExecutionHandler即可
策略名 | 对任务的处理方式 |
---|---|
Abort | 抛出异常 |
CallerRuns | 使用调用者所在线程来运行任务 |
DiscardOldest | 丢弃队列里最近的一个任务,并执行当前任务 |
Discard | 直接丢弃掉 |
3、线程池生命周期
生命周期一般考察的不多,但是需要了解一下。
生命周期 | 对应操作 |
---|---|
running | 接收新任务,处理队列中的任务 |
shutdown | 不接收新任务,但是会处理队列中的任务 |
stop | 不接收新任务,不处理队列中的任务,并且中断正在执行的任务 |
tidying | 清空worker对象 |
terminated | 完全停止 |
4、线程池膨胀过程
该问题是最常考察的。主要针对 corePoolSize、maximumPoolSize、workQueue三个参数
5、线程池执行过程
线程池执行任务的流程如下,可结合源码分析:
- 1、根据任务提交和执行情况,逐步饱和线程池。若线程数小于corePoolSize,创建线程;若任务队列未满,则将任务加入到队列中;若线程数小于maximumPoolSize,则创建线程;若线程池饱和,则根据回绝策略回绝新提交的线程
- 2、调用addWorker方法,将任务包装成Worker对象。Worker类继承了线程Thread类,线程启动后,通过runWorker方法执行任务
- 3、调用runWorker方法,先判断addWorker方法是否传递任务,若有,则执行任务;若无,则通过getTask()方法从队列中获取任务
- 4、getTask方法,会判断当前线程数是否大于corePoolSize或者allowCoreThreadTimeOut是否为true,以决定是是以阻塞的形式还是以带超时时间的形式去获取任务,并决定是否回收线程
- 5、如果线程应该被回收,则调用processWorkerExit方法回收线程,回收的时候要注意判断若线程池的状态为running或shutdown,并且任务队列不为空,则至少需要保留一个线程。但是由于回收线程在前,则需要再次通过addWorker方法创建一个线程,以用来执行队列中的任务
6、线程池大小预估
这个是比较常见的问题。包含线程数、队列大小两个方面
6.1 公式一
CPU 密集型应用,线程池大小设置为 N + 1;IO 密集型应用,线程池大小设置为 2N。这种估算方式网上比较常见,但是实际应用中很难界定到底什么是CPU 密集型应用、什么是IO 密集型应用。
6.2 公式二
core = tps(每秒能完成的事务数 = 并发量 / 平均响应时间)* time,max = tps * time * (1.7 ~ 2),该估算方式考虑到了业务场景,但是业务流量往往是不均匀分布的
6.3 公式三
线程数 = (1 + 线程等待时间/线程CPU时间 * 目标CPU的使用率 * 处理器核心数。例如:CPU Time 0.5ms,Wait Time 1.5ms,CPU使用率是90%,CPU核心数为8,(1 + 1.5/0.5) * 90% * 8 = 28.8。可以结合ThreadMXBean,计算出线程CPU时间、线程等待时间。
6.4 公式四
队列大小:queueSize = (coreSize/taskCost)*resTime。假设每个任务执行耗时,100ms,系统能够忍受的最大响应时间为2秒,线程池大小为32,那么队列大小可以设定为640
7、自定义线程池
实际应用中,可以自定义线程池,动态控制线程池的各个参数值、监控线程池的状态
自定义选项 | 分析 |
---|---|
线程数 | 通过setCorePoolSize、setMaximumPoolSize动态设定线程数 |
队列 | 自定义队列以支持长度的动态调整,拷贝LinkedBlockingQueue代码,提供setQueueSize方法 |
监控 | 通过beforeExecute、afterExecute方法,监控线程最长执行时间、平均执行时长、线程活跃度、队列长度等,并提供预警 |
拒绝策略 | 实现RejectedExecutionHandler接口,通过日志、持久化等方式存储不能被正常处理的任务以便将来恢复线程 |