线程池
1. 引子
在多线程环境下,存在这样一种业务场景:需要对线程进行频繁的创建和删除,这种场景是非常消耗我们的系统资源的,对此,我们可以引入线程池来提前创建好线程为我们服务
2. 概念
线程池:字面意思理解就是存放着线程的池子,能够将我们需要使用的线程在池子中提前准备好,需要使用线程时直接从线程池中获取,用完了再重新放回线程池等待被重新调用,减少了我们创建线程和销毁线程的开销,同时因为从线程池中取出和放回线程是纯用户态代码,操作速度比内核操作更快更稳定!!
3. ThreadPoolExecutor类
java标准库为我们提供了一个线程池的api:ThreadPoolExecutor
通过查询java文档可知它有以下四种构造方法:
这里我们主要讲解一下最后一种,它的参数是最全的:
-
int corePoolSize
:核心线程数,类似于最小线程数,线程池中最后的底线~ -
int maximumPoolSize
:最大线程数,可以在核心线程数的基础上继续添加线程,但最终线程数量不能超过最大线程数 -
long keepAliveTime
:保持存活时间,即除核心线程外的其它线程处于空闲状态时能够存在的最长时间 -
TimeUnit unit
:保持存活时间的时间单位,可以为ms,s,min,hour等 -
BlockingQueue<Runnable> workQueue
:任务队列,用来存放需要执行的任务,这里使用的是阻塞队列,它维护了线程安全以及阻塞功能 -
ThreadFactory threadFactory
:线程工厂,遵循工厂模式工厂模式:一种设计模式,通过工厂类来创建指定的对象,即通过静态方法将new操作封装起来,在方法内部设定不同属性完成对象初始化,构造对象的过程称为工厂模式
这里的
ThreadFactory
类封装了操作new Thread
的方法,通过这个类来创建线程对象,文档介绍如下: -
RejectedExecutionHandler handler
:拒绝策略,线程池在执行任务时需要遵循的策略,我们知道线程池中维护了一个任务队列,它存储任务的容量是有限的,当队列的存储容量满了的时候,又有新的任务进来了,对于线程池来说,该怎么做?这个时候它需要遵循以下四个拒绝策略之一:- ThreadPoolExecutor.AbortPolicy:直接抛出异常,新任务和老任务都无法执行了(相当于心态崩了,什么活都不想做~~)
- ThreadPoolExecutor.CallerRunsPolicy:由添加任务的线程来执行这个任务(谁揽的活谁做~~)
- ThreadPoolExecutor.DiscardOldestPolicy:丢弃最老的任务,然后来执行这个最新的任务(可能是boss下发的任务,优先级还挺高)
- ThreadPoolExecutor.DiscardPolicy:都不管这个新任务,继续执行自己的任务(与第二不同,这个新活没人做,第二还有任务添加者做)
ThreadPoolExecutor类适用于需要高度定制化线程池时使用,而如果只是需要简单使用线程池的话,可以使用Executors
4. Executors类
Executros是标准库提供的一个工厂类,它对ThreadPoolExecutor进行了封装,通过这个类我们可以创建出多个不同的线程池对象:
其中:
newFixedThreadPool
:固定线程数目的线程池newCachedThreadPool
:线程数目可以动态扩容的线程池newSingleThreadExecutor
:只能包含单个线程的线程池newScheuledThreadPool
:类似于定时器,可创建执行延时任务的线程
代码示例🌰:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolExecutorTest {
public static void main(String[] args) {
// 创建一个固定线程数目的线程池
ExecutorService executorService = Executors.newFixedThreadPool(5);
executorService.submit(new Runnable() {
@Override
public void run() {
System.out.println("Hello!!");
}
});
}
}
// 执行结果
// Hello!!
拓展
在创建线程池的时候,很多时候都需要我们先确定好要创建线程的数量,这该怎么确定??网络上有很多的说法,如通过CPU的逻辑核心数来确定,但其实对于不同的程序来说,能够设定的线程数量是不同的,更应该结合实际情况来分析,比如看它这个进程中线程是CPU密集型多还是I/O密集型多,当然很多时候一个进程中的线程,一部分是CPU密集型,一部分是I/O密集型,这里的比例无法完全估计,对此,需要我们通过测试实验来找到那个合适的值,尝试给线程池设定不同的线程数目,分别进行性能测试,衡量不同线程数目下进程总的时间开销还有系统资源开销,然后找到两者之间合适的值!!
5. 自定义线程池
java标准库为我们提供了上述的线程池类,我们也可以尝试自己构建一个线程池!!可以按下述四个步骤来:
- 提供一个阻塞队列,用来保存需要执行的任务(使用java提供的阻塞队列的话,已经维护好了线程安全与阻塞,这里就不需要重复维护了);
- 提供一个列表用来保存创建的线程;
- 提供一个构造方法,用来指定创建多少个线程,且每个线程都有执行任务操作的功能(这里我们创建固定线程数目的线程池);
- 提供一个submit方法,用来添加任务
代码示例🌰:
class MyThreadPoolExecutor {
// 用来保存创建的线程
private List<Thread> threadList = new ArrayList<>();
// 用来保存需要执行的任务
private BlockingQueue<Runnable> taskQueue = new ArrayBlockingQueue<>(1000); // 这里限制阻塞队列容量
// 通过 n 来指定创建的线程数量
public MyThreadPoolExecutor(int n) {
for (int i = 0; i < n; i++) {
Thread t = new Thread(() -> {
while (true) {
try {
// 从任务队列中取出任务,这里的阻塞队列已经维护了线程安全与阻塞功能
Runnable task = taskQueue.take(); // 当队列为空时会进入阻塞
task.run();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
threadList.add(t);
}
}
public void submit(Runnable task) throws InterruptedException {
taskQueue.put(task);
}
}
这样我们就自己创建好了一个线程池!!通过下述代码测试一下:
public class MyThreadPoolExecutorTest {
public static void main(String[] args) throws InterruptedException {
MyThreadPoolExecutor myThreadPoolExecutor = new MyThreadPoolExecutor(5); // 这里创建5个线程
for (int i = 0;i < 1000;i++) {
int n = i; // 这里为了解决变量捕获问题(在lambda中调用外部变量就会产生的问题)
myThreadPoolExecutor.submit(new Runnable() {
@Override
public void run() {
System.out.println("当前线程为:" + Thread.currentThread().getName() + ", 执行任务:" + n);
}
});
}
}
}
执行结果:
可以看到我们自己创建的线程池能够成功使用了!!
以上便是线程池的介绍与使用流程了,如果这些内容对大家有帮助的话请给一个三连关注吧💕( •̀ ω •́ )✧( •̀ ω •́ )✧✨