在我们web项目中经常会碰到请求量非常大的需求。这时我们会开启多个线程来处理请求。但是线程的创建和销毁的开销是非常大的。有的时候甚至比线程实际运行时间还长。除此以外如果我们为每个请求都创建线程。在请求量非常大的情况下。会在jvm创建大量的线程。不仅占用了很大的资源。也会出现内存用完或切换过度的问题。线程池为线程生命周期开销问题和资源不足问题提供了解决方案。通过对多个任务重用线程,线程创建的开销被分摊到了多个任务上。其好处是,因为在请求到达时线程已经存在,所以无意中也消除了线程创建所带来的延迟。这样,就可以立即为请求服务,使应用程序响应更快。

  • newCachedThreadPool
    创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
    特点:
    1.工作线程的创建数量几乎没有限制(其实也有限制的,数目为Interger. MAX_VALUE), 这样可灵活的往线程池中添加线程。
    2.如果长时间没有往线程池中提交任务,即如果工作线程空闲了指定的时间(默认为1分钟),则该工作线程将自动终止。终止后,如果你又提交了新的任务,则线程池重新创建一个工作线程。
    3.在使用CachedThreadPool时,一定要注意控制任务的数量,否则,由于大量线程同时运行,很有会造成系统瘫痪。
    适用场景:执行很多短期异步的小程序或者负载较轻的服务器
    示例:
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
        for (int i=0;i<100;i++){
            final int index = i;
            cachedThreadPool.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println("线程名称:"+Thread.currentThread().getName()+"当前顺序:"+index+"-时间:"+new Date().getTime());
                }
            });
        }
//控制台输出
线程名称:pool-1-thread-13当前顺序:61-时间:1548306768622
线程名称:pool-1-thread-10当前顺序:98-时间:1548306768624
线程名称:pool-1-thread-6当前顺序:96-时间:1548306768624
线程名称:pool-1-thread-28当前顺序:89-时间:1548306768623
线程名称:pool-1-thread-5当前顺序:95-时间:1548306768623
线程名称:pool-1-thread-27当前顺序:81-时间:1548306768623
线程名称:pool-1-thread-16当前顺序:93-时间:1548306768623
线程名称:pool-1-thread-15当前顺序:92-时间:1548306768623
  1. newFixedThreadPool
    创建一个指定工作线程数量的线程池。每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中。
    特点:
    1.工作线程最大数量固定
    2.线程运行结束也不会回收
    3.如果请求无空闲线程,会将请求存入队列进行等待。
    适用场景:执行长期的任务,性能好很多
    示例:
ExecutorService executorService = Executors.newFixedThreadPool(5);
        for (int i=0;i<100;i++){
            final int index = i;
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println("线程名称:"+Thread.currentThread().getName()+"当前顺序:"+index+"-时间:"+new Date().getTime());
                }
            });
        }
//控制台打印
程名称:pool-1-thread-1当前顺序:97-时间:1548309162720
线程名称:pool-1-thread-1当前顺序:98-时间:1548309162720
线程名称:pool-1-thread-1当前顺序:99-时间:1548309162720
线程名称:pool-1-thread-2当前顺序:29-时间:1548309162714
线程名称:pool-1-thread-4当前顺序:28-时间:1548309162714
线程名称:pool-1-thread-5当前顺序:53-时间:1548309162717
线程名称:pool-1-thread-3当前顺序:52-时间:1548309162717
  1. newSingleThreadExecutor
    创建一个单线程化的Executor,即只创建唯一的工作者线程来执行任务
    特点:
    1.保证任务执行顺序
    适用场景:一个任务一个任务执行的场景
    示例:
ExecutorService executorService = Executors.newSingleThreadExecutor();
        for (int i=0;i<20;i++){
            final int index = i;
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println("线程名称:"+Thread.currentThread().getName()+"当前顺序:"+index+"-时间:"+new Date().getTime());
                }
            });
        }
//控制台输出
线程名称:pool-1-thread-1当前顺序:13-时间:1548309652741
线程名称:pool-1-thread-1当前顺序:14-时间:1548309652741
线程名称:pool-1-thread-1当前顺序:15-时间:1548309652741
线程名称:pool-1-thread-1当前顺序:16-时间:1548309652741
线程名称:pool-1-thread-1当前顺序:17-时间:1548309652741
线程名称:pool-1-thread-1当前顺序:18-时间:1548309652741
线程名称:pool-1-thread-1当前顺序:19-时间:1548309652741
  1. NewScheduledThreadPool
    创建一个定长的线程池,而且支持定时的以及周期性的任务执行
    特点:
    1.工作最大线程数量固定
    2.可以定时执行任务
    3.超出线程数量则存入队列
    适用场景:周期性执行任务的场景
    示例:
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(5);
        for (int i=0;i<20;i++){
            final int index = i;
            final Date startDate = new Date();
            executorService.schedule(new Runnable() {
                @Override
                public void run() {
                    System.out.println("线程名称:"+Thread.currentThread().getName()+"当前顺序:"+index+"延时:"+(new Date().getTime()-startDate.getTime())/1000);
                }
            },2, TimeUnit.SECONDS);
        }
