前言
当初熊某第一次接触数据库连接池的时候,简直是一脸懵逼!(令我想起一次在惠州罗浮山的天然游泳池上游泳的经历)连接对象我知道,还会有连接池?!生活中的一些事物都可以用到代码中吗?!顿时觉得我自己原来是那么渺小。
后来得知是提前创建预先约定好的连接对象,然后放到一个容器上(可以是数组、队列或者其它),这样就能减少连接创建时间。
那线程池也是同样的道理,在比较简单的程序下,你直接new Thread()也不会有什么大问题,毕竟在简单业务情况,创建和销毁线程的微小变化可以小到你都察觉不出来。
可惜啊,在二十一世纪二十年代(简称2020年)的现在,如果业务情况负责的情况下,直接new Thread()带来的资源损耗后果都直接可以让你宕机。
首先创建和销毁线程又是个大工程,毕竟线程也需要内存空间。大量线程创建都可以搞得你的程序报出OOM(Out of memory)的幸福红字。
其次大量线程回收也会影响GC回收时间。
再次你创建一个线程要5s,执行只需要2s,执行时间少于创建时间,感觉有点得不偿失啊。
不过,重点来了!
我们可以模仿连接池的设计思路,自己来实现一个线程池,这样就能避免以上的麻烦!
设计思路
相信各位(包括我)都已经蠢蠢欲动,不过在开始前还是要整理下思路:
- 我们需要一个队列,用于存放已创建的线程,这里用阻塞队列。
- 在创建线程池的时候应该要设定边界,无界或者边界特别大的线程池容易造成OOM。
- 需要一个任务提交方法,当提交任务后,空闲线程会自动执行,否则会在阻塞队列中等待。
实现环节
好了,接下来上代码!
public class MyThreadPool {
// 阻塞队列
private BlockingQueue<Runnable> workQueue;
// 用于存放已经创建的工作线程对象
private List<ExecuteThread> executeThreadList = new ArrayList<>();
// 创建线程池时需要设定创建的工作线程线程数(pool)大小
public MyThreadPool(int pool, BlockingQueue<Runnable> queue){
this.workQueue = queue;
for (int i = 0; i < pool; i++){
ExecuteThread executeThread = new ExecuteThread();
executeThread.start();
executeThreadList.add(executeThread);
}
}
// 提交任务
public void execute(Runnable command){
workQueue.add(command);
}
// 创建一个内部类作为工作线程类
class ExecuteThread extends Thread{
@Override
public void run() {
// 循环取出任务并执行
while (true){
try {
// 当调用take方法时,任务对象会自动出列
Runnable task = workQueue.take();
task.run();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
是不是很简单?接下来我们写个测试demo看看怎么样:
public static void main(String[] args) {
MyThreadPool myThreadPool = new MyThreadPool(5, new ArrayBlockingQueue<>(2));
myThreadPool.execute(() -> {
System.out.println("熊小哥好帅啊");
});
}
结果就和现实一样,非常符合:
代码如果理解的不太顺的话可以回去研究下。关于阻塞队列方面的知识我这里就不说了,因为还有更重要的要说,就是生产者-消费者模式!
什么是生产者-消费者模式
生产者-消费者模式一个有三个角色:生产者、缓存区、消费者。(图片网上找的)
产生数据的一方就是生产者,将产生的数据进行处理的一方就是消费者。
但是因为可能有时候生产者的效率不高,一分钟只能产出一条数据,而消费者可以一分钟处理十条数据,如果每次生产者产生一条数据,消费者就处理一条数据,这种方式可以说很低效,那么就引入了缓存区这一角色,生产者产生的数据放到缓存区,10分钟后消费者发现有10条数据,就进行处理,这之前消费者该干嘛就干嘛。这就是常说的批处理。同时生产者和消费者之间不需要相互调用,达到一个解耦的作用。
简单点说就是生产者就是产生数据然后把数据放到缓存区中,消费者则将缓存区的数据取出来进行处理,缓存区相当于一个中间点的角色。
了解这一设计模式可以加深我们对阻塞队列的认知,用上述代码来说明,首先线程池本身就是消费者,因为它是负责处理数据的:
// 负责处理数据,也就是消费阻塞队列上的任务,那么阻塞队列就相当于上面的缓存区这一角色
class ExecuteThread extends Thread{
@Override
public void run() {
// 循环取出任务并执行
while (true){
try {
// 当调用take方法时,任务对象会自动出列
Runnable task = workQueue.take();
task.run();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
我们使用线程池的地方就是生产者,生产后的数据交给线程池去处理,这里的生产者指的就是上面的测试demo代码:
public static void main(String[] args) {
MyThreadPool myThreadPool = new MyThreadPool(5, new ArrayBlockingQueue<>(2));
myThreadPool.execute(
() -> {
// 生产数据
System.out.println("熊小哥好帅啊");
});
}
结尾
今天的聊天差不多结束了,总结一下就是深入理解生产者-消费者模式可以更有助于我们对线程池的理解,我这里还有一些Java中的线程池没有介绍,各位看官感兴趣可以搜索看下。
好了,告辞告辞!