近年来由于互联网的兴起,所以现在Java 面试题中也经常会问到线程池技术,所以今天我们就说一说面试中经常问道的知识点。
一 、 基础知识
为什么要使用线程池呢?
在实际使用中,线程是很占用系统资源的,如果对线程管理不善很容易导致系统问题。因此,在大多数并发框架中都会使用线程池技术来管理线程,那么使用线程池管理线程主要有下面三点好处:
降低资源消耗。 通过复用已经存在的线程和降低线程关闭的次数来尽可能降低系统的消耗;
提升系统响应速度。通过复用线程,省去创建线程的过程,因此整体上提升了系统的响应速度。
提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统的资源,还会降低系统的稳定性,因此,需要使用线程池管理线程。
Java 线程的整体架构体系
Java中的线程池是通过Executor框架实现的,该框架中用到了Executor,Executors,ExecutorService,ThreadPoolExecutor这几个类 。
三个JDK 线程实现类
Executors.newFixedThreadPool(int):执行长期任务性能好,创建一个线程池,一个池中有N个固定线程,有固定线程数的线程池。
// 代码实现 模拟十个人去银行办理业务,然后有5个窗口进行业务
ExecutorService executorService = Executors.newFixedThreadPool(5); //固定线程数
for (int i = 0; i < 10; i++) {
// Thread.sleep(20);
executorService.execute(()->{
System.out.println(Thread.currentThread().getName()+" 办理业务");
});
}
executorService.shutdown();
Executors.newSingleThreadExecutor():一个一个任务的执行,一池一线程
// 代码实现
ExecutorService executorService = Executors.newSingleThreadExecutor(); //一池子一个工作线程类似银行只有一个窗口
for (int i = 0; i < 10; i++) {
// Thread.sleep(20);
executorService.execute(()->{
System.out.println(Thread.currentThread().getName()+" 办理业务");
});
}
executorService.shutdown();
Executors.newCachedThreadPool(): 执行很
多短期任务,线程池根据需要创建新线程,但是先前构建的线程可用时将他重用起来,可扩容。
// 代码实现
ExecutorService executorService = Executors.newCachedThreadPool(); //可扩容线程池
for (int i = 0; i < 10; i++) {
// Thread.sleep(20);
executorService.execute(()->{
System.out.println(Thread.currentThread().getName()+" 办理业务");
});
}
executorService.shutdown();
以上三个实现方法是Java 给我们带的,但是如果在面试的时候面试官问你线程池的技术你要是管说这三个实现,基本上你就挂了。其实我们看他们三个的底层实现都是一个类那就是ThreadPoolExecutor
ThreadPoolExecutor
那么我们看看那个ThreadPoolExecutor 类里面的内容,下面就是线程池的源码,那我们现在就看看这些源码。
我们先看一下线程池的主要参数,7个参数
corePoolSize :线程池中常驻核心线程数
maximumPoolSize:线程池中能够容纳同时执行最大的线程数,此值必须大于等于1
keepAliveTime: 多余的空闲线程的存货时间,达到keepAliveTime时,多余线程会被销毁直到剩下corePoolSize个线程数为止。
unit:keepAliveTime 的单位
workQueue:任务队列,被提交但未被执行的任务。
threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程,一般默认即可。
handler:拒绝策略,表示当队列满了,并且工作线程大于等于线程池中最大的线程数(maximumPoolSize)时如何拒绝执行的runable的策略。
以上7个参数我们都是必须要掌握的,而且要知道为什么这么设置,所以接下来我们我们要看一下线程池的底层原理。
线程池的底层工作原理(以下很重要,包括上面的图也是很重要的)
在创建线程池后,开始等待请求。
在调用execute()方法添加一个请求任务时,线程池会做出如下判断:
2.1 如果正在运行的线程数量小于corePoolSize,那么马上会创建线程运行任务;
2.2 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列;
2.3 如果这个时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非线程立刻运行这个任务;
2.4 如果队列满了且正在运行的线程数量大于或者等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。
当一个线程完成任务时,它会从队列中取下一个任务来执行。
当一个线程无事可做超过一定的时间(keepAliveTime)时,线程会判断:如果当前运行的线程数量大于corePoolSize,那么这个线程就会被停掉。所以线程池的所有任务完成后,它最终会收缩到corePoolSize的大小。
以上时线程池执行的整体过程,下面要看一下线程的处理过程。
线程池用哪个?生产中如设置合理参数呢 ?
线程池的拒绝策略 。
什么是线程池的拒绝策略呢:等待队列已经排满了,再也塞不下新任务了同时,线程池中的max线程也达到了,无法继续为新任务服务。这个是时候我们就需要拒绝策略机制合理的处理这个问题。
JDK内置的4个拒绝策略(以下内置拒绝策略均实现了
RejectedExecutionHandle接口)
AbortPolicy(默认):直接抛出RejectedExecutionException异常阻止系统正常运行
CallerRunsPolicy:“调用者运行”一种调节机制,该策略既不会抛弃任务,也不
会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量。
DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加人队列中尝试再次提交当前任务。
DiscardPolicy:该策略默默地丢弃无法处理的任务,不予任何处理也不抛出异常。如果允许任务丢失,这是最好的一种策略。
工作中我们选择什么线程池呢? 这个是面试常问的考点,如果你要是回答JDK默认的三种线程池技术你就完蛋了,代表你只是了解一些,根本没有用过。
答案是一个都不用,工作中只能用自定义的。
JDK 都提供了为什么不用呢?
//Executors.newFixedThreadPool() 源码
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
// Executors.newSingleThreadExecutor()
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
//Executors.newCachedThreadPool()
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
- 看看JDK给我们自带的实现,之前我们说过7大参数,那么现在看看5个参数 其中这些参数设计的及其不合理(newCachedThreadPool 中的maximumPoolSize设置的是Integer.MAX_VALUE,有些太大了,这样会导致系统失败的),根本不符合企业的要求,所以我们要自定义。
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
2,
5,
3L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.DiscardPolicy());
for (int i = 0; i < 9; i++) {
//Thread.sleep(20);
threadPoolExecutor.execute(()->{
System.out.println(Thread.currentThread().getName()+" 办理业务");
});
}
threadPoolExecutor.shutdown();
但是这些参数我们要怎么设置呢? 其中最重要的就是配置最大线程数
如果线程池要处理的任务是cpu密集型,那么最大的任务就是cpu核数+ 1(但是我们不能写死了 要使用代码自动获取Runtime.getRuntime().availableProcessors())。
如果线程池要处理的任务是IO密集型,那么最大的任务就是cpu核数/阻塞系数。