首先我们要知道线程池是为了解决什么问题才出现的,都包含哪些要素
线程池最主要的目的就是避免多次的创建和销毁线程,因为线程的创建和销毁是一个比较耗时的操作,我们希望某些线程能够保留下来完成后续任务。
对于java中的threadpool他有比较多的参数:
-
核心线程数
-
最大线程数
-
任务队列
-
辅助线程最大存活时间
-
时间单位
-
任务队列满后的拒绝策略
分析完线程池的作用和结构后我们要尝试搭建出一个自定义的线程池。
1. 对比不使用线程池而是来一个任务我们就创建一个线程的方式
通过new Thread()方式来执行任务,我们没法对一个线程多次使用:
-
当线程的
run()
方法执行完毕后,线程会进入TERMINATED
状态,此时线程不再执行任何代码,也不再消耗资源。
-
线程对象(
Thread
实例)本身并不会被自动销毁,但它会成为一个普通的Java对象,如果没有其他引用指向它,最终会被垃圾回收器(GC)回收。
2. 因为线程池不是来一个任务就new一个线程,所以不是所有任务都是第一时间就被执行的,为了避免任务丢失我们就需要有一个任务队列来存放任务,然后由线程来取出执行。
3. 我们需要保留一些线程(核心线程),这些线程不会被销毁而是不断地从任务队列中去取出任务来执行.那么我们怎么保证线程不会被销毁呢,我们在上面已经有提到过了,一个线程他的run()方法执行完就会进入销毁状态,所以我们可以在线程的run方法中添加while(true)循环来保证线程不被销毁(先用while(true),后面可以优化,我们先以实现为主)。
Thread thread = new thread(() -> {
while(true) {
//如果有任务,从任务队列中取出任务
if(!common.isEmpty()) {
}
}
});
4. 我们是保留了一些线程,但是如果任务太多了已有线程处理不过来怎么办?就和开饭店一样,有3个正式员工了(核心线程),但是在饭点的时候还是忙不过来,那我们就可以找几个临时工(辅助线程)来帮忙,等不忙的时候就让他们离开。线程池也是这样,(辅助线程数=最大线程数-核心线程数)辅助线程可以当作是一部分冗余。并且辅助线程是可以被销毁的,当我们发现核心线程可以处理任务后就代表辅助线程没有存在的必要了应该被销毁。
5. 虽然辅助线程在空闲的时候应该被销毁,但是我们也不能太快决定,可能你只是空闲了一小会然后又忙起来了,如果你一检测到空闲就把辅助线程销毁了后面又需要重新创建浪费时间。所以我们也需要留一些冗余,如果过了一段时间还是空闲那么我们才认为辅助线程可以被销毁。这段冗余时间就是辅助线程的最大存活时间(还有时间单位决定)。
6. 现在就剩最后一个参数(拒绝策略)我们没讲了,拒绝策略就是当任务队列满了并且已经达到最大线程数的时候(即任务到来的速度超过了处理速度)对于新来的任务我们用什么策略去处理。
-
我们可以直接将这部分任务丢弃
-
也可以用这部分任务去代替任务队列中的部分任务
通过以上分析我们写出了一份最简单的线程池代码:
/*
* 这个是只有一个线程的线程池,我们先实现简单的的再往难的去拓展
* */
public class OnlyOneThread {
//任务队列
private final ArrayList<Runnable> taskQueue = new ArrayList<>();
//唯一线程
private final Thread thread = new Thread(() -> {
while (true) {
//判断任务队列有没有任务
if(!taskQueue.isEmpty()) {
//取出任务并执行
Runnable task = taskQueue.remove(0);
task.run();
}
}
}, "lpl");
{
//启动线程
thread.start();
}
void execute(Runnable task) {
//我们接收到一个任务后需要将任务加入任务队列
taskQueue.add(task);
}
}
但是我们也发现了如果taskQueue是空的,我们的线程并不会停下而是不断的循环判断,这是很浪费cpu资源的,所以我们希望在发现taskQueue空的时候阻塞线程,等不为空的时候再继续运行。这就很符合阻塞 队列的特点,所以我们可以用阻塞队列来改进。
public class OnlyOneThread {
//任务队列
BlockingQueue<Runnable> taskQueue = new ArrayBlockingQueue<>(1024);
//唯一线程
private final Thread thread = new Thread(() -> {
while (true) {
try {
//取出任务并执行
Runnable task = taskQueue.take();// 阻塞队列,如果队列中没有任务,就会阻塞等待
task.run();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}, "lpl");
{
//启动线程
thread.start();
}
void execute(Runnable task) {
//我们接收到一个任务后需要将任务加入任务队列, 使用add方法在taskQueue满的时候会抛异常而offer和add方法类似但在队列满的时候会返回false
boolean offer = taskQueue.offer(task);
}
}
我们接下来就可以拓展多个线程的线程池了(在代码中注释已经比较清楚了,这里就不重复说明了),这段代码中没有实现拒绝策略,其实这个东西大家知道就行,大部分情况我们都是不去处理多出来的任务的,也就是直接丢弃掉。
tip:这个线程池demo没有对多线程环境下的线程竞争添加锁,真实情况肯定更复杂,我们可以通过synchronized关键字或CAS原子操作来解决。
public class MyThreadPool {
//我们需要一个任务队列
private final BlockingQueue<Runnable> taskQueue;
//核心线程数
private final int coreSize;
//核心线程
private final ArrayList<Thread> coreThread = new ArrayList<>();
//最大线程数
private final int maxSize;
//辅助线程
private final ArrayList<Thread> helperThread = new ArrayList<>();
//辅助线程最大存活时间
private final long keepAliveTime;
//辅助线程存活时间单位
private final TimeUnit unit;
//让用户根据自己需要来创建线程池
public MyThreadPool(int coreSize, int maxSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> taskQueue) {
this.coreSize = coreSize;
this.maxSize = maxSize;
this.keepAliveTime = keepAliveTime;
this.unit = unit;
this.taskQueue = taskQueue;
}
//核心线程
class CoreThread extends Thread {
@Override
public void run() {
//核心线程需要执行的逻辑
while (true) {
//判断任务队列有没有任务
if(!taskQueue.isEmpty()) {
try {
//取出任务并执行
Runnable task = taskQueue.take();// 阻塞队列,如果队列中没有任务,就会阻塞等待
task.run();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
}
//辅助线程
class HelperThread extends Thread {
@Override
public void run() {
//阻塞线程需要执行的逻辑,因为辅助线程在空闲一段时间后需要被销毁,我们发现BlockingQueue的API中有一个poll方法,它可以在一段时间没能拿到对象后就返回null
while (true) {
//判断任务队列有没有任务
if(!taskQueue.isEmpty()) {
try {
//取出任务并执行
Runnable task = taskQueue.poll(keepAliveTime, unit);//如果超过等待时间没能拿到任务就说明当前空闲不需要辅助线程
if(task == null) {//辅助线程需要被销毁
break;//跳出循环,结束线程生命周期
}
task.run();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
System.out.println("辅助线程被销毁!");
//从helperTread中移除
helperThread.remove(Thread.currentThread());
}
}
void execute(Runnable task) {
if (coreThread.size() < coreSize) {//核心线程还没满
Thread thread = new CoreThread();//创建核心线程执行任务
coreThread.add(thread);
thread.start();
}
//核心线程已经满了,task不一定能被马上执行我们需要先将他提交给任务队列
boolean offer = taskQueue.offer(task);
//如果队列满了,我们就创建辅助线程来执行任务,线程池中创建辅助线程的前提就是任务队列满并且无核心线程空闲
if (!offer) {
if (helperThread.size() < maxSize - coreSize) {//辅助线程还没满
Thread thread = new HelperThread();//创建辅助线程执行任务队列中的任务
helperThread.add(thread);
thread.start();
}
//因为我们创建了辅助线程去任务队列拿任务去了,所以任务队列可能会空出来(为什么说是可能呢?因为在多线程环境可能会有别的线程先提交),所以我们再尝试一次将task提交到taskQueue中
if(!taskQueue.offer(task)) {//如果队列还是满的,我们就抛出异常
throw new RuntimeException("任务队列已满,无法提交任务");
}
}
}
}