目录
一、什么是线程池?
说线程池就要先说池化技术:在面向对象编程中,由于反复创建和销毁线程是十分消耗资源的,所以我们要尽可能减少创建和销毁的次数,池化技术是线程池的核心,就是事先创建若干个可执行的线程放入一个池中,需要的时候就从池中获取线程,使用完毕不用销毁线程而是放回池中,使得线程可以重复利用,从而减少创建和销毁线程对象的开销,并且通过线程池可以管理线程。
线程池需要熟练掌握以下几点:
1、3个方法
2、7种参数
3、4种拒绝策略
二、3个方法
Executors工具类有3大方法来创建线程池,分别是
Executors.newSingleThreadExecutor() //单个线程处理任务
Executors.newFixedThreadPool(参数) //创建一个固定大小的线程池
Executors.newCachedThreadPool() //可伸展, 任务多则线程多, 任务少线程少
注意:最后一个newCachedThreadPool的线程池,底层设置的是Integer.MAX_VALUE,也就是说可以无限大
分别测试一下
package com.yx.pool;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class demo1 {
public static void main(String[] args) {
ExecutorService threadPool = Executors.newSingleThreadExecutor();//单个线程处理任务
// ExecutorService threadPool1 = Executors.newFixedThreadPool(5);//创建一个固定大小的线程池
// ExecutorService threadPool2 = Executors.newCachedThreadPool();//可伸展的,任务多线程多,任务少线程少
try {
for (int i = 0; i < 10; i++) {
//使用线程池创建线程
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName()+"->Ok");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
}
根据底层源码+阿里巴巴开发手册说明,线程池不允许用Executors创建,因为可能会导致OOM(内存溢出)问题。要用ThreadPoolExecutor创建,这三个方法了解一下即可,下面是阿里巴巴开发手册说明。
三、7大参数
上面说到有7大参数,我们来看看哪7大,下面是源码,共有7个
corePoolsize,线程池的基本大小
maximumPoolSize,线程池中允许的最大线程数
keepAliveTime,等待时间(超过这个时间就释放回线程池)
TimeUnit,等待时间的单位,是秒/分/时
BlockingQueue,阻塞队列
Excutors.defaultThreadFactory,创建线程的工厂
defaultHandler,拒绝策略
我们举一个例子让大家明白这7个参数,假设一个银行有5个办理业务的窗口,但平时一般只开两个窗口办业务,如果人多,再开其他窗口
注意:人–任务,业务窗口—线程,等候区–阻塞队列
如图,办理业务的所有窗口就是最大线程数maximumPoolSize,这里是5个,而只开2个窗口,就表示corePoolsize基本大小为2,就是默认一开始开两个窗口。突然人越来越多,等候区也坐满了,就是阻塞队列BlockingQueue满了,并且还在进人
那么银行经理(线程池管理线程)看到人太多,排长队了,于是就通知开新窗口
这时,等候区的人就少了很多,后面可能还有在等候区等候的,但是远远少了很多。慢慢的快到中午了,人数少了很多,不需要这么多窗口了,经理见状等了一等,看着等候区也没啥人,人真的不多了,不需要这么多窗口了,于是就关闭之前开的三个。这个等待时间keepAliveTime,就是等闲下来的时候,再等keepAliveTime,如果超过这个时间,那么就关闭额外开的线程。如keepAliveTime写3,TimeUnit设为秒,就是等待3秒,3秒后释放新开的三个线程,这样就可以做到根据任务量灵活控制,先给出下面代码,其中AbortPolicy是四种拒绝策略的其中一种,关于拒绝策略后面会讲。
package com.yx.pool;
import java.util.concurrent.*;
public class demo2 {
public static void main(String[] args) {
ExecutorService threadPool= new ThreadPoolExecutor(
2,
5,
3,
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
try {
for (int i = 0; i < 10; i++) {
//使用线程池创建线程
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName()+"->Ok");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
}
AbortPolicy是此处的拒绝策略,也就是当线程池中所有线程都被使用,并且阻塞队列也全满了,然后AbortPolicy拒绝策略就会抛出异常,上面就是最大线程数maximumPoolSize=5,阻塞队列大小为3,所以打印了8条信息,而for (int i = 0; i < 10; i++)
表示要执行10个任务,在执行8个以后,还要执行两个,但是此时线程全被占用,阻塞队列也全满了,所以遇到第九个任务就会触发AbortPolicy拒绝策略,这个策略是不处理,抛出异常,每个拒绝策略的拒绝方式都不一样。
四、4种拒绝策略
4.1 AbortPolicy
现在我们来讲这四个策略,四个策略和阻塞队列有关
假设到了下午,人又开始多了起来,等候区又坐了很多人,又需要开窗口了。
但是虽然全部的窗口都开了,人还是在进,等候区也满了,意思就是最大线程数的5个全部都在占用,并且阻塞队列也全占满了,这时就需要拒绝策略了
我们在代码里看看
new ThreadPoolExecutor.AbortPolicy()//银行满了,还有人进来,不处理这个人,抛出异常
AbortPolicy拒绝策略就是不处理,抛出异常
当执行5个任务时,表示2个任务正在被线程处理,3个在阻塞队列等待,也就是2个在办理业务,3个在等候区等候,这时就用两个线程即可
代码运行后,可以看到就只有1和2两个线程在处理,corePoolSize表示线程池的基本大小,这里面基本的线程够用就不会用其他的,只有5个任务,那么只要3个在阻塞队列等,2个占用线程执行即可
当执行7个任务时,就会新调用两个线程,执行代码,这时就1,2,3,4这四个线程,最大线程数已经设为5,所以最多只能有5个任务同时被5个线程处理,阻塞队列最多只能有3个等候(代码中设置为3),所以最大承载为8,max+阻塞队列。
当有9个任务要处理时,最后一个就会抛出异常,RejectedExecutionException
总结:所以AbortPolicy就是线程全部被占用,阻塞队列也满了,达到最大承载时,还有任务进来时,就抛出异常
4.2 CallerRunsPolicy
这里拒绝策略就是"哪来的去哪里",就是这个属于哪个线程的就让它去找这个线程,通俗讲就是我们很忙,没时间,你找别人吧
第9个由于是main下的,只能回去找main线程了,所以又main处理了
4.3 DiscardPolicy
达到最大承载,不抛出异常,直接丢掉再进来的任务
4.4 DiscardOldestPolicy
相当于DiscardPolicy升级版,会判断第一个线程是否处理完任务,如果没有,再丢掉任务,也不会抛出异常,也就是说会再努力争取一下
五、附加思考:maxinumPoolSize如何确定?
1、CPU密集型
由于每个人的电脑能同时并行的线程不一样,这个可以看任务管理器,我这里逻辑处理器是16,就表示支持16个线程并行,也就是8核16线程,那么maxinumPoolSize就可以设置为16,但是这就有一个问题,每个人电脑配置不一样,所以我们可通过代码获取电脑可以并行的线程数,也可以直接在任务管理器看自己电脑性能配置。
2、IO密集型
IO密集型就是通过判断系统中十分耗资源的IO操作,然后定义最大线程大于这个数量即可,一般取它的两倍