线程复用:线程池笔记

《实战Java高并发程序设计》学习笔记(三) : JAVA线程池笔记。
主要记录了一些Java线程池的基础知识。

线程复用:线程池

线程池总概

什么是线程池?

接触过JDBC的人,一定听说过数据库连接池(比如,c3p0、Druid等)。其实在我的理解中,两者是差不多的。不过线程池中放的是线程而已。
线程是一种轻量级工具,但其创建与关闭都需要花费一定的时间。而且大量的线程会抢占内存资源。盲目的大量资源会对系统造成极大的压力。
线程池,中有一定数量的活跃线程。创建线程变成了从线程池中获得空闲线程;关闭线程变成了向线程池归还线程。

JDK对于线程池的支持

Java通过Executors提供五种线程池,分别为:

  • newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
  • newFixedThreadPool创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
  • newScheduledThreadPool创建一个定长线程池,支持定时及周期性任务执行。
  • newSingleThreadExecutor创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
  • newSingleThreadScheduledExcutor创建单线程化的线程池,支持定时及周期性任务执行。

线程池的使用

首先是简单使用,这个没有什么特殊之处。
只需记得newFixedThreadPool创建的是定长的线程池,可控制线程最大并发数,超出的线程会在队列中等待。
newCachedThreadPool创建的线程池为无限大,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程

定时任务

newScheduledThreadPool支持定时及周期性任务执行,查看了其源码,主要有以下三种方法:

  • schedule():在给定时间,对任务进行调度;
  • scheduleAtFixedRate() 和 scheduleWithFixedDelay():对任务进行周期性调度,但两者有所区别。
scheduleAtFixedRate() 和 scheduleWithFixedDelay() 的区别
  1. 两种调度的区别:
    • FixedRate 方式:以上一个任务开始执行时间为起点,在之后的延迟时间后,调用下一次任务。
    • FixedDelay 方式:上一个任务结束后,再经过延迟时间进行任务调度。
  2. 若任务执行时间超过调度时间,
    • FixedRate 方式:若调度时间过短,那么任务会在上一个任务结束后立刻调用(不会出现任务堆叠的现场)。
    • FixedDelay 方式:会严格按照任务间隔时间 = 调度时间 + 任务执行时间

如果任务遇到异常,那么后续的所有子任务都会停止调度。因此必须保证,异常被及时处理,为周期性任务的稳定调度提供条件。

关于线程池的记录

拒绝策略

创建线程池的核心类 ThreadPoolExecutor 有一个参数指定了拒绝策略。拒绝策略,是系统超负荷运行时的补救措施,通常是由于压力太大而引起的,也就是线程池中的线程已经用完了且等待队列已经排满了。
JDK 提供了四种拒绝策略

  • AbortPolicy 策略:直接抛出异常,阻止系统正常工作。
  • CallerRnsPolicy 策略:只要线程池未关闭,将直接在调用者线程中运行被丢弃的任务。这种做法不会真的丢弃任务,但是任务提交线程的性能将急剧下降
  • DiscardOldestPolicy 策略:丢弃最老的一个请求,也就是即将被执行的任务(处于等待队列的队头),并尝试再次提交当前任务。
  • DiscardPolicy 策略:直接丢掉无法处理的任务。
  • 自定义策略:自己扩展 RejectedExecutionHandler 接口。

线程扩展

ThreadPoolExecutor是一个可扩展的线程池,有beforeExecute()afterExecute()terminated()能够对线程进行控制。

1
2
3
protected void beforeExecute(Thread t, Runnable r) { }
protected void afterExecute(Runnable r, Throwable t) { }
protected void terminated() { }

这是三个protected的空方法,摆明了可以让子类扩展。

  • 在执行任务的线程中将调用beforeExecuteafterExecute等方法,在这些方法中还可以添加日志、计时、监视或者统计信息收集的功能。
  • 无论是正常运行,还是抛出异常,都会调用afterExecute。但是,如果抛出Eorror,将不会调用该方法;或者beforeExecute抛出一个RuntimeException,则任务将不被执行,即该方法也不会被调用。
  • 关于terminated,在线程池完成关闭时(就是在所有任务已经完成且所有工作者线程已经关闭),用来释放Executor在生命周期里分配的各种资源,此外还能执行信息通知、日志记录等功能。

补充

  1. 使用线程池被”吃”掉了异常堆栈信息
    在使用线程池提交线程时,可能会发生异常堆栈信息被”吃”掉的现象,而解决方法:

    • 放弃submit(),改用execute()。
    • 获取submit()方法返回类的get()方法。

      1
      2
      Future future = pools.submit(new Thread());
      future.get();
    • 扩展 ThreadPoolExecutor 线程池,让其在调度任务前,先保存提交任务线程的堆栈消息(就是重写线程池线程的调用方法)。

  2. 自定义线程:ThreadFactory
    这个接口只有一个方法 newThread(Runnable r),主要是由线程池调用新建线程。

  3. 优化线程池线程数量
    在《Java Concurrency in Practice》书中给了一个估算线程池大小的经验公式:

    1
    2
    3
    4
    5
    Ncpu = CPU数量
    Ucpu = 目标CPU的使用率,0 <= Ucpu <= 1
    W/C = 等待时间与计算时间的比率
    所以,最优的线程池大小为:
    Nthreads = Ncpu * Ucpu * ( 1 + W/C )

同时,在Java中,可以通过Runtime.getRuntime().availableProcessors获取可用的CPU数量。

参考资料

  • 《实战Java高并发程序设计》(葛一鸣 郭超 著)
评论