博客有一个多月没更新了,主要是因为刚换了工作,需要适应一下新环境,另外新公司正好赶上了几个比较忙的项目,每天晚上到家就比较晚了,实在是分身乏术,不过该更新还是要更新滴,写博客贵在坚持,今天就来讲一下线程池的复用原理吧,希望能对你有所帮助!
提起线程,相信大家并不陌生,它可以帮助我们异步处理任务,提高CPU的利用率。在平时的开发中我们通常会利用线程池来创建和使用线程,这样我们可以对线程进行重复利用,避免频繁创建线程带来的内存开销,它会自动帮助我们对线程资源进行调度,简单,方便,快捷。
在日常的开发中,我们可以使用JDK自带的线程池工具类Executor来创建和使用线程池,创建方式及其优缺点如下:
//根据线程的使用情况自行创建线程,按需分配,优点:自动调整线程池大小,缺点:无限制创建,容易OOM
Executors.newCachedThreadPool();
//创建指定线程数量的线程池 优点:可以创建指定数量 缺点:不够灵活
Executors.newFixedThreadPool(5);
//同newCachedThreadPool,只不过可以指定核心线程数,不常用
Executors.newScheduledThreadPool(5);
//创建只存在一个线程的线程池 优点:可以保证任务顺序指定 缺点:cpu利用率较低
Executors.newSingleThreadExecutor();
由于JDK自带的工具类有较多的局限性,所以我们通常会自定义线程池的参数来手动创建线程,阿里的编码规约中也强制要求这样做,使用方法如下:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
RejectedExecutionHandler handler)
/*corePoolSize : 线程的核心线程数
maximumPoolSize:线程允许的最大线程数
keepAliveTime:当前线程池线程总数大于核心线程数时,终止多余的空闲线程的时间
Unit:keepAliveTime:参数的时间单位
workQueue:任务缓存队列,如果线程池达到了核心线程数,并且其他线程都处于活动状态的时候,则将新任务放入此队列
threadFactory:定制线程的创建过程
Handler:拒绝策略,当workQueue队列满时,采取的措施*/
通过new ThreadPoolExecutor来创建线程池的方式明显更加灵活,可定制性更强,JDK提供的Executor工具类本质也是new 了一个ThreadPoolExecutor,感兴趣的朋友可以通过源码自行查看。
线程池的使用方法也非常简单,在这里举个简单的例子,如下:
ExecutorService fixExecutor = Executors.newFixedThreadPool(5);
//执行线程池中的任务
fixExecutor.execute(() -> {
System.out.println(Thread.currentThread().getName() + ":线程运行!");
});
//运行完任务后停止运行
fixExecutor.shutdown();
虽然线程池使用起来容易,但在看的你可能对线程池中线程的复用原理并不了解,下面我们手写一个简易线程池来帮助理解。
对于线程池的复用原理,可以简单的用一句话概括:创建指定数量的线程并开启,判断当前是否有任务执行,如果有则执行任务。再通俗易懂一些:创建指定数量的线程并运行,重写run方法,循环从任务队列中取Runnable对象,执行Runnable对象的run方法。
图示:
接下来开始手写线程池吧,注意是简易线程池,跟JDK自带的线程池无法相提并论,在这里我省略了判断当前线程数有没有大于核心线程数的步骤,简化成直接从队列中取任务,对于理解原理来说已然足矣,代码如下:
public class MyExecutorService {
/**
* 一直保持运行的线程
*/
private List workThreads;
/*
* 任务队列容器
*/
private BlockingDeque taskRunables;
/*
* 线程池当前是否停止
*/
private volatile boolean isWorking = true;
public MyExecutorService(int workThreads, int taskRunables) {
this.workThreads = new ArrayList<>();
this.taskRunables = new LinkedBlockingDeque<>(taskRunables);
//直接运行核心线程
for (int i = 0; i < workThreads; i++) {
WorkThread workThread = new WorkThread();
workThread.start();
this.workThreads.add(workThread);
}
}
/**
* WorkThread累,线程池的任务类,类比JDK的worker
*/
class WorkThread extends Thread {
@Override
public void run() {
while (isWorking || taskRunables.size() != 0) {
//获取任务
Runnable task = taskRunables.poll();
if (task != null) {
task.run();
}
}
}
}
//执行execute,jdk中会存在各种判断,这里省略了
public void execute(Runnable runnable) {
//把任务加入队列
taskRunables.offer(runnable);
}
//停止线程池
public void shutdown() {
this.isWorking = false;
}
}
代码有具体注释,耐心看一看非常容易理解,下面测试一下:
//测试自定义的线程池
public static void main(String[] args) {
MyExecutorService myExecutorService = new MyExecutorService(3, 6);
//运行8次
for (int i = 0; i < 8; i++) {
myExecutorService.execute(() -> {
System.out.println(Thread.currentThread().getName() + "task begin");
});
}
myExecutorService.shutdown();
}
执行结果如下:
测试成功,手写线程池到此结束!
通过以上分析并手写线程池,我们应该已经基本理解了线程池的复用机制原理,实际上JDK的实现机制远比我们手写的要复杂的多,主要有以下两点,可以让我们进一步加深理解:
1.当有新任务来的时候,首先判断当前的线程数有没有超过核心线程数,如果没超过则直接新建一个线程来执行新的任务,如果超过了则判断缓存队列有没有满,没满则将新任务放进缓存队列中,如果队列已满并且线程池中的线程数已经达到了指定的最大线程数,那就根据相应的策略拒绝任务,默认为抛异常。
2.当缓存队列中的任务都执行完毕后,线程池中的线程数如果大于核心线程数并且已经超过了指定的存活时间(存活时间通过队列的poll方法传入,如果指定时间内没有获取到任务,则break退出,线程运行结束),就销毁多出来的线程,直到线程池中的线程数等于核心线程数。此时剩余的线程会一直处于阻塞状态,等待新的任务到来。
好了,以上就是对线程池复用机制的具体分析,这点在面试中也属于高频问题,还是需要我们掌握一下的,学习一门技术,做到知其然,知其所以然,才能做到融会贯通,发挥出它真正的作用!
ps:代码生成器近期会更新哒,别催啦。。
喜欢的朋友可以关注公众号 螺旋编程极客 获取最新内容更新哦!