1、为什么要使用线程池?

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

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

2、线程池核心类的类关系

  线程池中核心的类和接口大致有以下几个,它们分别是:

  • Executor:它是一个接口,是 Executor 框架的基础,将任务的提交与任务的执行分离开来。
  • ExecutorService:它是一个接口,继承了 Executor,在其上做了一些扩展,如 shutdown()、submit() 等,可以说是真正的线程池接口。
  • AbstractExecutorService:它是一个抽象类,实现了 ExecutorService 接口中的大部分方法。
  • ThreadPoolExecutor:它线程池的核心实现类,用来执行被提交的任务。
  • ScheduledExecutorService:它是一个接口,继承了 ExecutorService 接口,提供了带"周期执行"功能的 ExecutorService。
  • ScheduledThreadPoolExecutor:它是一个实现类,实现 ScheduledExecutorService 接口,可以在给定的延迟后执行任务, 或者定期执行任务。它比 Timer 更灵活,功能更强大。

它们之间类的UML图如下所示:

java 多线程并发大数据处理 java多线程并发编程 线程池_java 多线程并发大数据处理

3、线程池的生命周期状态

  线程池的生命周期状态,一共5种,线程池中使用一个 AtomicInteger 的 ctl 变量表示线程池的状态和数量,高3位表示线程池状态,低29位表示线程数量。

private private static final int TERMINATED =  3 << COUNT_BITS;
private static final int TERMINATED =  3 << COUNT_BITS;
  • RUNNING: 接受新任务并处理排队的任务,一旦线程被创建初始状态就是 RUNNING。
  • SHUTDOWN: 不接受新任务,但处理排队的任务和正在执行的任务。
  • STOP: 不接受新任务,不处理排队的任务,并中断正在进行的任务。
  • TIDYING: 所有任务都已终止,workerCount(线程数)为零,线程状态过渡到 TIDYING 状态将要执行终止方法 terminated()。
  • TERMINATED: 终止方法 terminated() 执行完成

各状态之间转换如下所示:

  • RUNNING -> SHUTDOWN :调用 shutdown() 方法,可能隐含在finalize()中。
  • (RUNNING or SHUTDOWN) -> STOP :调用 shutdownNow() 方法
  • SHUTDOWN -> TIDYING:当队列和线程池都为空时2111
  • STOP -> TIDYING:当线程池为空时
  • TIDYING -> TERMINATED:当 terminated() 方法执行完成

java 多线程并发大数据处理 java多线程并发编程 线程池_并发编程_02

  使用一个变量 ctl 表示线程池的两个变量值,高3位表示线程池状态,低29位表示线程数量。
  为什么要这样设计,不用两个变量,使用一个变量来表示呢?一个变量可以使用一条CAS指令,可以无锁,保证原子性。如果是两个变量的话,就要使用两条CAS指令,不能保证原子性。

4、线程池的工作原理

  1)线程池中一开始是不存在线程的,当一个任务被提交给线程池后,线程池会创建一个新线程来执行任务。如果当前线程池中运行的线程数少于corePoolSize,则创建新线程来执行任务。
  2)如果线程池中运行的线程数达到核心线程数corePoolSize的上限,则将新提交的任务加入BlockingQueue队列当中去。
  3)如果无法将新提交的任务加入BlockingQueue队列中(有界队列,队列已满),则创建新的线程来执行任务,前提是maximumPoolSize大于corePoolSize。
  4)如果线程池中线程数达到maximumPoolSize,任务将被拒绝,执行拒绝策略,并调用RejectedExecutionHandler接口的rejectedExecution()方法。

特别注意:如果线程池的核心线程数被设置为0,那么提交的任务会启用空闲线程来执行。

线程池的工作原理图如下所示:

java 多线程并发大数据处理 java多线程并发编程 线程池_并发编程_03

5、ThreadPoolExecutor 类的构造方法

  ThreadPoolExecutor 类一共有 4 个构造方法,如下图所示:

java 多线程并发大数据处理 java多线程并发编程 线程池_线程池原理_04


  接下来分析一下参数最多的一个构造方法,其他的几个构造方法不再过多讲述。

public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}

此方法一共有7个参数:
  1)corePoolSize:核心线程数
  2)maximumPoolSize:最大线程数
  3)keepAliveTime:maximumPoolSize-corePoolSize 线程数(空闲线程)的存活时间,如果超时,则把线程销毁。
  4)unit:参数 keepAliveTime 的时间单位
  5)workQueue:任务存放的队列
  6)threadFactory:线程工程,主要是给线程取一个自定义的名字
  7)handler:拒绝策略

6、如何合理地配置线程池

  线程池的大小决定着系统的性能,数量过大或者过小线程池都没有办法发挥最优的系统性能。那如何合理地配置线程池?想必这是每个开发人员必须要了解的知识点,想要合理地配置线程池,就必须首先分析任务特性,可以从以下几个方面来分析。

  • 任务性质:CPU密集型、IO密集型和混合型。
  • 任务优先级:高、中、低。
  • 任务执行时间:长、中、短。
  • 任务依赖性:是否依赖其他系统资源,比如数据库连接等等。

定义下文中的几个变量:

  • NCPU:表示处理器(CPU)的核心数目,Java可以通过Runtime.getRuntime().availableProcessors()方法获取。
  • UCPU:表示期望CPU 的利用率(介于 0 和 1 之间)
  • W/C:表示等待时间与计算时间的比率。等待时间与计算时间在 Linux 下使用相关的 vmstat 命令或者 top 命令查看。

CPU密集型任务:
  尽量使用较小的线程池数量,一般为 NCPU+1。因为CPU密集型任务使得CPU使用率很高,若设置过大的线程数,会造成CPU过度切换,反而会降低系统的性能。

OI密集型任务:
  可以使用稍微大的线程池数量,一般为 NCPU*2 。IO密集型任务CPU使用率并不高,因此可以让CPU在等待IO的时候有其他线程去处理别的任务,充分利用CPU时间。在进行I/O操作的时候,是将任务交给DMA(Direct Memory Access)来处理,请求发出后CPU就不管了,在DMA处理完后通过中断通知CPU处理完成了,I/O操作消耗的CPU时间很少。
  对于IO密集型任务的最佳线程数,有个最佳计算公式:线程数 = NCPU * UCPU * (1 + W/C)。

混合型任务:
  可以将任务拆分成IO密集型任务和CPU密集型任务,使用不同的线程池去执行提交的任务。确保拆分完成后两个任务的执行时间没有明显的差距,那么这样的拆分可以给系统带来效率的提升;如果有明显的差距,则没有必要拆分。

  优先级不同的任务可以使用优先级队列 PriorityBlockingQueue 来处理。它可 以让优先级高的任务先执行。
  执行时间不同的任务可以交给不同规模的线程池来处理,或者可以使用优先 级队列,让执行时间短的任务先执行。
  依赖数据库连接池的任务,因为线程提交 SQL 后需要等待数据库返回结果, 等待的时间越长,则 CPU 空闲时间就越长,那么线程数应该设置得越大,这样才能更好地利用 CPU。


线程池的工作原理就分析到这里了,如有分析不到位的地方欢迎读者留言讨论,下一篇文章主要详细讲解线程池 ThreadPoolExecutor 的基本应用,比如如何使用线程池提交任务、关闭线程池及其各参数的用途示例证明等等。