深入理解Java自带的线程池和缓冲队列

前言

线程池是什么

线程池的概念是初始化线程池时在池中创建空闲的线程,一但有工作任务,可直接使用线程池中的线程进行执行工作任务,任务执行完成后又返回线程池中成为空闲线程。使用线程池可以减少线程的创建和销毁,提高性能。

举个例子:我是一个包工头,代表线程池,手底下有若干工人代表线程池中的线程。如果我没接到项目,那么工人就相当于线程池中的空闲线程,一但我接到了项目,我可以立刻让我手下的工人去工作,每个工人同一时间执行只执行一个工作任务,执行完了就去

执行另一个工作任务,知道没有工作任务了,这时工人就可以休息了(原谅我让工人无休止的工作),也就是又变成了线程池中的空闲线程池。

队列是什么

队列作为一个缓冲的工具,当没有足够的线程去处理任务时,可以将任务放进队列中,以队列先进先出的特性来执行工作任务

举个例子,我又是一个包工头,一开始我只接了一个小项目,所以只有三个工作任务,但我手底下有四个工人,那么其中三人各领一个工作任务去执行就好了,剩下一个人就先休息。但突然我又接到了几个大项目,那么有现在有很多工作任务了,但手底下的工人不够啊。

那么我有两个选择:

(1)雇佣更多的工人

(2)把工作任务记录下来,按先来后到的顺序执行

但雇佣更多等工人需要成本啊,对应到计算机就是资源的不足,所以我只能把工作任务先记录下来,这样就成了一个队列了。

为什么要使用线程池

假设我又是一个包工头,我现在手底下没有工人了,但我接到了一个项目,有了工作任务要执行,那我肯定要去找工人了,但招人成本是很高的,工作完成后还要给遣散费,这样算起来好像不值,所以我事先雇佣了固定的几个工人作为我的长期员工,有工作任务就干活,没有就休息,如果工作任务实在太

多,那我也可以再临时雇佣几个工人。一来二去工作效率高了,付出的成本也低了。Java自带的线程池的原理也是如此。

Java自带的线程池

Executor接口是Executor的父接口,基于生产者--消费者模式,提交任务的操作相当于生产者,执行任务的线程则相当于消费者,如果要在程序中实现一个生产者--消费者的设计,那么最简单的方式通常是使用Executor。

ExecutorService接口是对Executor接口的扩展,提供了对生命周期的支持,以及统计信息收集、应用程序管理机制和性能监视等机制。

常用的使用方法是调用Executors中的静态方法来创建一个连接池。

(1)newFixedThreadPool

代码演示:

 1 public class ThreadPoolTest {
 2     public static void main(String[] args){
 3         ExecutorService executor = Executors.newFixedThreadPool(3);
 4         for (int i = 0; i < 4; i++){
 5             Runnable runnable = new Runnable() {
 6                 public void run() {
 7                     CountDownLatch countDownLatch = new CountDownLatch(1); //计数器,用于阻塞线程
 8                     System.out.println(Thread.currentThread().getName() + "正在执行");
 9                     try {
10                         countDownLatch.await();
11                     } catch (InterruptedException e) {
12                         e.printStackTrace();
13                     }
14                 }
15             };
16             executor.execute(runnable);
17         }
18     }
19 }

测试结果:

pool-1-thread-1正在执行
pool-1-thread-3正在执行
pool-1-thread-2正在执行

newFixedThreadPool将创建一个固定长度的线程池,每当提交一个任务时就会创建一个线程,直到达线程池的最大数量,这时线程池的规模不再变化(如果某个线程由于发生了未预期的Exception而结束,那么线程池会补充一个新的线程)。上述代码中最大的线程数是3,但我提交了4个任务,而且每个任务都阻塞住,所以前三个任务占用了线程池所有的线程,那么第四个任务永远也不会执行,因此该线程池配套使用的队列也是无界的。所以在使用该方法创建线程池时要根据实际情况看需要执行的任务是否占用过多时间,会不会影响后面任务的执行。

(2)newCachedThreadPool

测试代码:

 1 public class ThreadPoolTest {
 2     public static void main(String[] args){
 3         ExecutorService executor = Executors.newCachedThreadPool();
 4         for (int i = 0; i < 4; i++){
 5             Runnable runnable = new Runnable() {
 6                 public void run() {
 7                     CountDownLatch countDownLatch = new CountDownLatch(1); //计数器,用于阻塞线程
 8                     System.out.println(Thread.currentThread().getName() + "正在执行");
 9                     try {
10                         countDownLatch.await();
11                     } catch (InterruptedException e) {
12                         e.printStackTrace();
13                     }
14                 }
15             };
16             executor.execute(runnable);
17         }
18     }
19 }

测试结果:

pool-1-thread-1正在执行
pool-1-thread-3正在执行
pool-1-thread-2正在执行
pool-1-thread-4正在执行

newCachedThreadPool将创建一个可缓存的线程池。如果线程池的当前规模超过了处理需求时,那么就会回收部分空闲的线程(根据空闲时间来回收),当需求增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。

(3)newSingleThreadExecutor

测试代码:

 1 public class ThreadPoolTest {
 2     public static void main(String[] args){
 3         ExecutorService executor = Executors.newSingleThreadExecutor();
 4         for (int i = 0; i < 4; i++){
 5             final int index = i;
 6             Runnable runnable = new Runnable() {
 7                 public void run() {
 8                     System.out.println(Thread.currentThread().getName() + "正在执行工作任务--- >" + index);
 9                 }
10             };
11             executor.execute(runnable);
12         }
13     }
14 }

测试结果:

