线程池
一、线程池介绍
线程池:事先创建一组线程供程序调用,并且可以对其复用
二、为什么需要线程池
- 降低资源消耗: 通过重复利用已创建的线程降低线程创建和销毁造成的消耗
- 提高响应速度:当出现新的请求任务时,不需要等待线程创建即可立即使用
- 提高线程的可管理性 : 可以进行统一的分配,调优和监控
三、使用线程池
- Java中提供的线程池为:
Executors
- Executors线程池类位于
java.util.concurrent
包下,实现了Executor
接口 - 我们可以自定义线程池:需要先了解以下知识点
其中我们需要学习三大方法、七大参数、四大拒绝策略
1.1、Executors的三大方法
newSingleThreadExecutor:创建的线程池例只有一条线程
package com.migu;
import java.util.concurrent.*;
public class Test {
public static void main(String[] args) {
ExecutorService executor = Executors.newSingleThreadExecutor();
try {
for (int i = 0; i < 5; i++) {
// 通过execute来获取线程
executor.execute(()->{
System.out.println("当前线程为: " + Thread.currentThread().getName());
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
executor.shutdown(); // 关闭整个线程池(一般也是同服务器共生死,这边只是简单介绍下)
}
}
}
打印输出:
newFixedThreadPool:可指定线程池中的线程数量
newCachedThreadPool:创建一个自动伸缩的线程池
1.2、ThreadPoolExecutor的七大参数
从Executors的三大方法的源码进行分析
- newSingleThreadExecutor:
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
- newFixedThreadPool:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
- newCachedThreadPool:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
我们可以发现,三个创建线程池的方法本质上都是创建了ThreadPoolExecutor
对象。
ThreadPoolExecutor的构造方法:需要对七大参数进行设置
public ThreadPoolExecutor(int corePoolSize, // 核心线程池大小
int maximumPoolSize, // 最大核心线程池大小
long keepAliveTime, // 空闲线程最大存活时间
TimeUnit unit, // 存活时间单位
BlockingQueue<Runnable> workQueue, // 阻塞队列(上面提到过)
ThreadFactory threadFactory, // 线程工厂
RejectedExecutionHandler handler) { // 拒绝策略
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
其中在阿里巴巴开发规范中也提出尽量不要使用Executors
的三大方法,可以发现在调用newCachedThreadPool
方法中,里面最大核心线程池大小被设置为int
类型的最大值,达到21
亿多,可能会导致堆积大量的线程导致OOM
下图可以粗略的解释上面的情况,来自哔哩哔哩狂神说秦老师
而空闲线程指的大概就是:最大线程 减去 核心线程,且当这部分并没有任何任务在执行时,就可认为是空闲线程,此时我们设置的keepAliveTime
和unit
就发挥了作用,可以对其回收
1.3、四大拒绝策略
从七大参数的最后一个拒绝策略参数handler
可知,这是一个RejectedExecutionHandler
接口,其中有以下实现类
这些实现类都位于ThreadPoolExecutor的内部类中,为静态内部类
AbortPolicy
- 这是Executors三大方法的默认实现
- 会抛出异常
CallerRunsPolicy
- 对阻塞队列满时交由父线程处理该任务
DiscardOldestPolicy
- 当阻塞队列满时,弹出阻塞队列的头部获取者,并进入该队列【看源码可知】
- 不会抛出异常
DiscardPolicy
- 当阻塞队列满时,直接拒绝,也不抛出异常
1.4、自定义线程池
1.4.1、采用AbortPolicy策略
AbortPolicy:该拒绝策略会对最大承载外的线程抛出异常
- 阻塞队列可自由指定
package com.migu;
import java.util.concurrent.*;
public class Test {
public static void main(String[] args) {
// 总共创建五条线程,先开辟两条线程,交由请求任务使用
// 当阻塞队列满时,开辟所有线程
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, // 核心线程池大小
5, // 总线程数
10, // 空闲线程存活时间 10
TimeUnit.SECONDS, // 秒
new LinkedBlockingDeque<>(3), // 双端阻塞队列,也可以用Array
Executors.defaultThreadFactory(), // 使用默认的工厂
new ThreadPoolExecutor.AbortPolicy() // 线程池提供的策略
);
try {
// 当阻塞队列满时,AbortPolicy拒绝策略抛出异常
for (int i = 0; i < 8; i++) {
executor.execute(()->{
System.out.println("当前线程为: " + Thread.currentThread().getName());
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
executor.shutdown();
}
}
}
以上代码i
取8
,也就是存在8
个任务,那么总线程数5
+ 阻塞队列3
正好可以处理
打印输出:
以上代码当i
取9
时,AbortPolicy
策略将抛出异常:超出最大承载
AbortPolicy
策略会抛出异常,当该策略抛出异常时,该线程池直接无法使用
分别测试两种实现:
1、线程池可继续使用:
try {
// 当阻塞队列满时,AbortPolicy拒绝策略抛出异常
for (int i = 0; i < 15; i++) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
executor.execute(()->{
System.out.println("当前线程为: " + Thread.currentThread().getName());
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
executor.shutdown();
}
控制台打印:
2、抛出异常时,线程池其余线程再也无法使用:
try {
// 当阻塞队列满时,AbortPolicy拒绝策略抛出异常
for (int i = 0; i < 15; i++) {
executor.execute(()->{
// 在线程内部暂停测试
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("当前线程为: " + Thread.currentThread().getName());
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
executor.shutdown();
}
控制台打印:
1.4.2、采用CallerRunsPolicy策略
这边就直接打印输出:(取i
为9
时)
记住线池的线程时可复用的,请求任务执行完毕会回收回去
1.5、小结
一、最大线程池如何定义
1、CPU密集型
-
查看当前服务器有几条逻辑处理器,就定义几条线程
-
Runtime.getRuntime().availableProcessors() // 获取当前计算机的线程核数
2、IO密集型
- 判断当前应用程序大概会有几条大型任务的线程
- 比如是是调用IO任务相关的线程【这玩意就会耗资源】此时存在
15
条,那么就定义30
条线程
- 比如是是调用IO任务相关的线程【这玩意就会耗资源】此时存在