//控制台打印
线程名称:pool-1-thread-2当前顺序:16延时:2
线程名称:pool-1-thread-4当前顺序:12延时:2
线程名称:pool-1-thread-3当前顺序:11延时:2
线程名称:pool-1-thread-4当前顺序:19延时:2
线程名称:pool-1-thread-2当前顺序:18延时:2
线程名称:pool-1-thread-5当前顺序:17延时:2
线程名称:pool-1-thread-1当前顺序:15延时:2
  1. 阿里关于线程池创建的规范
    在阿里规范中有这一条:
    线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
    主要的原因是:
    1.FixedThreadPool 和 SingleThreadPool: 允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
    2.CachedThreadPool 和 ScheduledThreadPool: 允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM
    使用ThreadPoolExecutor 创建线程池示例:

ThreadPoolExecutor 构造方法

/**
     * @param corePoolSize 核心线程数量
     * @param maximumPoolSize 最大线程数量
     * @param keepAliveTime 线程空闲时间
     * @param unit 空闲时间单位
     * @param workQueue 等待队列
     */
    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
    }

Executors底层也是使用ThreadPoolExecutor创建上面所用的4种线程池。我们可以查看其创建方式

public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }
    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }
    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }
    public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, TimeUnit.NANOSECONDS,
              new DelayedWorkQueue());
    }

我们可以通过选择合适的等待队列,合适的最大线程数量和空闲时间来解决阿里规范所考虑的问题。
下面贴一些队列的简单介绍:

  1. ArrayBlockingQueue
    基于数组的阻塞队列实现,在ArrayBlockingQueue内部,维护了一个定长数组,以便缓存队列中的数据对象,这是一个常用的阻塞队列,除了一个定长数组外,ArrayBlockingQueue内部还保存着两个整形变量,分别标识着队列的头部和尾部在数组中的位置。
  2. LinkedBlockingQueue
    基于链表的阻塞队列,同ArrayListBlockingQueue类似,其内部也维持着一个数据缓冲队列(该队列由一个链表构成),当生产者往队列中放入一个数据时,队列会从生产者手中获取数据,并缓存在队列内部,而生产者立即返回;只有当队列缓冲区达到最大值缓存容量时(LinkedBlockingQueue可以通过构造函数指定该值),才会阻塞生产者队列,直到消费者从队列中消费掉一份数据,生产者线程会被唤醒,反之对于消费者这端的处理也基于同样的原理。而LinkedBlockingQueue之所以能够高效的处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。
  3. DelayQueue
    DelayQueue中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素。DelayQueue是一个没有大小限制的队列,因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞。
  4. PriorityBlockingQueue
    基于优先级的阻塞队列(优先级的判断通过构造函数传入的Compator对象来决定),但需要注意的是PriorityBlockingQueue并不会阻塞数据生产者,而只会在没有可消费的数据时,阻塞数据的消费者。因此使用的时候要特别注意,生产者生产数据的速度绝对不能快于消费者消费数据的速度,否则时间一长,会最终耗尽所有的可用堆内存空间。在实现PriorityBlockingQueue时,内部控制线程同步的锁采用的是公平锁。
  5. SynchronousQueue
    一种无缓冲的等待队列,类似于无中介的直接交易,有点像原始社会中的生产者和消费者,生产者拿着产品去集市销售给产品的最终消费者,而消费者必须亲自去集市找到所要商品的直接生产者,如果一方没有找到合适的目标,那么对不起,大家都在集市等待。相对于有缓冲的BlockingQueue来说,少了一个中间经销商的环节(缓冲区),如果有经销商,生产者直接把产品批发给经销商,而无需在意经销商最终会将这些产品卖给那些消费者,由于经销商可以库存一部分商品,因此相对于直接交易模式,总体来说采用中间经销商的模式会吞吐量高一些(可以批量买卖);但另一方面,又因为经销商的引入,使得产品从生产者到消费者中间增加了额外的交易环节,单个产品的及时响应性能可能会降低。