【重难点】【JUC 05】线程池核心设计与实现、线程池使用了什么设计模式、要你设计的话,如何实现一个线程池
文章目录
一、线程池核心设计与实现
线程池核心实现类是 ThreadPool,下图为 ThreadPoolExcutor 的 UML 类图
ThreadPoolExcutor 实现的顶层接口是 Executor,顶层接口 Excutor 提供了一种思想:将任务提交和任务执行进行解耦。用户无需关注如何创建线程、如何调度线程来执行任务,用户只需要提供 Runnable 对象,将任务的运行逻辑提交到执行器(Executor)中,由 Executor 框架完成线程的调配和任务的执行部分
ExecutorService 接口增加了一些能力:
- 扩充执行任务的能力,补充可以为一个或一批异步任务生成 Future 的方法
- 提供了管控线程池的方法,比如停止线程池的运行
AbstractExecutorService 则是上层的抽象类,将执行任务的流程串联了起来,保证下层的实现只需关注一个执行任务的方法即可
最下层的实现类 ThreadPoolExecutor 实现最复杂的运行部分,ThreadPoolExecutor 将会一方面维护自身的生命周期,另一方面同时管理线程和任务,使两者良好地结合从何执行并行任务
ThreadPoolExecutor 的运行机制如下图所示:
线程池在内部实际上构建了一个生产者消费者模型,将线程和任务两者解耦,并不直接关联,从而良好地缓冲任务,复用线程。线程池的运行主要分为两部分:任务管理、县城管理。任务管理部分充当生产者的角色,当任务提交后,线程池会判断该任务后续的流传:
- 直接申请线程执行该任务
- 缓冲到队列中等待线程执行
- 拒绝该任务
线程管理部分是消费者,它们被同一维护在线程池内,根据任务请求进行线程的分配,当线程执行完任务后则会继续获取新的任务去执行,最终当线程获取不到任务的时候,线程就会被回收
二、线程池使用了什么设计模式
数据库连接池、线程池本质上都是连接池技术,连接池技术的核心是让创建的资源复用,通过减少创建和销毁来提升性能
这正是享元模式的理念,享元模式的目的是实现对象的共享,当系统中对象多的时候可以减少内存的开销,通常与工厂模式一起使用
FlyWeightFactory 负责创建和管理享元单元,当一个客户端请求时,工厂需要检查当前对象池中是否有符合条件的对象,如果有,就返回已经存在的对象,如果没有,则创建一个新对象
设计模式详见【设计模式】第四章 工厂模式 和 【设计模式】第七章 享元模式
三、要你设计的话,如何实现一个线程池
1.线程池的关键变量
- 一个存放所有线程的集合
- 一个任务分配给线程池的时候,线程池可以分配一个线程处理它,当线程池中没有空闲线程时,我们还需要一个队列来存储提交给线程池的任务
- 初始化一个线程时,要指定这个线程池的大小
- 我们还需要一个变量来保存已经运行的线程数目
//存放线程的集合
private ArrayList<MyThread> threads;
//任务队列
private ArrayBlockingQueue<Runnable> taskQueue;
//线程池初始限定大小
private int threadNum
//已经工作的线程数目
private int workThreadNum;
2.线程池的核心方法
执行任务
向线程池提交一个任务时,如果已经运行的线程 < 线程池大小,则创建一个线程运行任务,并把这个线程放入线程池,否则将任务放入缓冲队列中
public void execute(Runnable runnable){
try{
mainLock.lock();
//线程池未满,每加入一个任务则开启一个线程
if(workThreadNum < threadNum){
MyThread myThread = new MyThread(runnable);
myThread.start();
threads.add(myThread);
workThreadNum++;
}
//线程池已满,放入任务队列,等待有空闲线程时执行
else{
//队列已满,无法添加时,拒绝任务
if(!taskQueue.offer(runnable)){
rejectTask();
}
}finally{
mainLock.unlock();
}
}
从任务队列中取出任务,分配给线程池中 “空闲” 的线程完成
很容易想到的思路是额外开启一个线程,时刻监控线程池的线程空余情况,一旦有线程空余,则马上从任务队列取出任务,交付给空余线程完成
这种思路理解起来很容易,但仔细思考,实现起来非常麻烦。第一,如何检测到线程池中的空闲线程是一个问题。第二,如何将任务交付给一个 .start() 运行状态中的空闲线程。想要实现这两点都不轻松
所以我们转换一下思维,线程池中的所有线程一直都是运行状态的,线程的空闲只是代表此刻它没有在执行任务而已。一旦运行中的线程没有执行任务,就自己去队列中取任务执行
为了达到这种效果,我们要重写 run 方法,所以要自定义一个 MyThread 类,把线程都放在这个自定义线程类
class MyThread extends Thread{
private Runnable task;
public MyThread(Runnable runnable){
this.task = runnable;
}
@Override
public void run(){
//该线程一直启动着,不断地从任务队列取出任务执行
while(true){
//如果初始化任务不为空,则执行初始化任务
if(task != null){
task.run();
task = null;
}
//否则去任务队列取任务并执行
else{
Runnable queueTask = taskQueue.poll();
if(queueTask != null)
queueTask.run();
}
}
}
}
完整代码
public class MyThreadPool{
public static void main(String[] args){
MyThreadPool myThreadPool = new MyThreadPool(5);
Runnable task = new Runnable(){
@Override
public void run(){
System.out.println(Thread.currentThread().getName()+"执行中");
}
};
for(int i = 0; i < 20; i++){
myThreadPool.execute(task);
}
}
//存放线程的集合
private ArrayList<MyThread> threads;
//任务队列
private ArrayBlockingQueue<Runnable> taskQueue;
//线程池初始限定大小
private int threadNum;
//已经工作的线程数目
private int workThreadNum;
private final ReentrantLock mainLock = new ReentrantLock();
public MyThreadPool(int initPoolNum){
threadNum = initPoolNum;
threads = new ArrayList<>(initPoolNum);
//任务队列初始化为线程池线程数的四倍
taskQueue = new ArrayBlockingQueue<>(initPoolNum*4);
threadNum = initPoolNum;
workThreadNum = 0;
}
public void execute(Runnable runnable){
try{
mainLock.lock();
//线程池未满,每加入一个任务则开启一个线程
if(workThreadNum < threadNum){
MyThread myThread = new MyThread(runnalbe);
myThread.start();
thread.start();
threads.add(myThread);
workThreadNum++;
}
//线程池已满,放入任务队列,等待有空闲线程时执行
else{
//队列已满,无法添加时,拒绝任务
if(!taskQueue.offer(runnable)){
rejectTask();
}
}
}finally{
mainLock.unlock();
}
}
private void rejectTask(){
System.out.println("任务队列已满,无法继续添加,请扩大您的初始化线程池");
}
class MyThread extends Thread{
private Runnable task;
public MyThread(Runnable runnable){
this.task = runnable;
}
@Override
public void run(){
//该线程一直启动着,不断地从任务队列取出任务执行
while(true){
//如果初始化任务不为空,则执行初始化任务
if(task != null){
task.run();
task = null;
}
//否则去任务队列取任务并执行
else{
Runnable queueTask = taskQueue.poll();
if(queueTask != null)
queueTask.run();
}
}
}
}
}
总结一下自定义线程池的工作流程:
- 初始化线程池,指定线程池大小
- 向线程池中放入任务执行
- 如果线程池中创建的线程数未满,则创建我们自定义的线程类放入线程池集合,并执行任务。任务完成后该线程会一直监听队列
- 如果线程池中创建的线程数已满,则将任务放入缓冲任务队列
- 线程池中所有创建的线程都会一直从缓存任务队列中取任务,取到任务后立即执行