时刻小心
从上面流程图的这两步可以看出会直接创建新的线程。
这个过程相对于中间直接写入阻塞队列的开销是非常大的,主要有以下两个原因:
- 创建线程会加锁,虽说最终用的是 ConcurrentHashMap 的写入函数,但依然存在加锁的可能。
- 会创建新的线程,创建线程还需要调用操作系统的 API 开销较大。
所以理想情况下我们应该避免这两步,尽量让丢入线程池中的任务进入阻塞队列中。
执行任务
任务是添加进来了,那是如何执行的?
在创建任务的时候提到过 worker.startTask()
函数:
/** * 添加任务,需要加锁 * @param runnable 任务 */ private void addWorker(Runnable runnable) { Worker worker = new Worker(runnable, true); worker.startTask(); workers.add(worker); }
也就是在创建线程执行任务的时候会创建 Worker
对象,利用它的 startTask()
方法来执行任务。
所以先来看看 Worker
对象是长啥样的:
其实他本身也是一个线程,将接收到需要执行的任务存放到成员变量 task
处。
而其中最为关键的则是执行任务 worker.startTask()
这一步骤。
public void startTask() { thread.start(); }
其实就是运行了 worker
线程自己,下面来看 run
方法。
- 第一步是将创建线程时传过来的任务执行(
task.run
),接着会一直不停的从队列里获取任务执行,直到获取不到新任务了。
- 任务执行完毕后将内置的计数器 -1 ,方便后面任务全部执行完毕进行通知。
- worker 线程获取不到任务后退出,需要将自己从线程池中释放掉(
workers.remove(this)
)。
从队列里获取任务
其实 getTask
也是非常关键的一个方法,它封装了从队列中获取任务,同时对不需要保活的线程进行回收。
很明显,核心作用就是从队列里获取任务;但有两个地方需要注意:
- 当线程数超过核心线程数时,在获取任务的时候需要通过保活时间从队列里获取任务;一旦获取不到任务则队列肯定是空的,这样返回
null
之后在上文的run()
中就会退出这个线程;从而达到了回收线程的目的,也就是我们之前演示的效果
- 这里需要加锁,加锁的原因是这里肯定会出现并发情况,不加锁会导致
workers.size() > miniSize
条件多次执行,从而导致线程被全部回收完毕。
关闭线程池
最后来谈谈线程关闭的事;
还是以刚才那段测试代码为例,如果提交任务后我们没有关闭线程,会发现即便是任务执行完毕后程序也不会退出。
从刚才的源码里其实也很容易看出来,不退出的原因是 Worker
线程一定还会一直阻塞在 task = workQueue.take();
处,即便是线程缩容了也不会小于核心线程数。
通过堆栈也能证明:
恰好剩下三个线程阻塞于此处。