线程池
顾名思义就是事先创建若干个可执行的线程放入一个池(容器)中,需要的时候从池中获取线程不用自行创建,使用完毕不需要销毁线程而是放回池中,从而减少创建和销毁线程对象的开销。
例如:为线程池提供一个Runnable, 就会有一个线程调用run方法。当run方法退出时,这个线程不会死亡,而是留着池中准备为下一个请求提供服务。
线程是一种昂贵的资源,需要非常大的开销,主要包括:
因此从整个系统的角度来看,我们需要一种有效使用线程的方式,从而减少开销。
线程池就是有效使用线程的一种常见方式
创建线程池的方式有多种,这里你只需要答 ThreadPoolExecutor
即可。
ThreadPoolExecutor() 是最原始的线程池创建,也是阿里巴巴 Java 开发手册中明确规范的创建线程池的方式。
降低资源消耗
:重用存在的线程,减少对象创建销毁的开销。提高响应速度
。可有效的控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。当任务到达时,任务可以不需要的等到线程创建就能立即执行。提高线程的可管理性
。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。定时执行、定期执行、单线程、并发数控制
等功能。RUNNING
:这是最正常的状态,接受新的任务,处理等待队列中的任务。SHUTDOWN
:不接受新的任务提交,但是会继续处理等待队列中的任务。STOP
:不接受新的任务提交,不再处理等待队列中的任务,中断正在执行任务的线程。TIDYING
:所有的任务都销毁了,workCount 为 0,线程池的状态在转换为 TIDYING 状态时,会执行钩子方法 terminated()。TERMINATED
:terminated()方法结束后,线程池的状态就会变成这个。简单来说就是要根据CPU密集和IO密集
来分配
CPU密集:
IO密集:
分配CPU和IO密集:
2*CPU核数
结论:
提交一个任务到线程池中,线程池的处理流程如下:
【实例】
为了让大家更清楚上面的面试题中的一些概念,我写了一个简单的线程池 Demo。
首先创建一个 Runnable 接口的实现类(当然也可以是 Callable 接口,我们上面也说了两者的区别。)
/**
* @Author: LiangYiFeng
* @Description 这是一个简单的Runnable类,需要大约5秒钟来执行其任务。
* @Date: Create in 2022/8/15 11:54
* @Modified By:
*/
public class MyRunnable implements Runnable{
private String command;
public MyRunnable(String command) {
this.command = command;
}
public void run() {
System.out.println(Thread.currentThread().getName() + "Start.Time = " + new Date());
processCommand();
System.out.println(Thread.currentThread().getName() + "End.Time = " + new Date());
}
private void processCommand() {
try{
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public String toString() {
return "MyRunnable{" +
"command='" + command + '\'' +
'}';
}
}
编写测试程序,我们这里以阿里巴巴推荐的使用 ThreadPoolExecutor 构造函数自定义参数的方式来创建线程池。
public class ThreadPoolExecutorDemo {
private static final int CORE_POOL_SIZE = 5;
private static final int MAX_POOL_SIZE = 10;
private static final int QUEUE_CAPACITY = 100;
private static final Long KEEP_ALIVE_TIME = 1L;
public static void main(String[] args) {
// 使用阿里巴巴 推荐的创建线程池的方式
// 通过ThreadPoolExecutor 构造函数自定义参数创建
ThreadPoolExecutor executor = new ThreadPoolExecutor(
CORE_POOL_SIZE,
MAX_POOL_SIZE,
KEEP_ALIVE_TIME,
TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(QUEUE_CAPACITY),
new ThreadPoolExecutor.CallerRunsPolicy());
for (int i = 0; i < 10; i++) {
Runnable worker = new MyRunnable("" + i);
executor.execute(worker);
}
// 终止线程池
executor.shutdown();
while (!executor.isTerminated()){
}
System.out.println("Finish all Threads");
}
}
可以看到我们上面的代码指定了:
corePoolSize
: 核心线程数为 5。maximumPoolSize
:最大线程数 10keepAliveTime
: 等待时间为 1L。unit
: 等待时间的单位为 TimeUnit.SECONDS。workQueue
:任务队列为 ArrayBlockingQueue,并且容量为 100;handler
:饱和策略为 CallerRunsPolicy。输出结果:
pool-1-thread-4Start.Time = Mon Aug 15 12:04:25 CST 2022
pool-1-thread-5Start.Time = Mon Aug 15 12:04:25 CST 2022
pool-1-thread-1Start.Time = Mon Aug 15 12:04:25 CST 2022
pool-1-thread-3Start.Time = Mon Aug 15 12:04:25 CST 2022
pool-1-thread-2Start.Time = Mon Aug 15 12:04:25 CST 2022
pool-1-thread-5End.Time = Mon Aug 15 12:04:30 CST 2022
pool-1-thread-4End.Time = Mon Aug 15 12:04:30 CST 2022
pool-1-thread-2End.Time = Mon Aug 15 12:04:30 CST 2022
pool-1-thread-3End.Time = Mon Aug 15 12:04:30 CST 2022
pool-1-thread-4Start.Time = Mon Aug 15 12:04:30 CST 2022
pool-1-thread-3Start.Time = Mon Aug 15 12:04:30 CST 2022
pool-1-thread-2Start.Time = Mon Aug 15 12:04:30 CST 2022
pool-1-thread-5Start.Time = Mon Aug 15 12:04:30 CST 2022
pool-1-thread-1End.Time = Mon Aug 15 12:04:30 CST 2022
pool-1-thread-1Start.Time = Mon Aug 15 12:04:30 CST 2022
pool-1-thread-3End.Time = Mon Aug 15 12:04:35 CST 2022
pool-1-thread-4End.Time = Mon Aug 15 12:04:35 CST 2022
pool-1-thread-5End.Time = Mon Aug 15 12:04:35 CST 2022
pool-1-thread-1End.Time = Mon Aug 15 12:04:35 CST 2022
pool-1-thread-2End.Time = Mon Aug 15 12:04:35 CST 2022
Finish all Threads
:Runnable+ThreadPoolExecutor:
Executor 管理多个异步任务的执行,而无需程序员显式地管理线程的生命周期。这里的异步是指多个任务的执行互不干扰,不需要进行同步操作。
每次执行任务创建线程 new Thread()比较消耗性能,创建一个线程是比较耗时、耗资源的,而且无限制的创建线程会引起应用程序内存溢出。
所以创建一个线程池是个更好的的解决方案,因为可以限制线程的数量并且可以回收再利用这些线程。利用Executors 框架可以非常方便的创建一个线程池。
接口
,只有一个execute
方法,用于执行能执行我们的线程任务
。普通工具类
,里面实现了各种新建线程池方法
能按照我们的需求创建了不同的线程池,来满足业务的需求。自定义线程池
。相同点:都可以开启线程执行线程池中的任务
不同点:
分类 | submit | execute |
---|---|---|
接收参数 |
submit()可以执行 Runnable 和 Callable 类型的任务。 | execute()只能执行 Runnable 类型的任务; |
返回值 |
submit()方法可以返回持有计算结果的 Future 对象,同时还可以抛出异常 | execute()不可以 |
即submit()方法用于需要提交返回值
的任务,execute()方法用于提交不需要返回值
的任务。
执行器(Executors)类有许多静态工厂方法,用来构造线程池,并提供了一些静态工厂方法,生成一些常用的线程池。
提供常用四种线程池:
方法 | 描述 |
---|---|
newCachedThreadPool | 创建一个可缓存线程池,会立即执行各个任务,如果线程池长度超过需要,可灵活回收空闲线程,若无可回收,则新建线程。空闲线程可保留60秒 |
newFixedThreadPool | 创建一个定长线程池,可控制线程最大并发数。空闲线程会一直保留 |
newScheduledThreadPool | 创建一个定长线程池,支持定时及周期性任务执行。用于调度执行的固定线程池 |
newSingleThreadExecutor | 创建一个单线程化的线程池,它只用唯一的工程线程来执行任务,保证所有任务按照指定顺序(FIFO,LIFO,优先级)执行 |
【实例】
newCachedThreadPool
:
public static void main(String[] args) {
// 创建无限大小的线程池,由jvm自动回收
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
final int temp = i;
executorService.execute(new Runnable() {
public void run() {
try{
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+",i=="+temp);
}
});
}
}
newFixedThreadPool
:
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(3);
for (int i = 0; i < 10; i++) {
final int temp = i;
executorService.execute(new Runnable() {
public void run() {
try{
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+",i=="+temp);
}
});
}
}
newScheduledThreadPool
:
public static void main(String[] args) {
ExecutorService executorService = Executors.newScheduledThreadPool(3);
for (int i = 0; i < 10; i++) {
final int temp = i;
executorService.execute(new Runnable() {
public void run() {
try{
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+",i=="+temp);
}
} );
}
}
newSingleThreadExecutor
:
public static void main(String[] args) {
ExecutorService executorService = Executors.newSingleThreadExecutor();
for (int i = 0; i < 10; i++) {
final int temp = i;
executorService.execute(new Runnable() {
public void run() {
System.out.println(Thread.currentThread().getName()+",i=="+temp);
try{
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
}
java.util.concurrent.ThreadPoolExecutor 类 就是一个线程池。
【实例】
使用Runnable+ThreadPoolExecutor :自定义线程池
首先创建一个 Runnable 接口的实现类(当然也可以是 Callable 接口,我们上面也说了两者的区别。)
/**
* @Author: LiangYiFeng
* @Description 这是一个简单的Runnable类,需要大约5秒钟来执行其任务。
* @Date: Create in 2022/8/15 11:54
* @Modified By:
*/
public class MyRunnable implements Runnable{
private String command;
public MyRunnable(String command) {
this.command = command;
}
public void run() {
System.out.println(Thread.currentThread().getName() + "Start.Time = " + new Date());
processCommand();
System.out.println(Thread.currentThread().getName() + "End.Time = " + new Date());
}
private void processCommand() {
try{
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public String toString() {
return "MyRunnable{" +
"command='" + command + '\'' +
'}';
}
}
编写测试程序,我们这里以阿里巴巴推荐的使用 ThreadPoolExecutor 构造函数自定义参数的方式来创建线程池。
public class ThreadPoolExecutorDemo {
private static final int CORE_POOL_SIZE = 5;
private static final int MAX_POOL_SIZE = 10;
private static final int QUEUE_CAPACITY = 100;
private static final Long KEEP_ALIVE_TIME = 1L;
public static void main(String[] args) {
// 使用阿里巴巴 推荐的创建线程池的方式
// 通过ThreadPoolExecutor 构造函数自定义参数创建
ThreadPoolExecutor executor = new ThreadPoolExecutor(
CORE_POOL_SIZE,
MAX_POOL_SIZE,
KEEP_ALIVE_TIME,
TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(QUEUE_CAPACITY),
new ThreadPoolExecutor.CallerRunsPolicy());
for (int i = 0; i < 10; i++) {
Runnable worker = new MyRunnable("" + i);
executor.execute(worker);
}
// 终止线程池
executor.shutdown();
while (!executor.isTerminated()){
}
System.out.println("Finish all Threads");
}
}
输出结果:
E:\JDK1.8\bin\java.exe "-javaagent:D:\IDEA\IntelliJ IDEA 2019.3.5\lib\idea_rt.jar=57388:D:\IDEA\IntelliJ IDEA 2019.3.5\bin" -Dfile.encoding=UTF-8 -classpath E:\JDK1.8\jre\lib\charsets.jar;E:\JDK1.8\jre\lib\deploy.jar;E:\JDK1.8\jre\lib\ext\access-bridge-64.jar;E:\JDK1.8\jre\lib\ext\cldrdata.jar;E:\JDK1.8\jre\lib\ext\dnsns.jar;E:\JDK1.8\jre\lib\ext\jaccess.jar;E:\JDK1.8\jre\lib\ext\jfxrt.jar;E:\JDK1.8\jre\lib\ext\localedata.jar;E:\JDK1.8\jre\lib\ext\nashorn.jar;E:\JDK1.8\jre\lib\ext\sunec.jar;E:\JDK1.8\jre\lib\ext\sunjce_provider.jar;E:\JDK1.8\jre\lib\ext\sunmscapi.jar;E:\JDK1.8\jre\lib\ext\sunpkcs11.jar;E:\JDK1.8\jre\lib\ext\zipfs.jar;E:\JDK1.8\jre\lib\javaws.jar;E:\JDK1.8\jre\lib\jce.jar;E:\JDK1.8\jre\lib\jfr.jar;E:\JDK1.8\jre\lib\jfxswt.jar;E:\JDK1.8\jre\lib\jsse.jar;E:\JDK1.8\jre\lib\management-agent.jar;E:\JDK1.8\jre\lib\plugin.jar;E:\JDK1.8\jre\lib\resources.jar;E:\JDK1.8\jre\lib\rt.jar;G:\项目文件\java_se\java_thread\target\classes com.lyf.thread.threadpool.threadpooldemo.ThreadPoolExecutorDemo
pool-1-thread-4Start.Time = Mon Aug 15 12:04:25 CST 2022
pool-1-thread-5Start.Time = Mon Aug 15 12:04:25 CST 2022
pool-1-thread-1Start.Time = Mon Aug 15 12:04:25 CST 2022
pool-1-thread-3Start.Time = Mon Aug 15 12:04:25 CST 2022
pool-1-thread-2Start.Time = Mon Aug 15 12:04:25 CST 2022
pool-1-thread-5End.Time = Mon Aug 15 12:04:30 CST 2022
pool-1-thread-4End.Time = Mon Aug 15 12:04:30 CST 2022
pool-1-thread-2End.Time = Mon Aug 15 12:04:30 CST 2022
pool-1-thread-3End.Time = Mon Aug 15 12:04:30 CST 2022
pool-1-thread-4Start.Time = Mon Aug 15 12:04:30 CST 2022
pool-1-thread-3Start.Time = Mon Aug 15 12:04:30 CST 2022
pool-1-thread-2Start.Time = Mon Aug 15 12:04:30 CST 2022
pool-1-thread-5Start.Time = Mon Aug 15 12:04:30 CST 2022
pool-1-thread-1End.Time = Mon Aug 15 12:04:30 CST 2022
pool-1-thread-1Start.Time = Mon Aug 15 12:04:30 CST 2022
pool-1-thread-3End.Time = Mon Aug 15 12:04:35 CST 2022
pool-1-thread-4End.Time = Mon Aug 15 12:04:35 CST 2022
pool-1-thread-5End.Time = Mon Aug 15 12:04:35 CST 2022
pool-1-thread-1End.Time = Mon Aug 15 12:04:35 CST 2022
pool-1-thread-2End.Time = Mon Aug 15 12:04:35 CST 2022
Finish all Threads
ThreadPoolExecutor
3 个最重要的参数:
corePoolSize
:用于执行核心线程大小,线程数定义了最小可以同时运行的线程数量。
maximumPoolSize
:线程池中允许存在的工作线程的最大数量
workQueue
:工作队列的阻塞队列,它相当于生产者-消费者模式中的传输通道。当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,任务就会被存放在队列中。
ThreadPoolExecutor其他常见参数:
keepAliveTime
:线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁;
unit
:keepAliveTime 参数的时间单位。
keepAliveTime 和 unit 合在一起用于指定线程池中空间线程的最大存活时间。
threadFactory
:为线程池提供创建新线程的线程工厂
handler
:线程池任务队列超过 maxinumPoolSize 之后的拒绝策略
ThreadPoolExecutor
自身提供了几个线程的RejectedExecutionException(饱和策略
) 接口实现类:
如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满时,
ThreadPoolTaskExecutor
定义一些策略:
实现类 | 所实现的处理策略 |
---|---|
ThreadPoolExecutor.AbortPolicy |
直接抛出RejectedExecutionException来拒绝新任务的处理。 |
ThreadPoolExecutor.DiscardPolicy |
丢弃当前被拒绝任务(而不抛出任何异常) |
ThreadPoolExecutor.DiscardOldestPolicy |
将工作队列中的最老的任务丢弃,然后重新尝试接纳被拒绝的任务 |
ThreadPoolExecutor.CallerRunsPolicy |
在客户端线程执行被拒绝的任务 |
举个例子: Spring 通过 ThreadPoolTaskExecutor 或者我们直接通过 ThreadPoolExecutor 的构造函数创建线程池的时候,当我们不指定 RejectedExecutionHandler 饱和策略的话来配置线程池的时候默认使用的是 ThreadPoolExecutor.AbortPolicy。在默认情况下,ThreadPoolExecutor 将抛出 RejectedExecutionException 来拒绝新来的任务 ,这代表你将丢失对这个任务的处理。 对于可伸缩的应用程序,建议使用 ThreadPoolExecutor.CallerRunsPolicy。当最大池被填满时,此策略为我们提供可伸缩队列。(这个直接查看 ThreadPoolExecutor 的构造函数源码就可以看出,比较简单的原因,这里就不贴代码了)
《阿里巴巴Java开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险
Executors 各个方法的弊端:
ThreaPoolExecutor创建线程池方式只有一种,就是走它的构造函数,参数自己指定