一、介绍
首先我们来看一下Executors工具类中创建线程池的几个方法。
ExecutorService newFixedThreadPool(int nThreads):固定大小线程池。
可以看到,corePoolSize和maximumPoolSize的大小是一样的(实际上,后面会介绍,如果使用无界queue的话maximumPoolSize参数是没有意义的),keepAliveTime和unit的设值表明什么呢?就是该实现不想keep alive!最后的BlockingQueue选择了LinkedBlockingQueue,该queue有一个特点,他是无界的。
ExecutorService newSingleThreadExecutor():单线程。可以看到,与fixedThreadPool很像,只不过fixedThreadPool中的入参直接退化为1。
ExecutorService newCachedThreadPool():无界线程池,可以进行自动线程回收。
这个实现就有意思了。首先是无界的线程池,所以我们可以发现maximumPoolSize为big big。
其次BlockingQueue的选择上使用SynchronousQueue。这个任务阻塞队列是直接提交方式。如何理解呢?当主线程提交任务到线程池的时候,
由于此时没有空闲线程等在SynchronousQueue队列的出口取任务,那么在底层被提交的任务执行offer(e)的操作会返回false,
所以ThreadPoolExecutor会创建一个线程来承接当前这个被提交的任务。
以上是一些简单的分析,下面请继续。
二、分析
先从BlockingQueue<Runnable> workQueue这个入参开始说起。在JDK中,其实已经说得很清楚了,一共有三种类型的queue。以下为引用:(我会稍微修改一下,并用红色突出显示)
所有 BlockingQueue
都可用于传输和保持提交的任务,除了SynchronousQueue。因为SynchronousQueue不能保持提交的任务。
可以使用此队列与池大小进行交互:
- 如果运行的线程少于 corePoolSize,则 Executor 始终首选添加新的线程,而不进行排队。(什么意思?如果当前运行的线程小于corePoolSize,则任务根本不会添加到queue中,而是直接抄家伙(thread)开始运行)
- 如果运行的线程等于或多于 corePoolSize,则 Executor 始终首选将请求加入队列,而不添加新的线程。
- 如果无法将请求加入队列,则创建新的线程,除非创建此线程超出 maximumPoolSize,在这种情况下,任务将被拒绝。
三、案例分析
new ThreadPoolExecutor(2, 3,60L, TimeUnit.SECONDS,new SynchronousQueue<Runnable>());
当核心线程已经有2个正在运行.LinkedBlockingQueue
如果运行的线程少于 corePoolSize,则 Executor 始终首选添加新的线程,而不进行排队。那么当任务继续增加,会发生什么呢?如果运行的线程等于或多于 corePoolSize,则 Executor 始终首选将请求加入队列,而不添加新的线程。OK,此时任务变加入队列之中了,那什么时候才会添加新线程呢?如果无法将请求加入队列,则创建新的线程,除非创建此线程超出 maximumPoolSize,在这种情况下,任务将被拒绝。
ArrayBlockingQueue。
这个是最为复杂的使用,所以JDK不推荐使用也有些道理。与上面的相比,最大的特点便是可以防止资源耗尽的情况发生。
四、BlockingQueue向队列尾部添加元素的三种方法区别
添加元素的方法有三个:add,put,offer。下面我们来看一下这三个方法的区别:add方法
简单来说就是:add方法在添加元素到队列尾部的时候,若没有超过队列长度的限制就返回true,否则就抛出IllegalStateException。
put方法
简单来说就是:若向队列尾部添加元素的时候发现队列已经满了会发生阻塞一直等待空间,以加入元素。
offer方法
简单来说就是:offer方法在添加元素到队列尾部的时候,如果发现队列已满无法添加的话,会直接返回false。
五、BlockingQueue移除队列头部元素的三种方法区别
remove
简单来说就是:remove方法从队列头部移除元素的时候,若队列为空,抛出NoSuchElementException异常。
take
简单来说就是:take方法从队列头部移除元素的时候,若队列为空,发生阻塞,等待有元素。
poll
简单来说就是:poll方法从队列头部移除元素的时候,若队列为空,返回null。
六、总结
- ThreadPoolExecutor的使用还是很有技巧的。
- 使用无界queue可能会耗尽系统资源。
- 使用有界queue可能不能很好的满足性能,需要调节线程数和queue大小
- 线程数自然也有开销,所以需要根据不同应用进行调节。
- 数量大,但是执行时间很短
- 数量小,但是执行时间较长
- 数量又大执行时间又长
- 除了以上特点外,任务间还有些内在关系