写在开头
之前给一个大四正在找工作的学妹发了自己总结的关于Java并发中线程池的面试题集,总共18题,将之取名为《Java并发编程之线程池十八问》,今天聊天时受了学妹的夸赞,心里很开心,毕竟自己整理的东西对别人起到了一点帮助,记录一下!
Java并发编程之线程池十八问
经过之前的学习,我们知道在Java中创建一个线程需要调用操作系统内科API,操作系统要为创建的线程分配一系列的资源,成本很高,因此,如果在一个程序中,我们频繁的创建线程和销毁线程,资源占用巨大,性能很差,因此,便但成了 池化思想
,将创建的线程放入一个池中管理,在Java中除了线程池,数据库连接、HTTP连接也用到了池化思想!
我们基于此,整理了线程池相关的常用面试题集,合计十八道,通过这样的方式充分的了解和学习线程池。
第一问:什么是线程池?
所谓 线程池
,就是一个可以管理若干线程的容器,当有任务需要处理时,会提交到线程池的任务队列中,由线程池分配空闲的线程处理任务,处理完任务的线程不会被销毁,而是在线程池中等待下一个任务。
第二问:为什么要用线程池?
至于为什么要用线程池,可以从如下几点回答面试官:
- 降低资源消耗: 频繁的创建与销毁线程,占用大量资源,线程池的出现避免了这种情况,减少了资源的消耗;
- 提高响应速度: 因为线程池中的线程处于待命状态,有任务进来无需等待线程的创建就能立即执行(前提是有空闲线程,任务量巨大,还是需要排队的哈);
- 更好的管理线程: 线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
第三问:如何创建一个线程池,为什么不推荐使用Executors?
在这里我们提供2种构造线程池的方法:
方法一: 通过ThreadPoolExecutor构造函数来创建(首选)
这是JDK中最核心的线程池工具类,在JDK1.8中,它提供了丰富的可设置的线程池构造参数,供我们设计不同的线程池,如下:
通过构造方法 ,可以给整个线程池设置大小、等待队列、非核心线程存活时间、创建线程的工厂类、拒绝策略等,具体参数描述可见 第六问
,它们在线程池中所对应的关系,可见下图。
方法二: 通过 Executor 框架的工具类 Executors 来创建(不推荐)
Executors 是java并发工具包中的一个静态工厂类,在JDK1.5时被创造出来,提供了丰富的创造线程池的方法,通过它可以创建多种类型的线程池。
- newFixedThreadPool:创建定长线程池,该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。当线程发生错误结束时,线程池会补充一个新的线程;
- newCachedThreadPool:创建可缓存的线程池,如果线程池的容量超过了任务数,自动回收空闲线程,任务增加时可以自动添加新线程,所有线程在当前任务执行完毕后,将返回线程池进行复用,线程池的容量不限制;
- newScheduledThreadPool:创建定长线程池,可执行周期性的任务;
- newSingleThreadExecutor:创建单线程的线程池,只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务,线程异常结束,会创建一个新的线程,能确保任务按提交顺序执行;
- newWorkStealingPool:任务可窃取线程池,不保证执行顺序,当有空闲线程时会从其他任务队列窃取任务执行,适合任务耗时差异较大的场景。
为何很多大厂都禁止使用Executors 创建线程池呢?
如果大家跟入到Executors这些方法的底层实现中去看一眼的话,立马就知道原因了,像FixedThreadPool 和 SingleThreadExecutor这两个方法内使用的是无界的 LinkedBlockingQueue存储任务,任务队列最大长度为 Integer.MAX_VALUE,这样可能会堆积大量的请求,从而导致 OOM。
而CachedThreadPool使用的是同步队列 SynchronousQueue, 允许创建的线程数量也为 Integer.MAX_VALUE ,如果任务数量过多且执行速度较慢,可能会创建大量的线程,从而导致 OOM,其他的方法所提供的均是这种无界任务队列
,在高并发场景下导致OOM的风险很大,故大部分的公司已经不建议采用Executors提供的方法创建线程池了。
第四问:如何给线程池命名?
如果我们的项目模块较多,在运行时调用了不同模块的线程池,为了在发生异常后快速定位问题,我们一般会在构建线程池时给它一个名字,这里我们提供几种线程池命名的方法。
方法一: 通过Spring 框架提供的CustomizableThreadFactory命名
ThreadFactory springThreadFactory = new CustomizableThreadFactory("Spring线程池:");
ExecutorService exec = new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(10),springThreadFactory);
exec.submit(() -> {
log.info(exec.toString());
});
方法二: 通过Google guava工具类提供的ThreadFactoryBuilder命名
//链式调用
ThreadFactory guavaThreadFactory = new ThreadFactoryBuilder().setNameFormat("guava线程池:").build();
ExecutorService exec = new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(10),guavaThreadFactory );
exec.submit(() -> {
log.info(exec.toString());
});
其实还有一个是Apache commons-lang3 提供的 BasicThreadFactory工厂类,也可以给线程池命名,咱这里就不贴代码了,原因是他们的本质都是通过Thread 的setName()方法实现的!所以,我们其实自己也可以设计一个工厂类也实现线程池的命名操作!
方法三: 自定义工厂类实现线程池命名
先定义一个工厂类,通过实现ThreadFactory的newThread方法,完成命名。
public class MyThreadFactory implements ThreadFactory {
private final AtomicInteger threadNum = new AtomicInteger();
private final String name;
/**
* 创建一个带名字的线程池生产工厂
*/
public MyThreadFactory(String name) {
this.name = name;
}
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setName(name + "-" + threadNum.incrementAndGet());
return t;
}
}
调用一下看看结果:
@Slf4j
public class Test {
public static void main(String[] args) {
MyThreadFactory myThreadFactory = new MyThreadFactory("javaBuild-pool");
ExecutorService exec = new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(10),myThreadFactory);
exec.submit(() -> {
log.info(exec.toString());
});
}
}
输出:
17:46:37.387 [javaBuild-pool-1] INFO com.javabuild.server.pojo.Test