目录
一、线程池的概念
二、Java 标准库中的线程池类
2.1 ThreadPoolExecutor 类
2.1.1 corePoolSize 和 maximumPoolSize
2.1.2 keepAliveTime 和 unit
2.1.3 workQueue
2.1.4 threadFactory
2.1.5 handler
2.1.6 创建一个参数自定义的线程池
2.2 Executors 类
2.3 实现自己的线程池
一、线程池的概念
1)什么是线程池? |
准备预期需要使用的对象,将这些对象放入一个可以随时取用的容器中,这个容器就被称为“池”。 使用过的对象也不立刻销毁,而是放回“池”中,以备下次使用。 将线程作为上述对象,放在一个容器中,这就称为“线程池”。 |
2)为什么使用线程池? |
在实际使用中,“线程池”并没有频繁的创建和销毁线程,而是从“池”中存取线程,这样可以减少每次启动和销毁线程的损耗,提高运行效率,减小系统开销。 |
3)为什么使用线程池可以提高效率? |
通常创建线程,是通过向系统申请创建,需要通过系统内核完成,是具有内核态的代码。 而从线程池中存取线程,是属于用户态的代码,相比于内核态代码更可控、稳定,操作更快。 |
二、Java 标准库中的线程池类
2.1 ThreadPoolExecutor 类
这个类的构造方法参数众多,包含以下参数: | |
int corePoolSize | 核心线程数 |
int maximumPoolSize | 最大线程数 |
long keepAliveTime | 空闲线程存活时间 |
TimeUnit unit | 时间单位 |
BlockingQueue<Runnable> workQueue | 任务队列 |
ThreadFactory threadFactory | 线程工厂 |
RejectedExecutionHandler handler | 拒绝策略 |
2.1.1 corePoolSize 和 maximumPoolSize
int corePoolSize 核心线程数 | |
线程池中保持持有的最小线程数量。 | |
int maximumPoolSize 最大线程数 | |
线程池中可以持有的最大线程数量。 |
标准库提供的线程池,持有的线程数量是可以变动的,可以根据需要执行的任务数量,自适应线程的数量。 |
2.1.2 keepAliveTime 和 unit
long keepAliveTime 空闲线程存活时间 | |
当线程池中的线程处于空闲状态时,会等待 keepAliveTime 时间后自动关闭。 如果keepAliveTime为0,线程在执行完任务后则不会关闭。 | |
TimeUnit unit 时间单位 | |
keepAliveTime 空闲线程存活时间的时间单位。 |
2.1.3 workQueue
BlockingQueue<Runnable> workQueue 任务队列 | |
用于存储等待执行的任务。当线程池中的线程数量达到最大值时,新任务将被添加到队列中等待执行。 执行的任务应该是 Runnable 接口的实现类, |
2.1.4 threadFactory
前置知识点:工厂模式
什么是工厂模式? |
工厂模式(Factory Pattern)是一种创建型设计模式,它提供了一种在不指定具体类的情况下创建对象的接口。 工厂模式的核心思想是,将对象的创建过程抽象出来,使得使用者的创建与类的实例化过程解耦。 在工厂模式中,会实现一个工厂类,它提供一个创建对象的接口,而使用者只需要调用这个接口即可,工厂类会根据客户端的请求,返回不同类型的对象。 |
threadFactory 具体指什么:
ThreadFactory threadFactory 线程工厂 | |
通过工厂类创建线程对象(简单理解就是通过这个类产出线程)。ThreadFactory 是一个接口,接口中只中有一个方法 newThread 方法,通过覆写该方法获得一个线程,同时可以给这个新线程设置一些属性。 |
2.1.5 handler
RejectedExecutionHandler handler 拒绝策略 | |
线程池异常处理器,也称拒绝策略。这里的 handler 表示一个实现了 RejectedExecutionHandler 接口的实现类,传入的 handler 参数决定了出现异常时,线程池如何处理异常。 |
有哪几种拒绝策略,分别表示什么? | ||||
ThreadPoolExecutor.AbortPolicy | 当线程池已满,且阻塞队列已满,新任务无法执行,则立即抛出 RejectedExecutionException 异常,是默认策略。 | |||
ThreadPoolExecutor.CallerRunspolicy | 当线程池已满,且阻塞队列已满,新任务无法执行,则由调用线程自己执行新任务。 | |||
ThreadPoolExecutor.DiscardOldestPolicy | 当线程池已满,且阻塞队列已满,新任务无法执行,则丢弃最早添加的任务,然后添加新任务。 | |||
ThreadPoolExecutor.DiscardPolicy | 当线程池已满,且阻塞队列已满,新任务无法执行,则丢弃新任务,不执行。 |
2.1.6 创建一个参数自定义的线程池
创建并运行以下线程池和任务:
分析上述代码运行结果:
2.2 Executors 类
1)简单介绍 Executors 类 | |
ThreadPoolExecutor 类参数较多,使用较复杂。因此,为方便使用,标准库中又提供了 Executors 工厂类。 | |
Executors 类是对 ThreadPoolExecutor 类进行了一层封装,是一个工厂类,通过这个类创建出不同属性的线程池对象。 | |
Executors 类返回的是一个 ExecutorService 类型的线程池实例。通过 ExecutorServic.submit 可以向线程池中添加任务。 |
2)简单介绍常用的 Executors 类创建线程池的方法 | |||
newFixedThreadPool | 创建固定线程数的线程池 | ||
newCachedThreadPool | 创建线程数目动态增长的线程池 | ||
newSingleThreadExecutor | 创建只包含单个线程的线程池 | ||
newScheduledThreadPool | 创建可以定时执行任务的线程池 |
3)开发过程中应该使用 ThreadPoolExecutor 类还是使用 Executors 类? | |
ThreadPoolExecutor 类参数较多、可以自定义,这意味着使用这个类创建的线程池更灵活。 而 Executors 类相比 ThreadPoolExecutor 类多了一层封装,部分参数已经设定好,这使得 Executors 类在使用上更便利。 这两种选择,前者更偏向于高度定制化的线程池,而后者偏向于通用性。两者并无高下之分,各有偏向,使用者根据实际应用场景使用即可。 |
2.3 实现自己的线程池
线程池的使用有以下关键点: | ||
<1> | 线程池,肯定要有线程。在这里实现一个线程数量固定的线程池,就需要在这个类的构造方法中,根据输入的参数 n ,新建 n 个线程。 | |
<2> | 维护一个阻塞队列,队列持有需要执行的任务。 | |
<3> | 提供一个 submit 方法,用于将任务添加到任务队列中。 | |
<4> | 线程池中的线程,也有自己的任务,就是不断地扫描任务队列,如果队列中有任务,则取出任务后执行。 | |
<5> | 注意新建的线程需要有合适的启动时机。在以下实现中,我们让线程一创建就启动。 |
代码演示线程池的实现:
class MyThreadPool{
//创建一个用于存放任务的队列;
private BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(5);
//构造方法,用于构造线程池。方法中需要将线程new出来;
public MyThreadPool(int n){
//设置线程需要执行的任务;
Runnable runnable = new Runnable() {
@Override
public void run() {
while (true){
try {
//从任务队列中取出任务,并执行;
queue.take().run();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
//循环创建线程,并启动,将线程放入队列中;
for (int i = 0; i < n; i++){
Thread t = new Thread(runnable);
t.start();
}
}
//添加任务方法。往任务队列中添加任务;
public void submit(Runnable runnable) throws InterruptedException {
queue.put(runnable);
}
}
public class ThreadPool_Demo35 {
public static void main(String[] args) throws InterruptedException {
//新建线程池;
MyThreadPool threadPool = new MyThreadPool(2);
//给线程池添加任务;
for (int i= 0; i < 10; i++){
int n = i;
threadPool.submit(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 执行任务:" + n);
}
});
}
}
}
//运行结果:
Thread-0 执行任务:0
Thread-1 执行任务:1
Thread-0 执行任务:2
Thread-1 执行任务:3
Thread-0 执行任务:4
Thread-0 执行任务:6
Thread-0 执行任务:7
Thread-0 执行任务:8
Thread-0 执行任务:9
Thread-1 执行任务:5
...
十个任务全部成功打印。
应该注意到,线程没有执行结束,还在等待新的任务加入,这是线程池的合理功能。
阅读指针 -> 《 Synchronized 锁进阶 -- 锁策略》
<JavaEE> 锁进阶 -- 锁策略(乐观锁和悲观锁、重量级锁和轻量级锁、自旋锁和挂起等待锁、可重入锁和不可重入锁、公平锁和非公平锁、读写锁)-CSDN博客介绍了以下锁策略:乐观锁和悲观锁、重量级锁和轻量级锁、自旋锁和挂起等待锁、可重入锁和不可重入锁、公平锁和非公平锁、读写锁;https://blog.csdn.net/zzy734437202/article/details/134916407