这个标题我取得一股营销号味道hhh,很早之前学习多线程的时候有自己写过一个简易的线程池,最近读了源码发现自己以前写的代码有很多可以改进的地方。我尤其喜欢用迭代的方式去学习——先实现,然后深入学习,最后改进之前实现的,完成一个迭代。因为这种方式更能看到自己的思路的过程,更明白所以然。好了,闲话不多说。开撸。
本文分为三个部分:
一、如何实现一个简单的线程池?
二、jdk线程池源码分析
三、改进自实现的线程池
一、如何实现一个简单的线程池?
思路分析:线程池其实就是一个生产者消费者模型。
首先我们需要一个队列作为任务容器作为存放任务, 还需要多个线程去执行任务, 消费者线程不断查询任务队列是否有任务,如果没有该线程等待,如果有任务,取出一个任务,唤醒所有在等待获取队列的线程,释放掉锁。执行任务线程的run方法。
分析完毕,在开始写代码之前想想具体做法。
- 任务容器,用LinkedList实现。
- add方法,用于把任务线程放入任务容器。
- 构造函数,一次性启动3个 消费者线程。
- 任务:一个有run方法的线程。
1 | public class ThreadPool { |
1 | public class TestThreadPool { |
启动TestThreadPool,输出
大功告成。
注意
ConsumerThread类中这段代码:
1 | synchronized (tasks) { |
从任务中取任务时候是获取任务队列锁的,但执行任务的时候要先释放锁。如果不释放就去执行,那它执行的过程中其他线程都得等着,相当于一次只能有一个线程进行任务。
2019年06月13号注:最近读书《java开发手册》OOP规约第11条:构造方法里禁止加入任何业务逻辑,如果有初始化逻辑,请放在init中。上面代码违背了这个规约。我这里也不改了,做个反例。再接下来的代码中改进
二、jdk线程池源码分析
线程池的重要方法
execute()接受任务
1 | public void execute(Runnable command) { |
解释一下这行代码, 这个方法会返回一个表示 “线程池状态” 和 “线程数” 的整数。
1 | //一开始的状态是ctlOf(RUNNING, 0) |
线程池的状态
- RUNNING:接受新的任务,处理等待队列中的任务
- SHUTDOWN:不接受新的任务提交,但是会继续处理等待队列中的任务
- STOP:不接受新的任务提交,不再处理等待队列中的任务,中断正在执行任务的线程
- TIDYING:所有的任务都销毁了,workCount 为 0。线程池的状态在转换为 TIDYING 状态时,会执行钩子方法 terminated()
- TERMINATED:terminated() 方法结束后,线程池的状态就会变成这个
重点区分SHUTDOWN和STOP。
在提一下关闭线程池的方法。
shutdown():设置 线程池的状态 为 SHUTDOWN,然后中断所有没有正在执行任务的线程
shutdownNow():设置 线程池的状态 为 STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表
使用建议:一般调用shutdown()关闭线程池;若任务不一定要执行完,则调用shutdownNow()
addWorker()方法:增加工作线程
1 | // 参数一:第一个任务。参数二:是否使用核心线程数 corePoolSize 作为创建线程的界限 |
总结
线程池的线程创建时机
- 线程池是懒加载的,声明的时候并不会创建好线程等待任务,而是当提交第一个任务时才去新建线程;
- 当提交一个任务时,如果当前线程数小于corePoolSize,就直接创建一个新线程执行任务;
- 如果当前线程数大于corePoolSize,继续提交的任务被保存到阻塞队列中,等待被执行;
- 如果阻塞队列满了,并且当前线程数小于maxPoolSize,那就创建新的线程执行当前任务;
- 如果池里的线程数大于maxPoolSize,这时再有任务来,只能调用拒绝策略。
工厂模式
从线程池的创建讲起,线程池有两种创建方式。
一是ThreadPoolExecutor threadPool= new ThreadPoolExecutor(10, 15, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
二是executorService= Executors.newFixedThreadPool(3);
第二钟方式用到了工厂模式,点进方法newFixedThreadPool(3)去看是返回一个定义好初始化的参数线程池实例,代码如下:
1 | public static ExecutorService newFixedThreadPool(int nThreads) { |
第二种方法最终还是会导向方法一ThreadPoolExecutor的构造方法ThreadPoolExecutor
1 | public ThreadPoolExecutor(int corePoolSize, |
工厂模式的好处是:
- 用户只要知道名字就行了,比如我想创建一个固定长度的线程池,只需要调用newFixedThreadPool()方法,而不需要在了解ThreadPoolExecutor构造函数之后自己new一个配置一堆参数。不需要关注太多细节。
- 用工厂模式可以消除循环依赖,如果一个类构造函数的参数变了,所有实例化这个类的代码都得改。
阻塞队列
上一篇我是用synchronized锁住任务队列,工作线程wait和notify去实现线程阻塞去任务队列去任务,jdk是用LinkedBlockingQueue的put和take方法来实现。如果put或者take操作无法立即执行,这两个方法调用将会发生阻塞,直到能够执行。
抛出异常 | 特殊值 | 阻塞 | 超时 | |
---|---|---|---|---|
插入 | add(e) |
offer(e) |
put(e) |
offer(e, time, unit) |
移除 | remove() |
poll() |
take() |
poll(time, unit) |
检查 | element() |
peek() |
不可用 | 不可用 |
延迟加载Lazy_load
线程池是懒加载的,声明的时候并不会创建好线程等待任务,而是当提交第一个任务时才去新建线程;
三、改进自实现的线程池
改进
1.实现延迟加载。我们是在构造函数创建线程,而java线程池只有当提交一个任务时,线程池才会创建一个新线程执行任务,直到当前线程数等于corePoolSize。这样会更节约资源。
2.这里我们用LinkedList加线程wait()notify()来实现,也可以像java线程池一样用阻塞链表LinkedBlockingQueue来实现。
1 | import java.util.HashSet; |
1 | public class TestThreadPool { |
参考