线程池的优势
1、降低资源消耗。通过重复利用已创建的线程降低创建和销毁造成的消耗。
2、提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
3、提高线程的可管理性。线程是稀缺资源,如果无限的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配。
总结:
线程复用、控制并发数、管理线程
线程池的使用
数组 Array 数组工具类:Arrays
集合 Collection 集合工具类:Collections
线程 Executor 线程工具类:Executors
线程池的常见三种使用
Executors.newFixedThreadPool(); 创建一个固定大小的线程池
Executors.newSingleThreadExecutor(); 创建单一的线程池
Executors.newCachedThreadPool(); 创建一个带有缓冲的线程池可以自动扩容,用于负载较轻的服务器。
代码实现:
public static void main(String[] args) {
//创建一个固定大小的线程池
ExecutorService executorService = Executors.newFixedThreadPool(5);
try{
for(int i=1;i<=10;i++){ //模仿10次请求
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "\t"+"办理业务");
}
});
}
}finally{
executorService.shutdown(); //用完归还给线程池
}
}
结果:
pool-1-thread-1 办理业务
pool-1-thread-1 办理业务
pool-1-thread-1 办理业务
pool-1-thread-1 办理业务
pool-1-thread-1 办理业务
pool-1-thread-1 办理业务
pool-1-thread-2 办理业务
pool-1-thread-3 办理业务
pool-1-thread-4 办理业务
pool-1-thread-5 办理业务
总结:总共就5个线程,是个用户请求,处理完之后还给线程池,可以复用。
单一线程池代码实现:
Executors.newSingleThreadExecutor();
public static void main(String[] args) {
//创建一个单一线程的线程池
ExecutorService executorService = Executors.newSingleThreadExecutor();
try{
for(int i=1;i<=5;i++){ //模仿5次请求
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "\t"+"办理业务");
}
});
}
}finally{
executorService.shutdown(); //用完归还给线程池
}
}
结果:
pool-1-thread-1 办理业务
pool-1-thread-1 办理业务
pool-1-thread-1 办理业务
pool-1-thread-1 办理业务
pool-1-thread-1 办理业务
总结:线程池中只有一个线程。
随着业务量增长可以实现扩容的线程池代码实现:
Executors.newCachedThreadPool();
public static void main(String[] args) {
//创建一个自动扩容的线程池
ExecutorService executorService = Executors.newCachedThreadPool();
try{
for(int i=1;i<=10;i++){ //模仿10次请求
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "\t"+"办理业务");
}
});
}
}finally{
executorService.shutdown(); //用完归还给线程池
}
}
结果:
pool-1-thread-1 办理业务
pool-1-thread-2 办理业务
pool-1-thread-3 办理业务
pool-1-thread-5 办理业务
pool-1-thread-4 办理业务
pool-1-thread-6 办理业务
pool-1-thread-7 办理业务
pool-1-thread-8 办理业务
pool-1-thread-10办理业务
pool-1-thread-9 办理业务
线程池底层源码分析
其实这三个方法创建线程池,点进去源码其实都是创建一个ThreadPoolExecutor对象。这个对象构造里面可以传7个参数。
如图:
七个参数分析
int corePoolSize :线程池中的常驻核心线程数
int maximumPoolSize :线程池能够容纳同时执行的最大线程数,此值必须大于等于1
long keepAliveTime : 多余的空闲线程存活时间。
TimeUnit unit : 存活时间单位
BlockingQueue workQueue : 阻塞队列,被提交但未被执行的任务
ThreadFactory threadFactory :表示生成线程池中工作线程的线程工厂,用于创建线程一般用默认即可
RejectedExecutionHandler handler : 拒绝策略,表示当队列满了,并且工作线程数达到线程池最大线程数,会触发(4中拒绝策略)。
线程池的工作原理:
分析:
线程池执行execute()方法开启任务,
1、首先由两个核心线程负责执行,
2、如果来的请求大于核心线程数,会被加入到阻塞队列中等待。
3、如果阻塞队列满了,会扩容线程数,直到扩容到线程池最大容纳线程数,
4、如果还来请求就触发拒绝策略(默认用AbortPolicy抛异常)。
生活中的例子:
银行中星期天只有两个窗口开放,就相当于我们的核心线程(corepool)客户数量大于这两个线程的处理能力再来的客户就会被请到候客区,相当于我们的阻塞队列(BlokingQueue),如果等候区满了,就开始分配新的窗口直到线程池满,也就是(maximumPool),就开始触发拒绝策略,拒绝策略会阻止客户请求,相当于保安不让人进来。等到线程池中的空闲线程超过一段时间,就会自动自动销毁。
具体的流程图:
四种拒绝策略的介绍
什么时候会触发拒绝策略:
阻塞队列已满,并且线程池中的线程数达到了max。
AbortPolicy(默认) 直接抛异常阻止系统正常运行。
CallerRunsPolicy:不会抛异常,也不会丢弃任务,而是把任务回退到调用者。
DiscardOldestPolicy:直接丢弃等待最久的任务。
DiscardPolicy:直接丢弃任务不做任何处理。
这四个类均实现了第七个参数(RejectedExecutionHandler)接口。
Executors创建线程池
Executors.newFixedThreadPool(); 创建一个固定大小的线程池
Executors.newSingleThreadExecutor(); 创建单一的线程池
Executors.newCachedThreadPool(); 创建一个带有缓冲的线程池可以自动扩容,用于负载较轻的服务器。
在开发中我们这三种线程池我们都不用。因为这三种的阻塞队列是LinkedBlockingQueue,虽然是有界阻塞队列,但是默认值确实int的最大值也就是2^31-1将近21亿。因此永远都不会满,就会出现任务一直在阻塞队列中得不到处理,会导致OOM(堆)异常。因此要手写线程池。
手写线程池
代码如下:
public static void main(String[] args) {
//JDKThreadPool();
ExecutorService executorService = new ThreadPoolExecutor(
2,
5,
2L,
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
try{
for(int i=1;i<=8;i++){ //模仿10次请求
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "\t"+"办理业务");
}
});
}
}finally{
executorService.shutdown(); //用完归还给线程池
}
}
--最多能同时接受8个请求,超过8个就会触发拒绝策略。直接抛异常。
手写线程池最重要的就是ThreadPoolExecutor类里边传的7个参数。
如何配置合理线程池数
根据您的业务:
调用Runtime.getRuntime().availableProcessors()得到硬件cpu数。
cpu密集型:
最大线程数 = cpu数+1 (尽量减少上下文切换)
io密集型:(经常操作数据库)
io阻塞不高(操作数据库就会阻塞,慢)
最大线程数 = cpu数*2
io阻塞较高:
最大线程数 = cpu数/(1-阻塞系数) 阻塞系数通常0.8~0.9之间。