线程池
线程池概述
线程池是一个可以去复用线程的技术。
不使用线程池的问题:
用户每发起一个请求,后台就需要创建一个新线程来处理,下次新任务来了肯定又要创建新线程处理的,创建新线程的开销是很大的,并且请求过多时,肯定会产生大量的线程出来,这样会严重影响系统的性能。
创建线程池
JDK5.0起提供了代表线程池的接口:ExecutorService。
创建线程池的方式:
方式一:使用ExecutorService的实现类ThreadPoolExecutor自创建一个线程池对象。
ExecutorService=》ThreadPoolExecutor
方式二:使用Executors(线程池的工具类)调用方法返回不同特点的线程池对象。
通过ThreadPoolExecutor创建线程池
ThreadPoolExecutor类提供的构造器 | 作用 |
---|---|
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue |
使用初始化参数创建一个线程池对象 |
参数说明: | |
参数一:corePoolSize:指定线程池的核心线程的数量。 | |
参数二:maximumPoolSize:指定线程池的最大线程数量。 | |
参数三:keepAliveTime:指定临时线程的存活时间。 | |
参数四:unit:指定临时线程存活的时间单位(秒、分、时、天) | |
参数五:workQueue:指定线程池的任务队列。 | |
参数六:threadFactory:指定线程池的线程工厂。 | |
参数七:handler:指定线程池的任务拒绝策略(线程都在忙,任务队列也满了的时候,新任务来了该怎么处理) |
ExecutorService的常用方法:
方法名称 | 说明 |
---|---|
void execute(Runnable command) | 执行 Runnable 任务 |
Future |
执行Callable 任务,返回未来任务对象,用于获取线程返回的结果 |
void shutdown() | 等全部任务执行完毕后,再关闭线程池! |
List |
立刻关闭线程池,停止正在执行的任务,并返回队列中未执行的任务 |
线程池处理Runnable任务代码
package com.example.demo.MyExecutor;
import java.util.concurrent.*;
public class ExecutorServiceDemo1 {
public static void main(String[] args) {
//使用线程池的实现类ThreadPoolExecutor创建线程池
//new ArrayBlockingQueue<>(5)代表任务队列可以排队3个
// new ThreadPoolExecutor.AbortPolicy() 拒绝策略代表新任务超过上限直接抛异常
ExecutorService pool = new ThreadPoolExecutor(3, 5, 10,
TimeUnit.SECONDS, new ArrayBlockingQueue<>(3),
Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
MyRunnable myRunnable = new MyRunnable();
//使用线程池来处理Runnable任务,Runnable是可以公用的,不需要New多个Runnable对象
pool.execute(myRunnable);//提交第一个任务,因为第一次执行会创建线程处理任务
pool.execute(myRunnable);//提交第二个任务,第二次不一定会创建新的线程,因为可能第一次的线程已经执行完了,会自动复用线程
pool.execute(myRunnable);//提交第三个任务
pool.execute(myRunnable);//提交第四个任务 测试线程复用
pool.execute(myRunnable);//提交第五个任务 测试线程复用
//关闭线程池(一般不关闭)
pool.shutdown();//会等待所有线程执行完毕再进行关闭
pool.shutdownNow();//立即关闭,会强制关闭正在执行的任务,并且不再接受新的任务
}
}
线程池处理Callable任务代码
package com.example.demo.MyExecutor;
import java.util.concurrent.Callable;
public class MyCallable implements Callable<String> {
int n;
public MyCallable(int n){
this.n=n;
}
public String call() throws Exception {
int sum =0;
for (int i = 0; i < n; i++){
sum += i;
}
return "子线程:"+Thread.currentThread().getName()+"执行结果,i="+sum;
}
}
package com.example.demo.MyExecutor;
import java.util.concurrent.*;
public class ExecutorServiceDemo2 {
public static void main(String[] args) {
//使用线程池的实现类ThreadPoolExecutor创建线程池
//new ArrayBlockingQueue<>(5)代表任务队列可以排队3个
// new ThreadPoolExecutor.AbortPolicy() 拒绝策略代表新任务超过上限直接抛异常
ExecutorService pool = new ThreadPoolExecutor(3, 5, 10,
TimeUnit.SECONDS, new ArrayBlockingQueue<>(3),
Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
//使用线程池来处理Runnable任务,Runnable是可以公用的,不需要New多个Runnable对象
Future<String> submit1 = pool.submit(new MyCallable(100));
Future<String> submit2 = pool.submit(new MyCallable(200));
Future<String> submit3 = pool.submit(new MyCallable(300));
Future<String> submit4 = pool.submit(new MyCallable(600));
try{
System.out.println(submit1.get());
System.out.println(submit2.get());
System.out.println(submit3.get());
System.out.println(submit4.get());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
临时线程创建与拒绝新任务时机
临时线程创建:
新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程。
什么时候会拒绝新任务:
核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始拒绝任务。
任务拒绝策略:
|策略|说明|
|ThreadPoolExecutor.AbortPolicy()|丢弃任务并抛出RejectedExecutionException异常。是默认的策略|
|ThreadPoolExecutor. DiscardPolicy()|丢弃任务,但是不抛出异常,这是不推荐的做法|
|ThreadPoolExecutor. DiscardOldestPolicy()|抛弃队列中等待最久的任务 然后把当前任务加入队列中|
|ThreadPoolExecutor. CallerRunsPolicy()|由主线程负责调用任务的run()方法从而绕过线程池直接执行|
通过Executors工具类创建线程池
Executors是一个线程池的工具类,提供了很多静态方法用于返回不同特点的线程池对象。
方法名称 | 说明 |
---|---|
public static ExecutorService newFixedThreadPool(int nThreads) | 创建固定线程数量的线程池,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程替代它。 |
public static ExecutorService newSingleThreadExecutor() | 创建只有一个线程的线程池对象,如果该线程出现异常而结束,那么线程池会补充一个新线程。 |
public static ExecutorService newCachedThreadPool() | 线程数量随着任务增加而增加,如果线程任务执行完毕且空闲了60s则会被回收掉, |
public static ScheduledExecutorService newscheduledThreadPool(int corePoolsize) | 创建一个线程池,可以实现在给定的延迟后运行任务或者定期执行任务。 |
注意:这些方法的底层,都是通过线程池的实现类ThreadPoolExecutor创建的线程池对象。
使用可能存在的陷阱
大型并发系统环境中使用Executors如果不注意可能会出现系统风险。
阿里巴巴Java开发手册
【强制】线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明:Executors 返回的线程池对象的弊端如下:
1)FixedThreadPool和singleThreadPool:
允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM(内存溢出)。
2)CachedThreadPool和ScheduledThreadPool:
允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。
线程池的核心线程和最大连接的设置
配置线程池的核心线程数量(corePoolSize
)和最大线程数量(maximumPoolSize
)时,没有固定的公式,但可以根据具体的应用场景和系统资源进行合理设置。
以下是一些建议:
-
核心线程数 (
corePoolSize
):- 对于CPU密集型任务,可以将核心线程数设置为
CPU 核心数 + 1
,以充分利用 CPU 资源。 - 对于IO密集型任务,可以将核心线程数设置得更高,因为 IO 操作会阻塞线程,更多的线程可以提高并发处理能力。
- 对于CPU密集型任务,可以将核心线程数设置为
-
最大线程数 (
maximumPoolSize
):- 最大线程数通常设置为核心线程数加上一个合理的额外线程数,以应对突发的高负载情况。
- 一般情况下,最大线程数可以设置为
2 * CPU 核心数
或更高,具体取决于系统的内存和 CPU 资源。
-
队列大小 (
workQueue
):- 队列大小也会影响线程池的性能。如果队列大小设置得过大,可能会导致大量任务积压,占用大量内存。
- 如果队列大小设置得过小,可能会导致频繁创建新线程,增加系统开销。
综合考虑以上因素,可以参考以下公式进行配置:
int cpuCores = Runtime.getRuntime().availableProcessors();
int corePoolSize = cpuCores + 1; // CPU 密集型任务
// int corePoolSize = 2 * cpuCores; // IO 密集型任务
int maximumPoolSize = 2 * cpuCores;
当然,上述这些只是建议值,实际应用中需要根据具体的业务需求和系统资源进行调整。
其他常见问题
Executors工具类底层是基于什么方式实现的线程池对象?
线程池ExecutorService的实现类:ThreadPoolExecutor
Executors是否适合做大型互联网场景的线程池方案?
不合适。建议使用ThreadPoolExecutor来指定线程池参数,这样可以明确线程池的运行规则,规避资源耗尽的风险