上面已经知道了,线程池更加充分合理的协调利用cpu 、内存、网络、i/o等系统资源,减少线程创建和销毁时候浪费的大量资源,减少并发编程的风险。那我们如何创建线程池呢?
在java中ThreadPoolExecutor这个类就是用来创建线程池的主要武器,我们需要彻底了解。至于类的继承关系如下所示:ThreadPoolExecutor 继承自 AbstractExecutorService 实现了 ExecutorService 继承自 Executor。要了解其中原理,我们需要来看一下threadPoolExecutor的源码,看一下如何构造一个线程池。
我们分析一下,我们看到构造一个线程池我们需要设置7个参数,是不是很懵,别着急,我们来一步步分析这些参数,我们先知道这些参数是什么含义。
首先是corePoolSize(核心线程数)和maximumPoolSize(最大线程数)。
当在execute(Runnable)方法中提交新任务并且少于corePoolSize线程正在运行时,即使其他工作线程处于空闲状态,也会创建一个新线程来处理该请求。 如果有多于corePoolSize但小于maximumPoolSize线程正在运行,则仅当队列已满时才会创建新线程。我们看一下任务处理图。
看到这儿,你是不是有一个问题,队列是啥?这么理解,假设你核心线程数是五个,但是你提交了8个线程,这些线程肯定有3个是没法创建的,那怎么办,这些线程又是你需要的,你不能销毁,我们怎么处理?这个时候队列就派上用场了,相当于你去吃自助,已经爆满了,商家为了赚钱(资源利用最大化)会在门口给你放一排座位,让你等桌,等有客人吃完离开,你就可以进去,等待队列干的就是这么个事。(当然了,这只是为了通俗理解,搞技术的不要弄得这么生活)。好了,大概意思就这么个意思。当然了,队列也是构造线程池需要传入的一个参数。BlockingQueue<Runnable> workQueue就是。当然了这个等待队列还是有三种构造方式的,我们来捋一捋捋一捋。
- 直接提交,即SynchronousQueue
- 无界队列策略,即LinkedBlockingQueue
- 有界队列,使用ArrayBlockingQueue
所谓直接提交就是:首先SynchronousQueue是无界的,也就是说他存数任务的能力是没有限制的,但是由于该Queue本身的特 性,在某次添加元素后必须等待其他线程取走后才能继续添加。
所谓无界队列策略:如果运行的线程等于或多于 corePoolSize,则 Executor 始终首选将请求加入队列,而不添加新的线程。
所谓有界队列:如果超过核心线程数,就先进入等待队列,但是因为等待队列的容量是有限的,等到队列满了以后,有新的任务进来就直接创建线程,达到最大线程数就请求拒绝策略。
好了,我们现在来说一下long keepAliveTime 和 TimeUnit unit这两个参数。我们知道等待队列满了时候会创建除核心线程之外的线程,当然这些线程数小于最大的线程数,但是假如现在任务已经执行完了,那些线程还存在,那是不是会造成资源的一种浪费,但是如果我们等任务一结束就关闭线程,假如一秒以后又有新的任务进来,你就得重新启动线程,这也是对资源的一种浪费。所以考虑到这一点,我们就设置了一个时间,让任务结束后这段时间内线程还存活,过了这段时间就自动销毁,非常合理,当然我们对于这个时间的设置需要按照实际情况考虑。unit这个参数就是单位,有秒,分等选择。
上文中我们提到:达到最大线程数就请求拒绝策略,那拒绝策略是啥?我们查看一下源码:
仔细一看,你以为发现了新宇宙,但其实啥也没看出来,不过我们可以看出我们是可以自定义拒绝策略的(具体怎么定义请自行研究)当然了,我们要人性化,就怕你不会定义,所以jdk已经帮你定义好了四种:
第一种就是拒绝之后直接就抛出异常,然后丢弃。
第二种就比较人性化了,会先提供一个反馈,然后减缓一下新任务的提交速度。
第三种也是直接抛弃,但是不会抛出异常。
第四种就比较复杂:如果执行程序尚未关闭,则位于工作队列头部的任务将被删除,然后重试执行程序(如果再次失败,则重复此过程)这个是啥意思呢,就是我进来了,就把最老的(排在队列最前)的那个任务丢弃。
说了半天这么热闹,那怎么创建线程呢?这个就用到了我们最后一个参数。ThreadFactory threadFactory,线程工厂,其实这是设计模式中工厂模式一个很好的体现,通过线程工厂来创建线程,就降低了我们与线程之间的耦合,不用直接创建线程,很方便。而我们要做的就是实现这个接口:
至于怎么用工厂模式来创建线程,我会在设计模式讲工厂模式那块和大家说一说。
到了这里,看过一些博客的童鞋,会有个疑问,不是有四种创建线程池的方式吗?怎么这里啥也没提到?说了半天也不知道到底怎么创建。
请看源码:
发现了啥?
啥也没发现?发现了这些构造方法只是将这几个参数中的一个或者多个变成了默认的参数,就有了四种不同的构造方法,神奇不神奇,生气不生气哈。源码还是很重要的哈。
好吧,那咱们就按照网上的教程分析一波,网上说我们有四种创建线程池的方式。但是我必须强调,不建议直接这么构造,因为要构造合适自己的,这些只是相当于一些模板,可能不适合自己实际的情况。但是当你不知道怎么构造什么是适合自己的线程池时,这四种可以给你参考。
- FixedThreadPool
- CachedThreadPool
- SingleThreadExecutor
- ScheduledThreadPool