pool-1-thread-1正在执行工作任务--- >0
pool-1-thread-1正在执行工作任务--- >1
pool-1-thread-1正在执行工作任务--- >2
pool-1-thread-1正在执行工作任务--- >3

newSingleThreadExecutor是一个单线程的Executor,它创建单个工作者线程来执行任务,如果这个线程异常结束,会创建另一个线程来代替。newSingleThreadExecutor能确保依照任务在队列中的顺序来串行执行。

(4)newScheduledThreadPool

测试代码:

 1 public class ThreadPoolTest {
 2     public static void main(String[] args){
 3         ScheduledExecutorService executor = Executors.newScheduledThreadPool(3);
 4         for (int i = 0; i < 3; i++){
 5             final int index = i;
 6             Runnable runnable = new Runnable() {
 7                 public void run() {
 8                     System.out.println(Thread.currentThread().getName() + "延时1s后,每5s执行一次工作任务--- >" + index);
 9                 }
10             };
11             executor.scheduleAtFixedRate(runnable,1,5,TimeUnit.SECONDS);
12         }
13     }
14 }

测试结果:

pool-1-thread-1延时1s后,每5s执行一次工作任务--- >0
pool-1-thread-2延时1s后,每5s执行一次工作任务--- >1
pool-1-thread-3延时1s后,每5s执行一次工作任务--- >2
pool-1-thread-1延时1s后,每5s执行一次工作任务--- >0
pool-1-thread-3延时1s后,每5s执行一次工作任务--- >2
pool-1-thread-2延时1s后,每5s执行一次工作任务--- >1

newScheduledThreadPool创建了一个固定长度的线程池,而且以延迟或定时或周期的方式来执行任务,类似于Timer。可应用于重发机制。

以上四种创建线程池的方法其实都是调用以下这个方法,只是参数不一样

corePoolSize  ---------------------> 核心线程数

maximumPoolSize ---------------> 最大线程数

keepAliveTime --------------------> 当线程数大于核心时,此为终止前多余的空闲线程等待新任务的最长时间

unit -----------------------------------> 时间单位

workQueue ------------------------> 用于存储工作工人的队列

threadFactory ---------------------> 创建线程的工厂

handler ------------------------------> 由于超出线程范围和队列容量而使执行被阻塞时所使用的处理程序

常用的几种队列

(1)ArrayBlockingQueue:规定大小的BlockingQueue,其构造必须指定大小。其所含的对象是FIFO顺序排序的。

(2)LinkedBlockingQueue:大小不固定的BlockingQueue,若其构造时指定大小,生成的BlockingQueue有大小限制,不指定大小,其大小有Integer.MAX_VALUE来决定。其所含的对象是FIFO顺序排序的。

(3)PriorityBlockingQueue:类似于LinkedBlockingQueue,但是其所含对象的排序不是FIFO,而是依据对象的自然顺序或者构造函数的Comparator决定。

(4)SynchronizedQueue:特殊的BlockingQueue,对其的操作必须是放和取交替完成。

排队策略(以下排队策略文字来自------->https://www.oschina.net/question/565065_86540

排队有三种通用策略:

直接提交。工作队列的默认选项是 SynchronousQueue,它将任务直接提交给线程而不保持它们。在此,如果不存在可用于立即运行任务的线程,则试图把任务加入队列将失败,因此会构造一个新的线程。此策略可以避免在处理可能具有内部依赖性的请求集时出现锁。直接提交通常要求无界 maximumPoolSizes 以避免拒绝新提交的任务。当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性。

无界队列。使用无界队列(例如,不具有预定义容量的 LinkedBlockingQueue)将导致在所有 corePoolSize 线程都忙时新任务在队列中等待。这样,创建的线程就不会超过 corePoolSize。(因此,maximumPoolSize的值也就无效了。)当每个任务完全独立于其他任务,即任务执行互不影响时,适合于使用无界队列;例如,在 Web页服务器中。这种排队可用于处理瞬态突发请求,当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性。

有界队列。当使用有限的 maximumPoolSizes时,有界队列(如 ArrayBlockingQueue)有助于防止资源耗尽,但是可能较难调整和控制。队列大小和最大池大小可能需要相互折衷:使用大型队列和小型池可以最大限度地降低 CPU 使用率、操作系统资源和上下文切换开销,但是可能导致人工降低吞吐量。如果任务频繁阻塞(例如,如果它们是 I/O边界),则系统可能为超过您许可的更多线程安排时间。使用小型队列通常要求较大的池大小,CPU使用率较高,但是可能遇到不可接受的调度开销,这样也会降低吞吐量。

我们选其中的LinkedBlockingQueue队列来解析

在上述Java自带的创建线程池的方法中,newFixedThreadPool使用的队列就是LinkedBlockingQueue

public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

如果需要执行的工作任务少于核心线程数,那么直接使用线程池的空闲线程执行任务,如果任务不断增加,超过核心线程数,那么任务将被放进队列中,而且是没有限制的,线程池中的线程也不会增加。

其他线程池的工作队列也是根据排队的通用策略来进行工作,看客们可以自己分析。

总结:

没有创建连接池的方式,只有最适合的方式,使用Java自带的方式创建或者自己创建连接池都是可行的,但都要依照自身的业务情况选择合适的方式。

如果你的工作任务的数量在不同时间差距很大,那么如果使用newFixedThreadPool创建固定的线程就不合适,创建少了到时队列里会塞进太多的工作任务导致处理不及时,创建多了会导致工作任务少时有太多的线程处于空闲状态造成资源浪费。

所以还是需要根据实际情况使用适合的创建方式。

posted @ 2019-01-09 15:32  苹果大大个  阅读(26077)  评论(0编辑  收藏  举报