线程池没你想的那么简单(中)

简介: 自己动手写一个五脏俱全的线程池,同时会了解到线程池的工作原理,以及如何在工作中合理的利用线程池。

时刻小心



从上面流程图的这两步可以看出会直接创建新的线程


这个过程相对于中间直接写入阻塞队列的开销是非常大的,主要有以下两个原因:


  • 创建线程会加锁,虽说最终用的是 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(); 处,即便是线程缩容了也不会小于核心线程数。


通过堆栈也能证明:



恰好剩下三个线程阻塞于此处。


相关文章
|
3月前
|
NoSQL Java 应用服务中间件
|
1月前
|
缓存 Java
|
3月前
|
缓存 Java API
厉害了,线程池就该这么玩
厉害了,线程池就该这么玩
25 0
|
8月前
|
Java
线程池总结
线程池总结
39 0
|
8月前
|
缓存 Java
线程池简单总结
线程池简单总结
61 0
|
5月前
|
Java
6. 实现简单的线程池
6. 实现简单的线程池
25 0
|
9月前
|
存储 Java 调度
线程池使用
线程池使用
|
9月前
|
存储 缓存 Java
理解与实现线程池
理解与实现线程池
108 0
|
10月前
|
存储 Java 测试技术
13.一文彻底了解线程池
大家好,我是王有志。线程池是Java面试中必问的八股文,涉及到非常多的问题,今天我们就通过一篇文章,来彻底搞懂Java面试中关于线程池的问题。
369 2
13.一文彻底了解线程池
|
10月前
|
设计模式 Java C++