目录
五. ThreadPoolExecutor的封装类——Executors
一. 线程池的概念
最初引入线程,就是因为进程太重了,频繁创建销毁进程的开销比较大(大小是相对的);随着业务上对性能要求越来越高,对应的线程创建销毁的频次越来越多。此时,创建销毁线程的开销变得比较明显,无法忽略不计了~~
线程池就是解决上述问题的常见方案——把线程提前从系统中申请好,放到线程池中。后面需要使用线程的时候,直接从这个地方来取,而不是从系统重新申请。
线程池是一种用于管理和复用线程的机制,它可以提高程序的性能和响应速度。线程池的主要目的是减少创建和销毁线程的开销,从而降低资源消耗,提高响应速度,提高线程的使用率。
二. 内核态 vs 用户态
操作系统 = 操作系统内核 + 操作系统配套的应用程序
操作系统内核负责完成一个操作系统的核心工作(资源管理、进程管理、内存管理、文件系统管理、网络管理......管理工作)对应的,执行的很多代码逻辑都是要用户态的代码和内核态的代码配合完成的~~
在操作系统中,用户态和内核态是处理器运行的两种不同的模式,它们定义了代码执行时的权限级别和可访问的资源。内核态和用户态的划分是为了保护操作系统内核不受用户程序的影响。
- 内核态是内核工作的环境(比如进行IO操作、创建进程或线程、分配内存等):内核代码在内核态下运行,执行系统级别的操作。
- 用户态是用户程序工作的环境:用户程序在用户态下运行,通过系统调用与内核交互。
- 状态切换:当用户程序需要执行系统级别的操作时(如文件读写、网络通信等)它会通过系统调用从用户态切换到内核态,完成操作后再返回用户态。
线程池主要是在用户态下工作的,但是它的工作过程中会涉及到与内核态的交互。
- 从系统创建线程,就是调用系统 API,由系统内核执行一系列逻辑来完成这个过程。但应用程序有很多,这些应用程序,都是由内核统一负责管理和服务,因此内核里的工作就可能非常繁忙,导致提交给内核要做的任务,具体什么时间完成,可能是不可控的~~
- 直接从线程池里取,整个过程都是纯用户态代码,整个过程更可控,效率更高。
因此,通常认为,纯用户态操作就比经过内核的操作效率更高。
三. 用户态下的工作
3.1 线程管理
- 线程池在用户态下管理着一组线程,这些线程通常是由线程池在程序启动时预先创建的,或者是根据需要动态创建的。
- 线程池中的线程执行任务时,通常处于用户态,它们执行的是应用程序的代码。
3.2 任务调度
- 当一个新的任务到达线程池时,线程池的调度逻辑(通常在用户态下运行)会决定将任务分配给哪个线程执行。
- 如果有线程空闲,任务会直接分配给该线程执行。如果没有空闲线程且线程池未达到最大线程数,线程池可能会创建新的线程来处理任务。
3.3 任务队列/工作队列
- 线程池通常包含一个任务队列,用于存储和管理待执行的任务。这个队列的操作也是在用户态下进行的。
四. 标准库的线程池ThreadPoolExecutor
标准库提供了 ThreadPoolExecutor (java.util.concurrent并行并发)
下面是四个构造方法:
这个类的构造方法的参数的含义,也是 “经典的面试题”。
(1)核心线程数/最大线程数
int corePoolSize: 核心线程数 int maximunPoolSize: 最大线程数
(即使核心线程处于空闲状态,也不会被销毁,除非设置了allowCoreThreadTimeOut为true)
(非核心线程,在繁忙的时候被创建出来;空闲时,就会把这些线程真正释放掉)
此线程池可以支持“线程扩容”。某个线程池, 初始情况下,可能有 M 个线程,实际使用中发现 M 个不够用,就会自动扩容。
在 Java 标准库的线程池中,就把里面的线程分成两类:
- 核心线程(可以理解成最少有多少个线程)
- 非核心线程(线程扩容的过程中,新增的部分)
核心线程数 + 非核心线程数的最大值 = 最大线程数
(2)允许空闲的最大时间/时间单位
long keepAliveTime:非核心线程空闲时的存活时间 unit:存活时间的单位
非核心线程会在线程空闲的时候被销毁,keepAliveTime就是允许空闲的最大时间。
(3)任务队列
BlockingQueue<Runnable> workQueue:任务队列,用于存放待执行的任务
(Runnable接口本身含义就是一段可执行的任务)
线程池的工作过程是典型的“生产者消费者模型”。程序员使用的时候,通过“submit”这样的方法,把要执行的任务设定到线程池里。线程池内部的工作线程,负责执行这些任务。
此处的任务队列,是我们自行指定:a. 队列容量 b. 队列类型
(4)线程工厂--> 工厂设计模式
ThreadFactory threadFactory:线程工厂,用于创建线程
“工厂” 指的是“工厂设计模式”,也是一种常见的设计模式。
工厂设计模式(Factory Design Pattern)是软件开发中常用的一种创建型设计模式,它的主要目的是用于创建对象,同时隐藏创建逻辑,而不是通过直接使用 new 实例化对象,这使得程序更加模块化,提高了代码的可维护性和可扩展性。
构造方法是一种特殊方法,要求方法名和类名必须保持一致,不同版本的构造方法必须通过“重载”(overload)实现。但是,某些时候想要初始化的属性名不同,但是类型相同(比如构造坐标系,直角坐标系参数是int x, int y;极坐标系参数是int r, int θ),这就导致构造不可再使用一套构造方法。
为了解决上述问题,引入了"工厂设计模式”——通过"普通方法"(通常是静态方法,也是“简单工厂模式”) 完成对象构造和初始化的操作。
上面是最简单的工厂设计模式的写法,此处用来创建对象的static方法,就称为“工厂方法”。
有的时候,工厂方法也会放到单独的类里实现——用来放工厂方法的类 称为"工厂类”。
线程工厂,就是 Thread 类的工厂类,通过这个类,完成 Thread 的实例创建和初始化的操作。 此处的 ThreadFactory 就可以针对线程池里的线程,进行批量的设置属性。
此处一般不会进行调整,就使用标准库提供的ThreadFactory的默认值即可。
(5)拒绝策略
RejectedExecutionHandler handler:拒绝策略
(上述所有参数中,最重要,也最复杂的)
* 如果线程池的任务队列满了,还是要继续给这个队列添加任务,咋办呢??
——当队列满了不要阻塞,而是要明确的拒绝~~
Interface RejectedExecutionHandler 的实现类:
- AbortPolicy:直接抛出异常,阻止系统正常运行(abort:终止或中断程序的执行)
- CallerRunsPolicy:线程池拒绝执行,在调用者线程中运行任务,不会抛出异常
- DiscardOldestPolicy:抛弃队列中等待时间最长的任务,然后尝试提交当前任务
- DiscardPolicy:丢弃请求新添加的任务,不抛出异常
五. ThreadPoolExecutor的封装类——Executors
ThreadPoolExecutor 功能很强大,使用很麻烦。标准库对这个类进一步封装了一下,Executors提供了一些工厂方法,可以更方便的构造出线程池。
常见的线程池类型(都是对 ThreadPoolExecutor 进行了封装):
- FixedThreadPool:固定大小的线程池核心线程数和最大线程数相同,不会扩容
- SingleThreadExecutor:只有一个线程的线程池,确保所有任务按顺序
- CachedThreadPool:可缓存线程的线程池,线程数量不固定,适用于执行大量短期异步任务的程序
- ScheduledThreadPool:支持定时及周期性任务执行的线程池
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Executors1 {
public static void main(String[] args) throws InterruptedException {
ExecutorService service = Executors.newFixedThreadPool(4);
for (int i = 0; i < 100; i++){
//想获得i,不能直接获得,lambda表达式涉及变量捕获,必须是final或事实final
//通过变量赋值解决
int id = i;
//提交任务
service.submit(()->{
System.out.println(id + " "+Thread.currentThread().getName());
});
}
}
}
运行结果:
* 执行这个代码,虽然100个任务都执行完毕了,但是,整个进程并没有结束。为什么呢??
——由于此处线程池创建出的线程默认都是“前台线程”,虽然main线程结束了,但是这些线程池里的“前台线程”,仍然存在,因此进程没有结束。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Executors1 {
public static void main(String[] args) throws InterruptedException {
ExecutorService service = Executors.newFixedThreadPool(4);
for (int i = 0; i < 100; i++){
//想获得i,不能直接获得,lambda表达式涉及变量捕获,必须是final或事实final
//通过变量赋值解决
int id = i;
//提交任务
service.submit(()->{
System.out.println(id + " "+Thread.currentThread().getName());
});
}
//最好不要立即就终止,可能使任务还没执行完,线程就被终止了.
Thread.sleep(2000);
//把线程池里所有的线程都终止掉
service.shutdown();
System.out.println("程序退出");
}
}
通过调用shutdown方法,可以结束整个进程。
* 使用线程池的时候,需要指定线程个数,那线程个数如何指定呢?
一台主机上,并不是只运行一个程序,并且程序不是每个线程都跑满cpu(全部都是算术运算就会跑满cpu)。线程工作过程中,可能会涉及到一些IO操作/阻塞操作,从而主动放弃cpu 。
因此,实际开发中,建议的做法是通过“实验”的方式,找到一个合适的线程池的个数:给线程池设置不同的线程数,分别进行性能测试,关注响应时间/消耗的资源指标,挑选一个比较合适的数值。
六. 模拟线程池
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class MyThreadPool {
private final BlockingQueue<Runnable> taskQueue;
private boolean isShutdown = false;
private final List<Thread> threads;
public MyThreadPool(int n){
threads = new ArrayList<>(n);
taskQueue = new ArrayBlockingQueue<>(1000);
//创建出 n 个线程
for(int i = 0;i < n;i++){
Thread t = new Thread(()->{
//循环从队列中获取任务执行
while(!isShutdown){
try {
Runnable task = taskQueue.take();
task.run();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
});
t.start();
threads.add(t);
}
}
//添加任务
public void submit(Runnable task) throws InterruptedException {
taskQueue.put(task);
}
//结束线程
public void shutdown(){
isShutdown = true;
for(Thread t : threads){
t.interrupt();
}
}
}