近期听到了很多求职小伙伴的心声,线程池不是很了解,而且只知道如果运用,更别提深层研究了,导致每次都被面试官问的哑口无言。那我就做回天使吧,给大家系统一下。
我的套路是从原理----应用----源码;让你彻底弄成线程池。
What?
线程池,就是一个池子嘛,但是这个池子不是装水的,而是装线程的;
想想我们平常水池的是啥,还不是为了用水的时候方便,节省了来回运水的时间;而且我们可以让水可以循环利用,水资源多么宝贵呀,节约了水资源;同理,当面试官再问你,线程池好处的时候,是不是就可以结合“水池作用”,跟面试官装一把了;
Why?
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- **提高响应速度。**当任务到达时,任务可以不需要等到线程创建就能立即执行。
- **提高线程的可管理性。**线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。
分类?
多线程是采用了Executor框架。具体的类图关系如下;ThreadPoolExecutor是线程池创建的核心类。然后线程池的具体创建方式如下几种:
1. newCachedThreadPool
1.1.what?
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
1.2 代码实例
public class CacheThread {
public static void main(String[] args) {
ExecutorService cachThread= Executors.newCachedThreadPool();
for (int i = 0; i <10 ; i++) {
final int temp=i;
cachThread.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"当前线程的==="+temp);
}
});
}
}
1.3 执行结果
由上图,我们也得出了结果:虽然线程池为无限大,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程。
1.4 源码分析
1.5 缺点
最大线程数为Integer.MAX_VALUE,可能会创建很多的线程,然后导致OOM异常;
2.newFixedThreadPool
2.1 What?
创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
2.2 代码实例
public class FixThread {
public static void main(String[] args) {
// 有5个线程
ExecutorService fixThread= Executors.newFixedThreadPool(5);
for (int i = 0; i <10 ; i++) {
final int temp=i;
fixThread.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"当前线程=="+temp);
}
});
}
}
}
2.3 结果
我们可以得出结论:因为线程池的大小是5,所以它的好处是可以循环利用线程,合理的分配资源,去执行任务;
2.4 源码分析
通过源码,可以看出,它是核心线程数和最大线程数是相同的,采用的是LinkedBlockQueue<>队列;
2.5 缺点
LinkedBlockQueue<>没有设定范围,它的值为Inter.Max_Value,这样类似于无界队列,然后会存储很多的线程,造成OOM异常;
3. newScheduledThreadPool
3.1 what
创建一个定长线程池,支持定时及周期性任务执行
3.2 代码示例
public static void main(String[] args) {
ScheduledExecutorService scheduledExecutorService= Executors.newScheduledThreadPool(5);
for (int i = 0; i < 10; i++) {
final int temp=i;
scheduledExecutorService.schedule(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"当前线程"+temp);
}
},4, TimeUnit.SECONDS);
}
}
}
3.3 执行结果
延迟4秒执行,运算出下面结果
3.4 源码分析
通过源码分析,它底层也是 调用的ThreadPoolExecutor。
3.5 缺点
同newCachedThreadPool一样,最大线程数为Integer.MAX_VALUE,可能会创建很多的线程,然后导致OOM异常;
4、newSingleThreadExecutor
4.1 what?
创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
4.2 代码示例
public class SingleThread {
public static void main(String[] args) {
ExecutorService singleThread= Executors.newSingleThreadExecutor();
for (int i = 0; i <10 ; i++) {
final int temp=i;
singleThread.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"当前线程"+temp);
}
});
}
}
}
4.3 运行结果
只有一个线程在执行;
4.4 源码分析
4.5 缺点
同newFixThreadPool一样,LinkedBlockQueue<>没有设定范围,它的值为Inter.Max_Value,这样类似于无界队列,然后会存储很多的线程,造成OOM异常;
线程池
由上面的几中线程的创建方式,我们可以看出,他们底层都调用了 “ThreadPoolExecutor”,上面的方法都有弊端,所以我们可以用ThreadPoolExecutor然后自定义线程池,根据我们项目中的需要;
1.线程池原理
- 判断线程池里的核心线程是否都在执行任务,如果不是(核心线程空闲或者还有核心线程没有被创建)则创建一个新的工作线程来执行任务。如果核心线程都在执行任务,则进入下个流程。
- 线程池判断工作队列是否已满,如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程。
- 判断线程池里的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。
2. 自定义线程池
【案例一】此时运行5个任务===最大线程数+队列数
public class ThreadPoolExcutor {
public static void main(String[] args) {
ThreadPoolExecutor threadPoolExecutor= new ThreadPoolExecutor(1,
2,
60L,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(3),
new ThreadPoolExecutor.AbortPolicy()
);
//此时运行5个任务,没有超过 最大线程数+队列数
for (int i = 1; i <=5 ; i++) {
final int temp=i;
taskThread t1=new taskThread("任务线程");
threadPoolExecutor.execute(t1);
}
}
}
class taskThread implements Runnable{
private String taskName;
public taskThread(String taskName){
this.taskName=taskName;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"当前现成的任务"+taskName);
}
}
运行的结果:
【案例二】:此时用了两个线程,进行了复用,但是我们如果尝试一下运行6个任务(超过了最大线程数+队列的数据);
运行结果:
由结果可知,我们也就更进一步的明白了线程池的运行原理,当我们执行的任务超过了 最大线程数+队列中的线程,然后线程池就会采用一些拒绝策略,进行拒绝;
3、 拒绝策略
- AbortPolicy(默认):直接抛出RejectedException异常,阻止系统正常运行;
- CallerRunsPolicy:不会抛弃任务也不会抛出异常,而是回退给调用者;
- DiscardOldestPolicy:丢弃等待最久的任务,然后把当前任务加入队列中;
- DiscardPolicy:直接丢弃任务,不做任何处理。
上面的例子中,我采用的是默认AbortPolicy的拒绝策略,然后直接抛异常,下面我演示一下用 “CallerRunsPolicy”进行调用,查看一下结果;
4. 合理配置线程池
我们在项目中使用线程池需要自定义最大线程数,那我们可以根据以下两点进行判断;
1 . IO密集型: 即该任务需要大量的IO,即大量的阻塞。在单线程上运行IO密集型的任务会导致浪费大量的;
这时候我们可以设定:最大线程数=CPU核数 * 2;
2 CPU密集型:该任务需要大量的运算,而没有阻塞,CPU一直全速运行。
这时候我们可以设定:最大线程数=CPU核数 ;
5.应用
在项目中我们需要多个任务的处理的时候,比如 使用多线程去消费mq中的数据等等;
总结
在跟面试官吹的时候,可以借鉴上面的思虑然后在加上在项目中的经验,调理清晰;让面试官也大开眼界,不选你,选谁!