Java并发编程的艺术(十二)——线程池

433 阅读5分钟

使用线程池的好处

  • 降低资源的损耗。重复利用创建好的线程,减少了线程创建和销毁的开销。
  • 提高响应速度。当任务来临时,创建好的线程能立马处理新来的任务,不需要等待线程创建完毕后再执行任务。
  • 方便线程的管理。线程是稀缺资源,过多的创建线程可能会降低系统的稳定性,线程池能控制线程创建的数量,并对其进行监控、管理、调优,从而提高系统稳定性。

线程池的处理流程

  • 1.新任务来临时,若线程池的线程数小于基本线程数,则创建新的线程处理新来的任务(此时即便线程池有空闲的线程,也要创建新的线程);反之,则执行下一步。
  • 2.当新任务来临,而线程池的线程数大于或等于基本线程数时,新任务会被加入的阻塞队列中等待处理。阻塞队列满了之后,执行下一步。
  • 3.当队列满,且新任务来临时,则创建新的线程来执行任务。若线程池里的线程已达到最大线程数(即意味着无法再创建线程),执行下一步。
  • 4.通过饱和策略来处理任务。

线程池的使用

线程池的创建

new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, timeUnit, runnableTaskQueue, handler);
  • ●corePoolSize:基本线程数,代表线程池的基本大小。当线程池里的工作线程数小于基本线程数时,总是会创建新的线程来处理新来的任务。如果调用线程池的prestartAllCoreThreads(),线程池会提前创建好所有的基本线程。
  • ●maximumPoolSize:最大线程数,代表线程池允许创建的最大线程数目。当任务队列已满,且已创建的线程数小于最大线程数,则线程池会创建新的线程执行任务。需要注意的是,对于无界队列,该值无效。
  • ●keepAliveTime:线程池中除基本线程外,其余空闲线程的保活时间。超过这个时间后,空闲线程将会被终止。
  • ●timeUnit:线程保活时间的单位,可选的单位有天(DAYS)、小时(HOURS)、分钟(MINUTES)、毫秒(MILLSECONDS)、微秒(MICROSECONDS)、纳秒(NANOSECONDS)。
  • ●runnableTaskQueue:任务队列,当线程池里的基本线程都在执行任务时,新来的任务将被添加到任务队列里等待执行。该队列有以下几种选择:
    • 1.ArrayBlockingQueue:基于数组的有界阻塞队列,FIFO。
    • 2.LinkedBlockingQueue:基于链表的无界阻塞队列,FIFO,吞吐量通常要高于 ArrayBlockingQueue,静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
    • 3.SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
    • 4.PriorityBlockingQueue:一个具有优先级的无线阻塞队列。 ●handler:饱和策略,当队列和线程池都满了,新提交的任务通过饱和策略进行处理。饱和策略有以下4种:
      • ①.AbortPolicy:默认的策略,作用是直接抛出异常。
      • ②.CallerRunsPolicy:只用调用者所在线程来运行任务。
      • ③.DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
      • ④.DiscardPolicy:不处理,丢弃掉。

向线程池提交任务

可以使用两个方法向线程池提交任务,分别是execute()和submit()方法。

  • ●execute():用于提交不需要返回值的任务,无法判断任务是否被线程池执行成功。
  • ●submit():用于提交需要返回值的任务,返回值是有个Future类型的对象,通过这个对象可以判断任务是否执行成功,并且通过这个Future类型对象的get()方法可以阻塞当前线程直至任务执行成功,该方法会有返回值。

关闭线程池

可以通过调用线程池的shutdown()或者shutdownNow()方法来关闭线程池。他们的原理都是通过遍历线程池中的线程,然后逐个调用线程的interrupt()方法来中断线程。但这两个方法存在着差别:

  • shutdown():该方法中断未执行任务的线程,不中断正在执行任务的线程。
  • shutdownNow():该方法中断所有正在执行或已暂停的任务线程。

合理配置线程池

任务的性质可分为:CPU密集型任务、IO密集型任务、混合型任务。线程池的配置需要考虑任务特性。


  • ●CPU密集型任务:应分配尽可能小的线程池,如线程池大小配置为CPU核心数+1,如此可避免频繁的线程上下文切换造成的开销。
  • ●IO密集型任务:应分配尽可能大的线程池,如线程池大小配置为CPU核心数*2,因为IO密集型任务对CPU的使用较少,创建较多的线程可提高对CPU的利用率。
  • ●混合型任务:可以将任务拆分人CPU密集型任务和IO密集型任务,然后分别用两个线程池去处理,只有分完之后的两个任务的执行时间相差不大,那么执行起来比串行执行要高效。当两个任务的执行时间相差较大时,执行快的一方要等待执行慢的一方,在效率上并没有多大提升。

ThreadPoolExecutor运行机制

当ThreadPoolExecutor执行execute()方法增加新任务时,运行机制如下:

  • 1.如果线程池里运行的线程少于corePoolSize,即使有空闲线程,也会创建新的线程执行任务。
  • 2.如果线程池里运行的线程大于或等于corePoolSize,则把新来的任务加入队列。
  • 3.如果队列已满且运行的线程数小于maximumPoolSize,则创建新的线程执行任务。
  • 4.当队列已满且运行线程数达到最大值maximumPoolSize时,执行饱和策略。

  • 注意:创建新线程需要获取全局锁(该操作开销较大),ThreadPoolExecutor采取上述设计思路,是为了尽可能避免全局锁的获取。一般情况下,当线程池里运行的线程数大于或等于corePoolSize后,任务多被加入队列中等待执行,而不是直接创建线程处理任务,故避免了获取全局锁。