概述

Java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池。在开发过程中,合理地使用线程池能够带来3个好处。

  1. 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  2. 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
  3. 提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。

Executor框架

Executor框架的最顶层实现是ThreadPoolExecutor类,Executors工厂类中提供的newScheduledThreadPool、newFixedThreadPool、newCachedThreadPool方法其实也只是ThreadPoolExecutor的构造函数参数不同而已。通过传入不同的参数,就可以构造出适用于不同应用场景下的线程池。


常用参数

  • corePoolSize:线程池的核心线程数
  • maximumPoolSize:能容纳的最大线程数
  • keepAliveTime:空闲线程存活时间
  • unit:存活的时间单位
  • workQueue:存放提交但未执行任务的队列
  • threadFactory:创建线程的工厂类
  • handler:等待队列满后的拒绝策略


当提交任务数大于 corePoolSize 的时候,会优先将任务放到 workQueue 阻塞队列中。当阻塞队列饱和后,会扩充线程池中线程数,直到达到maximumPoolSize 最大线程数配置。此时,再多余的任务,则会触发线程池的拒绝策略了。即, 当提交的任务数大于(workQueue.size() + maximumPoolSize ),就会触发线程池的拒绝策略 。


拒绝策略

CallerRunsPolicy : 当触发拒绝策略,只要线程池没有关闭的话,则使用调用线程直接运行任务。一般并发比较小,性能要求不高,不允许失败。但是,由于调用者自己运行任务,如果任务提交速度过快,可能导致程序阻塞,性能效率上必然的损失较大。(阻塞,一直等到有线程才会处理,不丢弃

AbortPolicy : 丢弃任务,并抛出拒绝执行 RejectedExecutionException 异常信息。线程池默认的拒绝策略。必须处理好抛出的异常,否则会打断当前的执行流程,影响后续的任务执行。(丢弃任务+抛异常

DiscardPolicy : 直接丢弃,其他啥都没有。(直接丢弃任务,啥都不做

DiscardOldestPolicy : 当触发拒绝策略,只要线程池没有关闭的话,丢弃阻塞队列 workQueue 中最老的一个任务,并将新任务加入。(丢弃最老的一个任务,腾出位置给新任务

线程池四种创建方式

Java通过Executors(jdk1.5并发包)提供四种线程池,分别为:

  • newCachedThreadPool(常用)
  • newFixedThreadPool(常用)
  • newScheduledThreadPool
  • newSingleThreadExecutor

newCachedThreadPool

作用:创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

特点

  • 线程池为无限大
  • 当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程。

场景

适用于创建一个可无限扩大的线程池,服务器负载压力较轻,执行时间较短,任务多的场景

// 无限大小线程池 JVM自动回收
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();

for (int i = 0; i < 10; i++) {
    final int temp = i;
    newCachedThreadPool.execute(new Runnable() {
        @Override
        public void run() {
            try {
                Thread.sleep(100);
            } catch (Exception e) {
                // TODO: handle exception
            }
            System.out.println(Thread.currentThread().getName() + ",i:" + temp);
        }
    });
}

newFixedThreadPool

创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。定长线程池的大小最好根据系统资源进行设置。如:具体还要根据任务特性来区分

ExecutorService newFixedThreadPool =  Executors.newFixedThreadPool(5);

for (int i = 0; i < 10; i++) {
    final int temp = i;
    newFixedThreadPool.execute(new Runnable() {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getId() + ",i:" + temp);
        }
    });
}

newScheduledThreadPool

创建一个定长线程池,支持定时及周期性任务执行。

ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(5);

for (int i = 0; i < 10; i++) {
    final int temp = i;
    newScheduledThreadPool.schedule(new Runnable() {
        public void run() {
            System.out.println("i:" + temp);
        }
    }, 3, TimeUnit.SECONDS); //表示延迟3秒执行
}

newSingleThreadExecutor

创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor();

for (int i = 0; i < 10; i++) {
    final int index = i;
    newSingleThreadExecutor.execute(new Runnable() {
        @Override
        public void run() {
            System.out.println("index:" + index);
            try {
                Thread.sleep(200);
            } catch (Exception e) {
                // TODO: handle exception
            }
        }
    });
}

线程池原理剖析

提交一个任务到线程池中,线程池的处理流程如下:

  1. 判断核心线程:判断线程池里的核心线程是否都在执行任务,如果不是(核心线程空闲或者还有核心线程没有被创建)则创建一个新的工作线程来执行任务。如果核心线程都在执行任务,则进入下个流程。
  2. 判断队列:线程池判断工作队列是否已满,如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程。
  3. 判断其他线程:判断线程池里的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。
  4. 使用饱和策略处理

合理配置线程池

要想合理的配置线程池,就必须首先分析任务特性,可以从以下几个角度来进行分析:

  • 任务的性质:CPU密集型任务(computer-bound),IO密集型任务(I/O-bound),混合型任务。

Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数)

  • CPU密集型任务:尽可能少的线程数量,如配置Ncpu+1个线程的线程池。
  • IO密集型任务:需要等待IO操作,线程并不是一直在执行任务,则配置尽可能多的线程,如2*Ncpu
  • 混合型的任务:如果可以拆分,则将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐率要高于串行执行的吞吐率,如果这两个任务执行时间相差太大,则没必要进行分解。可以通过优先级不同的任务可以使用优先级队列PriorityBlockingQueue来处理。
  • 任务的优先级:高,中,低。
  • 优先级高的任务先得到执行,需要注意的是如果一直有优先级高的任务提交到队列里,那么优先级低的任务可能永远不能执行。
  • 任务的执行时间:长,中,短。
  • 执行时间不同的任务可以交给不同规模的线程池来处理,或者也可以使用优先级队列,让执行时间短的任务先执行
  • 任务的依赖性:是否依赖其他系统资源,如数据库连接。
  • 依赖数据库连接池的任务,因为线程提交SQL后需要等待数据库返回结果,如果等待的时间越长CPU空闲时间就越长,那么线程数应该设置越大,这样才能更好的利用CPU