推荐阅读
JUC源码
并发工具
19 | CountDownLatch和CyclicBarrier让多线程步调一致
17 | ReadWriteLock:如何快速实现一个完备的缓存?
在上篇 Executor与线程池:如何创建正确的线程池?中,介绍了Java线程池是一种生产者-消费者模式,ThreadPoolExecutor的核心参数,以及使用线程池的注意事项。
Java线程池,也是各大公司面试的重点,面试题也是层出不穷。这里梳理了一些“劝退”面试题,看你能够答上来几个。
问题1:为什么使用线程池,而不是直接创建线程?
线程是一个操作系统概念。操作系统负责这个线程的创建、挂起、运行、阻塞和终结操作。
而操作系统创建线程、切换线程状态、终结线程都要进行CPU调度——这是一个耗费时间和系统资源的事情。
线程属于操作系统的资源,使用线程池,预先创建一批线程,进行“池化”管理,减少业务使用线程时耗费的时间。
问题2:既然是使用池化技术,加快Thread创建和销毁的时间,为什么Java线程池的实现不采用常规池化设计?
灵魂发问!如果先不看下文,你能猜想的到吗?
还是先从 常规池化设计 说起,本质上,Java线程池属于池化思想的一个应用,但又不完全是。
使用Java线程池,一个重要目的就是减少Thread创建和销毁的时间,这也是“池化”对象的目的,就像数据库连接池ConnectionPool、Redis连接池JedisPool等,通过“池化”对象,管理复杂对象的创建和回收。
将需要使用对象时,直接从池子中获取一个,用完直接归还即可。
一般意义上的池化资源,都是下面这样实现的:
class XXXPool{
// 获取池化资源
XXX acquire() {
}
// 释放池化资源
void release(XXX x){
}
}
使用 XXXPool 也很简单:当你需要资源的时候就调用 acquire() 方法来申请资源,用完之后就调用 release() 释放资源。
若你带着这个固有模型来看并发包里线程池相关的工具类时,会很遗憾地发现它们完全匹配不上,Java 提供的线程池里面压根就没有申请线程和释放线程的方法。
为什么线程池没有采用一般意义上池化资源的设计方法呢?主要是Thread不同于一般的池化对象,且在API设计上不允许。
如果线程池采用一般意义上池化资源的设计方法,应该是下面示例代码这样:
//采用一般意义上池化资源的设计方法
class ThreadPool{
// 获取空闲线程
Thread acquire() {
}
// 释放线程
void release(Thread t){
}
}
//期望的使用
ThreadPool pool;
Thread T1=pool.acquire();
//传入Runnable对象
T1.execute(()->{
//具体业务逻辑
......
});
假设我们获取到一个空闲线程 T1,然后该如何使用 T1 呢?你期望的可能是这样:通过调用 T1 的 execute() 方法,传入一个 Runnable 对象来执行具体业务逻辑,就像通过构造函数 Thread(Runnable target) 创建线程一样。可惜的是,你翻遍 Thread 对象的所有方法,都不存在类似 execute(Runnable target) 这样的公共方法。
最终Java线程池,或者说目前业界各编程语言的线程池,普遍采用的都是生产者 - 消费者模式。线程池的使用方是生产者,线程池本身是消费者。
也就是使用方在执行任务时,才按配置数目“产生”线程;产生的线程,在线程池内部被使用。
很多大厂的编码规范都要求必须通过线程池来管理线程。线程池和普通的池化资源有很大不同,线程池实际上是生产者-消费者模式的一种实现,理解生产者-消费者模式是理解线程池的关键所在。
再看JDK实现的线程池,最核心的是 ThreadPoolExecutor,通过名字也能看出来,它强调的是 Executor,而不是一般意义上的池化资源(Pool)。
问题3:线程池ThreadPoolExecutor核心参数
ThreadPoolExecutor 的构造函数非常复杂,如下面代码所示,这个最完备的构造函数有 7 个参数。
ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
可以把线程池类比为一个项目组,而线程就是项目组的成员。
corePoolSize:表示线程池保有的最小线程数。有些项目很闲,但是也不能把人都撤了,至少要留 corePoolSize 个人坚守阵地。
maximumPoolSize:表示线程池创建的最大线程数。当项目很忙时,就需要加人,但是也不能无限制地加,最多就加到 maximumPoolSize 个人。当项目闲下来时,就要撤人了,最多能撤到 corePoolSize 个人
keepAliveTime & unit:上面提到项目根据忙闲来增减人员,那在编程世界里,如何定义忙和闲呢?很简单,一个线程如果在一段时间内,都没有执行任务,说明很闲,keepAliveTime 和 unit 就是用来定义这个“一段时间”的参数。也就是说,如果一个线程空闲了keepAliveTime & unit这么久,而且线程池的线程数大于 corePoolSize ,那么这个空闲的线程就要被回收了。
workQueue:工作队列,和上面示例代码的工作队列同义。
threadFactory:通过这个参数你可以自定义如何创建线程,例如你可以给线程指定一个有意义的名字。
handler:通过这个参数你可以自定义任务的拒绝策略。如果线程池中所有的线程都在忙碌,并且工作队列也满了(前提是工作队列是有界队列),那么此时提交任务,线程池就会拒绝接收。至于拒绝的策略,你可以通过 handler 这个参数来指定。
问题4:线程池的4种拒绝策略
ThreadPoolExecutor 已经提供了以下 4 种策略。
CallerRunsPolicy:提交任务的线程自己去执行该任务。
AbortPolicy:默认的拒绝策略,会 throws RejectedExecutionException。
DiscardPolicy:直接丢弃任务,没有任何异常抛出。
DiscardOldestPolicy:丢弃最老的任务,其实就是把最早进入工作队列的任务丢弃,然后把新任务加入到工作队列。
问题5:线程池有几种状态?
线程池有几种状态,以及是如何转化的,参考 吃透线程池ThreadPoolExecutor源码
问题6:线程池的关闭
问完线程池的状态(线程池生命周期),很可能接着问线程池的关闭,其实我们使用ThreadPoolExecutor,最经常的工作就是线程池的启动和关闭。
线程池的关闭主要是两个方法,shutdown和shutdownNow方法。
shutdown方法会更新状态到SHUTDOWN,不会影响阻塞队列里任务的执行,但是不会执行新进来的任务。同时也会回收闲置的Worker,闲置Worker的定义上面已经说过了。
shutdownNow方法会更新状态到STOP,会影响阻塞队列的任务执行,也不会执行新进来的任务。同时会回收所有的Worker。
ps
上面都是线程池ThreadPoolExecutor的基本使用,关系到ThreadPoolExecutor创建时指定的核心参数、任务拒绝策略,以及线程池的关闭。
下面几个,都是如何更好、更优的使用ThreadPoolExecutor,如何选用、选对合适的线程池,来贴切我们的业务。
问题7:说说几种常见的线程池及使用场景?
newFixedThreadPool (固定数目线程的线程池)
newCachedThreadPool(可缓存线程的线程池)
newSingleThreadExecutor(单线程的线程池)
newScheduledThreadPool(定时及周期执行的线程池)
newFixedThreadPool
核心线程数和最大线程数大小一样
没有所谓的非空闲时间,即keepAliveTime为0
阻塞队列为无界队列LinkedBlockingQueue
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory);
}
newCachedThreadPool
核心线程数为0
最大线程数为Integer.MAX_VALUE
阻塞队列是SynchronousQueue
非核心线程空闲存活时间为60秒
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
threadFactory);
}
newSingleThreadExecutor
核心线程数为1
最大线程数也为1
阻塞队列是LinkedBlockingQueue
keepAliveTime为0
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory));
}
newScheduledThreadPool
最大线程数为Integer.MAX_VALUE
阻塞队列是DelayedWorkQueue
keepAliveTime为0
scheduleAtFixedRate() :按某种速率周期执行
scheduleWithFixedDelay():在某个延迟后执行
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
Executors工具类提供的这四个静态方法,实际工作中,一般都不推荐使用。阿里规范强制使用 ThreadPoolExecutor。
问题8:线程数量如何确定?
CPU密集型 ,线程数=CPU核数+1;
IO 密集型 ,线程数=CPU 核数 * [ 1 +(I/O 耗时 / CPU 耗时)];
实际工作中,我们可能很难按照公式来,或者很难确定IO和CPU耗时比。更合理的方式是通过压测、预估要承受的业务请求数来确定合理线程数。
问题9:你分析过线程池源码吗?
线程池源码的问题,更是层出不穷,如:
提交任务的执行流程;
execute和submit的区别?
如何回收空闲的Worker工作线程?
等等
强烈推荐 吃透线程池ThreadPoolExecutor源码 ,你想要知道的全都有。这里不细展开讲,大概过一下:
提交任务的执行流程:
execute和submit的区别
submit底层就是调用execute()方法。但execute()执行Runnable无返回值,而submit提交Callable有返回值,于是submit内部将callable包装成FutureTask。
如何回收空闲的Worker工作线程
Worker一直getTask() 为空,超过了keepAliveTime 一直没新任务来执行,于是 processWorkerExit来尝试结束自己。推荐的文章里有详解。
本文首发于 公众号 架构道与术(ToBeArchitecturer),欢迎关注、学习更多干货~
推荐阅读
JUC源码
并发工具
19 | CountDownLatch和CyclicBarrier让多线程步调一致
17 | ReadWriteLock:如何快速实现一个完备的缓存?