我们知道创建线程对象,就会在内存中开辟空间,而线程中的任务执行完毕之后,就会销毁。
单个线程的话还好,如果线程的并发数量上来之后,就会频繁的创建和销毁对象。这样,势必会消耗大量的系统资源,进而影响执行效率。
所以,线程池就应运而生。
生产环境使用的场景:有个功能,运算数据量大(最大可一起运算5w条数据,由于业务原因,每条数据需要与数据库交互好几次,还涉及不同的库),属于后台管理类的功能,我们最理想的方式就是使用异步处理的方式,即请求发送到后台后,开启一个线程去处理,不用立即给前端相应处理结果,提示正在处理即可,如果操作量大的话,这时我们就可以使用线程池了
实际开发中,我们都是使用自定义线程池的(阿里巴巴开发手册也是这么建议的),至于为什么,后面揭晓
参数 | 说明 |
---|---|
corePoolSize | 核心线程 |
maximumPoolSize | 最大线程数 |
keepAliveTime | 存活时间 |
timeUnit | 时间单位 |
workQueue | 任务队列 |
threadFactory | 线程工厂 |
rejectedExecutionHandler | 拒绝策略 |
corePoolSize:的作用是代表要开启核心线程数量,核心线程会一直保留。
maximumPoolSize:的作用是最大可以创建的线程数量,当你的所有核心线程都在工作状态时,此时如果有新的任务需要执行,系统就会创建新的线程来执行任务(在队列已满的前提条件下)。
keepAliveTime:代表新开启的线程如果执行完毕后可以存活多长时间,如果在设置的时间内没有任务使用该线程,则线程资源就会归还操作系统。
timeUnit: 代表线程存活的时间单位。
workQueue: 任务队列,如果正在执行的任务超过了核心线程数,可以存放在队列中,当线程池中有空闲资源就可以从队列中取出任务继续执行。
任务队列类型(阻塞队例)有如下几种LinkedBlockingQueue 、DelayedWorkQueue、ArrayBlockingQueue、 SynchronousQueue、 TransferQueue,使用不同的队列就会产生不同类型的线程池。(任务队列的知识,我会单独的去写一篇文章)
threadFactory: 线程工厂,他的作用是用来产生线程的,可以自定义线程的类型,比如我们可以定义线程组名称,在jstack问题排查时,非常有帮助。
rejectedExecutionHandler: 拒绝策略, 当所有线程都在忙,并且任务队列处于满任务的状态,则会执行拒绝策略。
拒绝策略可以自定义,JDK默认给我们提供了4种,分别是:
AbortPolicy:直接拒绝,并抛出异常,这也是默认的策略。
CallerRunsPolicy:直接让调用execute方法的线程去执行此任务。
DiscardOldestPolicy:丢弃最老的未处理的任务,然后重新尝试执行当前的新任务。
DiscardPolicy:直接丢弃当前任务,但是不抛异常。
总结一下线程池的执行过程。
我们再来看下jdk 给我们提供了哪些线程池:
创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
单个线程线程池,只有一个线程的线程池,阻塞队列使用的是LinkedBlockingQueue,若有多余的任务提交到线程池中,则会被暂存到阻塞队列,待空闲时再去执行。按照先入先出的顺序执行任务
可以看到阻塞队例 使用的是LinkedBolckingQueue,且默认大小为Integer.MAX_VALUE,这样的话,如果有大量请求到来,会放入到这个任务队列里,可能会导致OOM;
private static void newSingleThreadExecutor() {
//创建一个单线程化的线程池
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
for (int i = 0; i < 10; i++) {
final int index = i;
singleThreadExecutor.execute(new Runnable() {
public void run() {
try {
//结果依次输出,相当于顺序执行各个任务
System.out.println(Thread.currentThread().getName()+"正在被执行,打印的值是:"+index);
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
}
pool-1-thread-1正在被执行,打印的值是:0
pool-1-thread-1正在被执行,打印的值是:1
pool-1-thread-1正在被执行,打印的值是:2
pool-1-thread-1正在被执行,打印的值是:3
pool-1-thread-1正在被执行,打印的值是:4
pool-1-thread-1正在被执行,打印的值是:5
pool-1-thread-1正在被执行,打印的值是:6
pool-1-thread-1正在被执行,打印的值是:7
pool-1-thread-1正在被执行,打印的值是:8
pool-1-thread-1正在被执行,打印的值是:9
总结:单线程化的线程池在并发量高的情况下会因为阻塞队列为LinkedBlockingQueue 的原因,可能会导致OOM.
定长线程池的大小最好根据系统资源进行设置。如Runtime.getRuntime().availableProcessors()
固定大小的线程池,可以指定线程池的大小,该线程池corePoolSize和maximumPoolSize相等,阻塞队列使用的是LinkedBlockingQueue,大小为整数最大值。
该线程池中的线程数量始终不变,当有新任务提交时,线程池中有空闲线程则会立即执行,如果没有,则会暂存到阻塞队列。对于固定大小的线程池,不存在线程数量的变化。
同时使用无界的LinkedBlockingQueue来存放执行的任务。当任务提交十分频繁的时候,LinkedBlockingQueue迅速增大,存在着耗尽系统资源的问题。
而且在线程池空闲时,即线程池中没有可运行任务时,它也不会释放工作线程,还会占用一定的系统资源,需要shutdown
private static void newFixedThreadPoolTest() {
System.out.println(Runtime.getRuntime().availableProcessors());
// 创建一个可重用固定个数的线程池
ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(3);
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
newFixedThreadPool.execute(new Runnable() {
public void run() {
// 打印正在执行的缓存线程信息
System.out.println(Thread.currentThread().getName() + "正在被执行");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
}
6
pool-1-thread-1正在被执行
pool-1-thread-2正在被执行
pool-1-thread-3正在被执行
pool-1-thread-1正在被执行
pool-1-thread-2正在被执行
pool-1-thread-3正在被执行
pool-1-thread-1正在被执行
pool-1-thread-1正在被执行
pool-1-thread-3正在被执行
pool-1-thread-3正在被执行
总结:核心线程数和最大线程数一样,但是因为阻塞队列使用的LinkedBlockingQueue,在并发高的场景下,一样可能导致OOM,或者资源耗尽。
创建一个定长线程池,支持定时及周期性任务执行
定时线程池,该线程池可用于周期性地去执行任务,通常用于周期性的同步数据。
scheduleAtFixedRate:是以固定的频率去执行任务,周期是指每次执行任务成功执行之间的间隔。
schedultWithFixedDelay:是以固定的延时去执行任务,延时是指上一次执行成功之后和下一次开始执行的之前的时间。
private static void newScheduledThreadPoolTest() {
//创建一个定长线程池,支持定时及周期性任务执行——延迟执行
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
//创建一个单列线程池,支持定时及周期性任务执行——延迟执行 dubbo延迟暴露服务的原理
ScheduledExecutorService scheduledThreadPool2 = Executors.newSingleThreadScheduledExecutor(
new NamedThreadFactory("dubbo", true));
//延迟1秒执行
scheduledThreadPool.schedule(new Runnable() {
public void run() {
System.out.println("延迟1秒执行");
}
}, 1, TimeUnit.SECONDS);
//延迟1秒后每3秒执行一次
scheduledThreadPool.scheduleAtFixedRate(new Runnable() {
public void run() {
System.out.println(Thread.currentThread().getName() + "延迟1秒后每3秒执行一次");
}
}, 1, 3, TimeUnit.SECONDS);
scheduledThreadPool2.schedule(new Runnable() {
public void run() {
System.out.println(Thread.currentThread().getName() +"延迟1秒执行");
}
}, 1, TimeUnit.SECONDS);
}
延迟1秒执行
pool-1-thread-2延迟1秒后每3秒执行一次
dubbo-thread-1延迟1秒执行
pool-1-thread-1延迟1秒后每3秒执行一次
pool-1-thread-2延迟1秒后每3秒执行一次
pool-1-thread-3延迟1秒后每3秒执行一次
pool-1-thread-3延迟1秒后每3秒执行一次
定时线程池核心线程数可指定,但是最大线程数为Integer.MAX_VALUE,显而易见也会导致OOM(资源耗尽)。
可缓存线程池,先查看线程池中有没有以前建立的线程,如果有就直接使用,如果没有新建一个线程加入线程池中,可缓存线程池
通常用于执行一些生存期很短的异步型任务;线程池为无限大,当执行当前任务时上一个任务已经完成,会复用执行上一个任务的线程,而不用每次新建线程
缓存的线程默认存活60秒。线程的核心池corePoolSize大小为0,核心池最大为Integer.MAX_VALUE,阻塞队列使用的是SynchronousQueue。
是一个直接提交的阻塞队列,他总会迫使线程池增加新的线程去执行新的任务。
在没有任务执行时,当线程的空闲时间超过keepAliveTime(60秒),则工作线程将会终止被回收,当提交新任务时,
如果没有空闲线程,则创建新线程执行任务,会导致一定的系统开销。
如果同时又大量任务被提交,而且任务执行的时间不是特别快,那么线程池便会新增出等量的线程池处理任务,这很可能会很快耗尽系统的资源。
private static void NewCachedThreadPoolDemo() {
// 创建一个可缓存线程池
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
try {
// sleep可明显看到使用的是线程池里面以前的线程,没有创建新的线程
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
cachedThreadPool.execute(new Runnable() {
public void run() {
// 打印正在执行的缓存线程信息
System.out.println(Thread.currentThread().getName()
+ "正在被执行");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
}
}
pool-1-thread-1正在被执行
pool-1-thread-2正在被执行
pool-1-thread-1正在被执行
pool-1-thread-2正在被执行
pool-1-thread-2正在被执行
pool-1-thread-2正在被执行
pool-1-thread-2正在被执行
pool-1-thread-2正在被执行
pool-1-thread-1正在被执行
pool-1-thread-2正在被执行
缓存线程池没有核心线程,新的请求来时先查看线程池中有没有以前建立的线程,如果有就直接使用,如果没有新建一个线程加入线程池中,最大线程数为Integer.MAX_VALUE,显而易见也会导致OOM(资源耗尽)。
可以看出四种线程池都是通过调用如下构造函数来返回一个线程池,而且因为最大线程数以及阻塞队里的原因,在并发搞得场景下都可能会导致OOM
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
阿里巴巴开发手册也是建议不要使用jdk提供的四中线程池,而是使用自定义的
jdk 1.8 引入的
可以传入线程的数量,不传入,则默认使用当前计算机中可用的cpu数量
能够合理的使用CPU进行对任务操作(并行操作)
适合使用在很耗时的任务中
底层用的ForkJoinPool 来实现的。 ForkJoinPool的优势在于,可以充分利用多cpu,多核cpu的优势,把一个任务拆分成多个“小任务”分发到不同的cpu核心上执行,执行完后再把结果收集到一起返回。
public static ExecutorService newWorkStealingPool() {
return new ForkJoinPool
(Runtime.getRuntime().availableProcessors(),//获取当前电脑核数
ForkJoinPool.defaultForkJoinWorkerThreadFactory,
null, true);
}
private static void newWorkStealingPool() throws InterruptedException {
//格式化
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
//AtomicInteger用来计数
AtomicInteger number = new AtomicInteger();
ExecutorService executorService = Executors.newWorkStealingPool();
for (int i = 0; i < 12; i++) {
executorService.execute(() -> {
System.out.println("第" + number.incrementAndGet() + "周期线程运行当前时间【" + sdf.format(new Date()) + "】");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
System.out.println("主线程运行当前时间【" + sdf.format(new Date()) + "】");
TimeUnit.SECONDS.sleep(3);
}
这里没有设置线程数,所以会根据你设备的cpu核心数来创建线程,我的是6核cpu,所以会每隔1秒输出6个数字。
主线程运行当前时间【2021-03-30 10:17:24】
第5周期线程运行当前时间【2021-03-30 10:17:24】
第3周期线程运行当前时间【2021-03-30 10:17:24】
第2周期线程运行当前时间【2021-03-30 10:17:24】
第1周期线程运行当前时间【2021-03-30 10:17:24】
第4周期线程运行当前时间【2021-03-30 10:17:24】
第6周期线程运行当前时间【2021-03-30 10:17:24】
第7周期线程运行当前时间【2021-03-30 10:17:25】
第8周期线程运行当前时间【2021-03-30 10:17:25】
第9周期线程运行当前时间【2021-03-30 10:17:25】
第10周期线程运行当前时间【2021-03-30 10:17:25】
第11周期线程运行当前时间【2021-03-30 10:17:25】
第12周期线程运行当前时间【2021-03-30 10:17:25】
这个ThreadFactory是干嘛的呢:我们一般用来设定自定义线程池中线程的名字(前缀)
如果我们不指定,线程池会使用默认的,但是这样不便于日志分析,所以生产环境中我们都会设定
private static void personalExecutor(){
// 创建数组型缓冲等待队列
BlockingQueue bq = new ArrayBlockingQueue(10);
// ThreadPoolExecutor:创建自定义线程池,池中保存的线程数为3,允许最大的线程数为6
ThreadPoolExecutor tpe = new ThreadPoolExecutor(3, 6, 50, TimeUnit.MILLISECONDS, bq);
// 创建6个任务
Runnable t1 = new TempThread();
Runnable t2 = new TempThread();
Runnable t3 = new TempThread();
Runnable t4 = new TempThread();
Runnable t5 = new TempThread();
Runnable t6 = new TempThread();
// 6个任务在分别在3个线程上执行
tpe.execute(t1);
tpe.execute(t2);
tpe.execute(t3);
tpe.execute(t4);
tpe.execute(t5);
tpe.execute(t6);
// 关闭自定义线程池
tpe.shutdown();
}
public static class TempThread implements Runnable {
@Override
public void run() {
// 打印正在执行的缓存线程信息
System.out.println(Thread.currentThread().getName() + "正在被执行");
try {
// sleep一秒保证3个任务在分别在3个线程上执行
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
pool-1-thread-1正在被执行
pool-1-thread-2正在被执行
pool-1-thread-3正在被执行
pool-1-thread-2正在被执行
pool-1-thread-1正在被执行
pool-1-thread-3正在被执行
看一个实际生产环境的自定义线程池(细节代码经过处理):
//最大线程数 可配置,如没有配置使用默认值16
int maxThread = XXX;
//核心线程数,可根据服务器计算出最优的
int corePoolSize = 5;
if (corePoolSize > maxThread) {
corePoolSize = maxThread;
}
//阻塞队里大小,可配置,如没有配置使用默认值32
int maxBlockingSize = XXX;
ArrayBlockingQueue queue = new ArrayBlockingQueue(maxBlockingSize);
// 创建自定义线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(corePoolSize, maxThread, 10, TimeUnit.MINUTES,
queue, new NamedThreadFactory("XXXThread"));
我们改下我们上面的代码,加入new NamedThreadFactory("CJThread")看看效果;
NamedThreadFactory 是dubbo工程里的一个实现了jdk ThreadFactory接口的类,(当然ThreadFactory也有其他第三方的各种实现)
CJTest-thread-1正在被执行
CJTest-thread-3正在被执行
CJTest-thread-2正在被执行
CJTest-thread-1正在被执行
CJTest-thread-3正在被执行
CJTest-thread-2正在被执行
至于线程池的原理以及阻塞队列后面单独出文章分析
线程池原理分析——线程是如何做到复用的
浅析ArrayBlockingQueue 和 LinkedBlockingQueue的阻塞原理