Java进阶|Java多线程详解(线程池)
嗨喽~小伙伴们我来了,
上一章我们介绍了Java中的Thread类里一些常用的方法。本节我们就来聊一聊线程池。
说到“池”,大家或许都不陌生,在java中,我们有见过数据库连接池,Java常量池,对象池等等,将实体进行“池化”,这种“池化”思想,有助于我们对实体进行统一的管理,监控和调用。
本章的主要内容有:
- 创建线程池
- 构造方法的参数解读
- 四种功能性线程池
- 关闭线程池
首先,我们来对比下面两个程序运行的效率。
程序一,使用前面我们学过的一般线程:
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.*;
/**
* @author sixibiheye
* @date 2021/9/2
* @apiNote 线程池初识
*/
public class ThreadPoolDemo1 {
public static void main(String[] args) throws InterruptedException {
Long start = System.currentTimeMillis();
Random random = new Random();
List list = new ArrayList();
for (int i = 0;
i < 100000;
i++) {
Thread thread = new Thread( () -> {
list.add(random.nextInt());
});
thread.start();
thread.join();
}
Long end = System.currentTimeMillis();
System.out.println("耗时:" + (end - start) + "ms");
System.out.println("大小:" + list.size());
}
}
简单理解就是创建100000个线程来对list添加数据,最后输出添加所需的总时间和 list 的大小。我们来看运行结果:
文章图片
程序二,使用线程池(不理解的小伙伴们可以先跳过往下看):
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
* @author sixibiheye
* @date 2021/9/2
* @apiNote 线程池初识
*/
public class ThreadPoolDemo2 {
public static void main(String[] args) throws InterruptedException {
Long start = System.currentTimeMillis();
Random random = new Random();
List list = new ArrayList<>();
ExecutorService executorService = Executors.newSingleThreadExecutor();
for (int i = 0;
i < 100000;
i++) {
executorService.execute( () -> {
list.add(random.nextInt());
});
}
executorService.shutdown();
executorService.awaitTermination(1, TimeUnit.DAYS);
Long end = System.currentTimeMillis();
System.out.println("耗时:" + (end - start) + "ms");
System.out.println("大小:" + list.size());
}
}
运行结果:
文章图片
从耗时来看,使用线程池所需的时间比使用一般线程所需的时间足足减少了100多倍!由此看来,线程池对于程序的优化有着重大的意义。
实际上,从原理上理解,使用一般的线程,都需要经历创建,使用,销毁三个步骤,当创建的线程数非常大的时候,这种操作对内存的消耗比较大,导致效率低下。因此我们希望创建好的线程能够在指定时间内继续执行其他的任务,通过减少创建和销毁线程的消耗,以此来提高效率。
其实,线程池概念的提出与我们的生活有密切关系,许多算法,概念的提出都能够在生活中找到对应的例子。
如果要理解线程池,咱就必须提到“银行办理业务排队???????”的场景逻辑,这是一个非常非常非常(重要的事情说三遍!!!)典型的例子,请小伙伴们务必看懂,对线程池的理解非常有帮助:
去过银行办理业务的朋友们都知道,银行里有多个窗口来办理业务,此外还有等候区供人们休息。我们现在假设有这样一个场景 :
文章图片
如上图,假设现在某银行有3个窗口(1,2,3号),2个备用窗口(4,5号),和可供3人休息的等候区。
现在有1人来办理业务, 这1个人带着“任务1”去了1号窗口办理业务:
文章图片
接着,第2,3个人也来办理业务,他们带着任务2,3分别去了2,3号窗口:
文章图片
这时,如果银行来了第4个人,他只能去等候区等候,第5,6个人亦是如此:
文章图片
此时,等候区人数已满,如果再来第7个人,银行行长只能开启备用窗口-----4号窗口,并让还在等候区等候的第4个人到4号窗口办理业务,等候区(队列)往前“挪一个”使得第7个人能够进入等候区:
文章图片
同理,如果再来第8个人,那就只能开启第二个备用窗口-----5号窗口,并让第5个人到5号窗口办理业务,等候区(队列)往前“挪一个”使得第8个人能够进入等候区:
文章图片
这时,不论是窗口数,还是等待区容量,都已经满了,如果来了第9个人,怎么办呢?
文章图片
对于第9个人,银行只能采取拒绝的方式,因为就当前情况,不管是窗口,还是等候区,都容不下第9个人了。
这个便是一个简单的银行排队流程。借鉴于这种思想,我们把它搬到线程里,描述如下:
线程池(银行)里有最多5个线程数,有3个是核心线程(1,2,3号窗口),另外2个是备用线程(4,5号窗口,或者叫非核心线程),当核心线程全被使用后,就将多余的任务以队列的形式放入“任务队列”(等候区)中,如果任务队列也满了,就开启备用线程(非核心线程),如果备用线程也全部被使用了,那么剩下多余的任务,就只能拒绝执行了。
理解完上述过程,学习线程池,就轻松多了。在Java中,线程池的真正实现类是ThreadPoolExecutor,翻阅源码,它有如下几种构造方法:
文章图片
文章图片
文章图片
文章图片
文章图片
上面四种构造方法中,最多的构造方法有七个参数,我们来看看这七个参数的具体含义:
1. corePoolSize (必需) : 核心线程数(类比银行的1,2,3号窗口)。默认情况下,核心线程会一直存活,除非将allowCoreThreadTimeout设置为true,这样超时后,核心线程也会被回收。
2. maxmumPoolSize (必需) : 最大线程数(类比银行的1,2,3,4,5号窗口)。对于非核心线程(4,5号窗口),在下面的keepAliveTime设定的时间超过之后,会被回收。同样的,将allowCoreThreadTimeout设置为true的话,这样超时后,核心线程也会被回收。
3. keepAliveTime (必需) : 如上,设定非核心线程的闲置时间,超时后,非核心线程会被回收。
4. unit (必需) : 指定上面keepAliveTime参数的单位,常用的有:
- TimeUnit.MILLISECONDS(毫秒)
- TimeUnit.SECONDS(秒)
- TimeUnit.MINUTES(分)
6. threadFactory (可选) : 线程工厂-----指定新线程创建的方式,自定义ThreadFactory的话可以修改线程名,线程组,优先级,是否为守护线程等等,如果不想自定义,使用默认的Executors.defaultThreadFactory()即可。
7. handler (可选) : 当线程池创建的线程数达到最大值时,需要执行的拒绝策略。
需要实现RejectedExecutionHandler接口,并重写
rejectedExecution(Runnable r , ThreadPoolExecutor executor) 方法。
Executors框架为我们提供了四种常见的拒绝策略:
- 1.AbortPolicy (默认) :丢弃任务并抛出 RejectedExecutionException 异常
- 2.CallerRunsPolicy :丢给调度线程处理该任务
- 3.DiscardPolicy :丢弃任务但不抛出异常。一般用于自定义处理模式。
- 4.DiscardOldestPolicy :丢弃队列最早的未处理完的任务,然后尝试执行新任务。???????
下面我们来认识一下 ThreadPoolExecutor 这个类,来看源码:
首先,最底层是一个函数式接口(只有一个抽象方法) Executor :
文章图片
【Java进阶|Java多线程详解(线程池)】接着有一个叫 ExecutorService 的接口继承了 Executor ,其扩展了一些方法,如 isShutdown() , shutdown() , awaitTermination()等等:
文章图片
然后,有一个叫 AbstractExecutorService 的类实现了 ExecutorService ,提供了一些方法的实现:
文章图片
最后, 咱 ThreadPoolExecutor 类继承了 AbstractExecutorService ,并实现了 execute() 等重要的方法:
文章图片
文章图片
现在,我们来看一个简单的程序:
import java.util.concurrent.*;
/**
* @author sixibiheye
* @date 2021/9/2
* @apiNote 线程池
*/
public class CustomThreadPool {
public static void main(String[] args) {ExecutorService executorService = new ThreadPoolExecutor(25,50,1L, TimeUnit.SECONDS,new ArrayBlockingQueue<>(50),Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
for (int i = 1;
i <= 100;
i++) {
executorService.execute(new TaskDemo4(i));
}
//当所有任务执行完之后,结束线程池服务
executorService.shutdown();
}
}
class TaskDemo4 implements Runnable{
private int i = 0;
public TaskDemo4(int i){
this.i = i;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "线程做了第" + i + "个任务");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
上述程序中,通过for循环产生了100个任务,请大家细细体会 ThreadPoolExecutor() 构造方法中的7个参数如何取定。
如果已有任务数超过了线程池的最大线程数与任务队列容量之和,线程池就会执行拒绝策略,默认为上述第一种拒绝策略。比如将上述代码中的线程池创建参数---任务队列修改如下:
new ArrayBlockingQueue<>(49)
则会抛出 RejectedExecutionException 异常:
文章图片
这七个参数中,大家比较模糊的是 workQueue ---- 任务队列。
下面我们来看看 workQueue 如何取定。任务队列是基于阻塞队列实现的,采用的是生产者-消费者模式,在Java中需要实现 BlockingQueue 接口,当然我们可以自定义实现类,但JDK已经为我们提供了7种阻塞队列的实现类,我们简单的介绍其中最常用的三种:
- 1. ArrayBlockingQueue :一个由顺序表结构组成的有界(需指明容量)阻塞队列,
- 2. LinkedBlockingQueue :一个由链表结构组成的阻塞队列。可以指明容量,未指明容量时,默认为无界(Integer.MAX_VALUE).
- 3. SynchronousQueue :一个不存储任何元素的同步阻塞队列。
基于 ThreadPoolExecutor 的七个参数值的不同设定,Executors类 (Executor接口的工具类)给我们封装了几个常用的创建线程池的方法:
1. 可缓存线程池(CachedThreadPool)方法源码:
文章图片
文章图片
- 特点:无核心线程,非核心线程无限大,线程闲置60s后被回收,任务队列为不存储任何元素的同步阻塞队列
- 适用场景:执行大量且耗时的操作
文章图片
文章图片
- 特点:只有核心线程,线程一旦闲置立即被回收,任务队列为链表结构的无界阻塞队列
- 适用场景:需要控制线程最大并发数的地方
3. 定时线程池(ScheduledThreadPool)方法源码:
文章图片
文章图片
文章图片
文章图片
文章图片
- 特点:核心线程固定,线程闲置10ms后被回收,任务队列为延时阻塞队列
- 适用场景:执行定时或周期性的任务???????
4. 单线程化线程池(SingleThreadExecutor)方法源码:
文章图片
文章图片
- 特点:核心线程固定为1个,没有非核心线程,线程一旦闲置立即被回收,任务队列为链表结构的无界阻塞队列
- 适用场景:串行执行所有任务
原因有两点:
1. 使用原始的 ThreadPoolExecutor 可以使我们更加明确线程池的运行机制,减少对资源的浪费。
2. 使用上述四种线程池还有自己的弊端:
- FixedThreadPool& SingleThreadExecutor :由于任务队列可以为无界队列,当任务过多时,可能会导致OOM(内存溢出)。
- CachedThreadPool & ScheduledThreadPool :由于最大线程数为无限大,当线程创建过多时,可能会导致CPU利用率接近100%。
在介绍如何关闭线程池之前,我们来看看线程池的五个状态:
文章图片
我们来简单认识一下这五种状态:
1.RUNNING
特点:线程池处在 RUNNING 状态时,能够接收新任务,能够执行已添加的任务。
- 线程池一旦被创建,就处于 RUNNING 状态,并且线程池中的任务数为0。
2.SHUTDOWN
特点:线程池处在 SHUTDOWN 状态时,不接收新任务,但能处理已添加的任务。
- 调用线程池的shutdown()方法时,线程池状态转变:RUNNING --> SHUTDOWN。
3.STOP
特点:线程池处在 STOP 状态时,不接收新任务,不处理已添加的任务,并且会中断正在处理的任务。
- 调用线程池的 shutdownNow()方法 时,线程池状态转变:( RUNNING or SHUTDOWN ) --> STOP。
4.TIDYING
特点:当所有的任务都已中止或结束后,ctl记录的“任务数量”为0,线程池会变为 TIDYING 状态。
- 当线程池在 SHUTDOWN 状态下,任务阻塞队列为空并且线程池中执行的任务也为空时,就会由 SHUTDOWN --> TIDYING。
- 当线程池在 STOP 状态下,线程池中执行的任务为空时,就会由 STOP --> TIDYING。
5.TERMINATED
特点:线程池彻底终止,就变成TERMINATED状态。
- 线程池处在 TIDYING 状态时,执行完terminated()方法后,就会由 TIDYING --> TERMINATED。
文章图片
文章图片
从上述源码结合前面学的知识可以发现:
- 当线程池创建以后,初始时,线程池处于 RUNNING 状态,此时线程池中的任务为0;
- 如果调用 shutdown() 方法,则线程池变为 SHUTDOWN 状态,此时线程池不能够接受新的任务,它会等待所有任务执行完毕;
- 如果调用 shutdownNow() 方法,则线程池处于 STOP 状态,此时线程池不能接受新的任务,并且会去尝试终止正在执行的任务;
- 当所有的任务已中止或结束后,“任务数量”为0,线程池会变为 TIDYING 状态。接着会执行 terminated() 函数。
- 线程池处在 TIDYING 状态时,执行完 terminated() 之后,线程池就被设置为TERMINATED状态。
推荐阅读
- JAVA(抽象类与接口的区别&重载与重写&内存泄漏)
- 放屁有这三个特征的,请注意啦!这说明你的身体毒素太多
- 爱就是希望你好好活着
- 昨夜小楼听风
- 知识
- 死结。
- 我从来不做坏事
- 烦恼和幸福
- 关于QueryWrapper|关于QueryWrapper,实现MybatisPlus多表关联查询方式
- 事件代理