Java线程池详解-笔记

什么是线程池

线程池就是创建若干个可执行的线程放入一个池(容器)中,有任务需要处理时,会提交到线程池中的任务队列,处理完之后线程并不会被销毁,而是仍然在线程池中等待下一个任务。

为什么使用线程池

  • 因为在 Java 中的线程直接映射到操作系统,需要调用操作系统内核的 API,操作系统要为线程分配一系列的资源,成本很高,所以线程是一个重量级的对象,应该尽量避免频繁创建和销毁。使用线程池就能很好地避免频繁创建和销毁。
  • 活动的线程也消耗系统资源,如果线程的创建数量没有限制,当大量的客户连接服务器的时候,就会创建出大量的工作线程,他们会消耗大量的内存空间,导致系统的内存空间不足,使用线程池可以较好的控制线程的数量。

使用线程池的好处

  • 降低资源消耗
  • 加快响应速度
  • 提高线程的可管理性
  • 合理利用CPU和内存

线程池试用场景:1、服务器处理大量请求时,使用线程池减少线程创建和销毁的开销;2、当需要5个以上线程时也可以使用线程池管理线程

一个线程池示例

  1. public static void main(String[] args)
  2. {
  3. // 使用线程池来创建线程
  4. ExecutorService threadPoolExecutor = new ThreadPoolExecutor(
  5. // 核心线程数为 :5
  6. 5,
  7. // 最大线程数 :10
  8. 10,
  9. // 空闲等待等待时间 :1L
  10. 1L
  11. // 等待时间的单位 :秒
  12. TimeUnit.SECONDS,
  13. // 任务队列为 ArrayBlockingQueue,且容量为 100
  14. new ArrayBlockingQueue<>(100),
  15. // 饱和策略为 AbortPolicy
  16. new ThreadPoolExecutor.AbortPolicy()
  17. );
  18. int count = 15;
  19. for (int i = 0; i < count; i++)
  20. {
  21. threadPoolExecutor.execute(() -> System.out.println("线程"+ Thread.currentThread().getName +"执行"));
  22. }
  23. threadPoolExecutor.shutdown();
  24. }

线程池(ThreadPoolExecutor)的核心参数详解

  1. new ThreadPoolExecutor(
  2. int corePoolSize,
  3. int maximumPoolSize,
  4. long keepAliveTime,
  5. TimeUnit unit,
  6. BlockingQueue<Runnable> workQueue,
  7. ThreadFactory threadFactory,
  8. RejectedExecutionHandler handler)
参数名 类型 含义
corePoolSize int 核心线程数
maxPoolSize int 最大线程数
keepAliveTime long 空闲线程存活时间
unit TimeUnit 存活时间单位
workQueue BlockingQueue 任务队列
threadFactory ThreadFactory 创建线程的线程工厂
Handler RejectExecutionHandler 无法处理任务的拒绝策略

corePoolSize(核心线程数):

  • 线程池在完成初始化后,默认情况下线程池中是没有任何线程的,线程池会等待任务的带来再去创建新线程执行任务。
  • 核心线程被创建后会一直存活,即使没有任务需要执行。
  • 当线程数小于核心线程数时,即使有线程空闲,线程池也会优先创建新线程处理。
  • 设置allowCoreThreadTimeout=true(默认false)时,核心线程会超时关闭。

maxPoolSize(最大线程数):

  • 线程池所能创建的最大线程数量
  • 当线程数大于等于corePoolSize,且任务队列已满时,线程池会创建新线程来处理任务。
  • 当线程数等于maxPoolSize,且任务队列已满时,线程池会根据拒绝策略来拒绝处理任务。

keepAliveTime(空闲存活时间):

  • 当线程数大于核心线程数时,空闲线程的最大存活时间,当线程空闲时间达到keepAliveTime时,线程会退出,直到线程数量等于corePoolSize。
  • 如果allowCoreThreadTimeout=true,则会直到线程数量=0。

unit(时间单位):

  • keepAliveTime 参数的时间单位,包括 TimeUnit.SECONDSTimeUnit.MINUTESTimeUnit.HOURSTimeUnit.DAYS 等等。

workQueue(任务队列):

任务队列,用来储存等待执行任务的队列。常用的任务队列有:

  • ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列
  • LinkedBlockingQueue :一个链表结构组成的有界阻塞队列,未指明容量时,默认为Integer.MAX_VALUE。
  • SynchronousQueue :一个不存储元素的阻塞队列,最大值Integer.MAX_VALUE.

threadFactory(线程工厂):

用来创建线程的工厂,一般默认即可。但是在阿里巴巴开发规范中,强制要求为线程和线程池取一个有意义的名字。

Java线程池 - 图1

可以使用Guava库的ThreadFactoryBuilder类创建一个线程工厂,并设置线程池名称后传入ThreadPoolExecutor构造函数中。

  1. ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
  2. .setNameFormat("service-pool-%d").build();

handler (拒绝策略):

当前同时运行的线程数量达到最大线程数量并且队列也已经被放满时,处理新任务时的策略。一般有以下几种拒绝策略:

  • AbortPolicy:抛出 RejectedExecutionException 来拒绝新任务的处理,是 Spring 中使用的默认拒绝策略。
  • CallerRunsPolicy:线程调用运行该任务的 execute 所在的线程去执行(run) 被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。此策略提供简单的反馈控制机制,能够减缓新任务的提交速度,但可能造成延迟。若应用程序可以承受此延迟且不能丢弃任何一个任务请求,可以选择这个策略。
  • DiscardPolicy:不处理新任务,直接丢弃掉
  • DiscardOldestPolicy:将丢弃最早的未处理的任务请求。

手动创建与自动创建线程池

手动创建线程池

使用ThreadPoolExecutor方法的构造函数进行线程池创建。

  1. public class ThreadPoolExecutorDemo
  2. {
  3. private static final int CORE_POOL_SIZE = 5;
  4. private static final int MAX_POOL_SIZE = 10;
  5. private static final int QUEUE_CAPACITY = 100;
  6. private static final Long KEEP_ALIVE_TIME = 1L;
  7. public static void main(String[] args)
  8. {
  9. // 最好为每个自定义的线程和线程池命名 Guava库的ThreadFactoryBuilder方法
  10. ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
  11. .setNameFormat("service-pool-%d").build();
  12. // 使用线程池来创建线程
  13. ExecutorService threadPoolExecutor = new ThreadPoolExecutor(
  14. // 核心线程数为 :5
  15. CORE_POOL_SIZE,
  16. // 最大线程数 :10
  17. MAX_POOL_SIZE,
  18. // 空闲等待等待时间 :1L
  19. KEEP_ALIVE_TIME,
  20. // 等待时间的单位 :秒
  21. TimeUnit.SECONDS,
  22. // 任务队列为 ArrayBlockingQueue,且容量为 100
  23. new ArrayBlockingQueue<>(QUEUE_CAPACITY),
  24. // 线程工厂
  25. namedThreadFactory,
  26. // 饱和策略为 AbortPolicy
  27. new ThreadPoolExecutor.AbortPolicy()
  28. );
  29. int count = 15;
  30. for (int i = 0; i < count; i++)
  31. {
  32. // 创建WorkerThread对象,该对象需要实现Runnable接口
  33. Runnable worker = new ThreadDemo();
  34. // 通过线程池执行Runnable
  35. try
  36. {
  37. threadPoolExecutor.execute(worker);
  38. } catch (Exception e)
  39. {
  40. // 当使用AbortPolicy策略时,线程数量(队列满了)不够时将抛出异常。
  41. System.out.println("服务器繁忙!请稍后再试!");
  42. }
  43. }
  44. // 使用完后终止线程池
  45. threadPoolExecutor.shutdown();
  46. /*
  47. shutdown() 执行后停止接受新任务,会把队列的任务执行完毕。
  48. shtdownNow() 也是停止接受新任务,但会中断所有的任务,将线程池状态变为 stop。
  49. */
  50. // (!pool.awaitTermination(1, TimeUnit.SECONDS)) 没秒检查线程任务是否执行完毕
  51. while (!threadPoolExecutor.isTerminated())
  52. {
  53. System.out.print("");
  54. }
  55. System.out.println("全部线程已终止");
  56. }
  57. }
  58. class ThreadDemo implements Runnable
  59. {
  60. @Override
  61. public void run()
  62. {
  63. System.out.println(Thread.currentThread().getName() + " 开始时间 : " + new Date());
  64. processCommand();
  65. System.out.println(Thread.currentThread().getName() + " 结束时间 : " + new Date());
  66. }
  67. private void processCommand()
  68. {
  69. try
  70. {
  71. // 模拟业务处理
  72. Thread.sleep(1000);
  73. } catch (InterruptedException e)
  74. {
  75. e.printStackTrace();
  76. }
  77. }
  78. }

运行结果

Java线程池 - 图2

从运行结果可以看到,当核心线程数为 5 时,即使总共要运行的线程有 15 个,每次也最多只会同时执行 5 个任务,剩下的任务则会被放入等待队列,当有核心线程空闲后执行(当任务队列满了时,才会继续创建线程,并且线程不会大于maxPoolSize)。

自动创建线程池

使用Executors工具类进行创建。

常用自动创建的线程池有四种

  • newFixedThreadPool():固定线程数量的线程池(corePoolSize=maxPoolSize),使用的是LinkedBlockingQueue无界队列,源码如下:
    1. return new ThreadPoolExecutor(
    2. nThreads, nThreads,
    3. 0L, TimeUnit.MILLISECONDS,
    4. new LinkedBlockingQueue<>(),
    5. threadFactory);


FixThreadPool 使用的是无界队列 LinkedBlockingQueue(队列容量为 Integer.MAX_VALUE),而它会给线程池带来如下影响 :

  • 当线程池中的线程数达到 corePoolSize 后,新任务将在无界队列中等待,因此线程池中的线程数不会超过 corePoolSize

  • 由于使用的是一个无界队列,所以 maximumPoolSize 将是一个无效参数,因为不可能存在任务队列满的情况,所以 FixedThreadPool 的 corePoolSizemaximumPoolSize 被设置为同一个值,且 keepAliveTime 将是一个无效参数;

  • 运行中的 FixedThreadPool(指未执行 shutdown()shutdownNow() 的)不会拒绝任务,因此在任务较多的时候可能会导致 OOM。

  • newCacheThreadPool():CachedThreadPool 是一个会根据需要创建新线程的线程池,但会在先前构建的线程可用时重用它,源码如下:
    1. return new ThreadPoolExecutor(
    2. 0, Integer.MAX_VALUE,
    3. 60L, TimeUnit.SECONDS,
    4. new SynchronousQueue<>(),
    5. threadFactory);


corePoolSize 被设置为 0,maximumPoolSize 被设置为 Integer.MAX.VALUE,也就是无界的。虽然是无界,但由于该线程池还存在一个销毁机制,即如果一个线程 60 秒内未被使用过,则该线程就会被销毁,这样就节省了很多资源。
但是,如果主线程提交任务的速度高于 maximunPool 中线程处理任务的速度,CachedThreadPool 将会源源不断地创建新的线程,从而依然可能导致 CPU 耗尽或内存溢出。

  • newSingleThreadPool():创建只有一个线程的线程池,源码如下:
    1. return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor(
    2. 1, 1,
    3. 0L, TimeUnit.MILLISECONDS,
    4. new LinkedBlockingQueue<>(),
    5. threadFactory));


除了池中只有一个线程外,其他和 FixThreadPool 是基本一致的。

  • newScheduledThreadPool():线程池支持定时以及周期性执行任务,核心方法为scheduleAtFixedRate()、schedule()和scheduleWithFixedDelay(),源码如下:
    1. super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
    2. new ScheduledThreadPoolExecutor.DelayedWorkQueue());

自动创建线程池的弊端

  1. Executors 返回线程池对象的弊端如下:
  2. * FixedThreadPool SingleThreadExecutor
  3. * 允许请求的队列长度为 Integer.MAX_VALUE,可能会堆积大量请求,从而导致 OOM
  4. * CachedThreadPool ScheduledThreadPool
  5. * 允许创建的线程数量为 Integer.MAX_VALUE,可能会创建大量线程,从而导致 OOM

正确创建线程池的方式应该是根据不同的业务场景和环境,自己设置线程池参数。

线程池线程数量设置指南

  • CPU密集型(加密、计算hash等):最佳线程数为CPU核心数的1-2倍左右。

  • 耗时IO型(读写数据库、文件、网络读写等):最佳线程数一般会大于cpu核心数很多倍,以JVM线程监控显示繁忙情况为依据,保证线程空闲可以衔接上,参考Brain Goetz推荐的计算方法∶

Java线程池 - 图3%0A#card=math&code=%E7%BA%BF%E7%A8%8B%E6%95%B0%3DCPU%E6%A0%B8%E5%BF%83%E6%95%B0%2A%281%20%2B%20%E5%B9%B3%E5%9D%87%E7%AD%89%E5%BE%85%E6%97%B6%E9%97%B4%20%2F%20%E5%B9%B3%E5%9D%87%E5%B7%A5%E4%BD%9C%E6%97%B6%E9%97%B4%29%0A)

线程池的关闭

线程池关闭的主要方法:

  • shutdown():不会直接关闭,在所有存在的任务执行完毕后再关闭线程池。但是执行该方法后不能再向线程池中添加任务,否则会报出异常。
  • isShutdown():判断是否进入停止状态,返回true表示进入停止状态,但线程池不一定已经关闭的。
  • isTerminated():当该方法返回true时,说明线程池已经停止。
  • awaitTermination(long timeout, TimeUnit unit):检测在一段时间内线程池是否已经停止,如果在等待时间内被打断会抛出InterruptException。
  • shutdownNow():直接关闭线程池,并且中断正在执行的任务线程和丢弃队列中的任务。该方法有一个List类型的返回值,这个list中存放了队列中被丢弃的任务。可以将list中的任务继续放入新的线程池执行,或者保存起来之后执行。

线程池的一些相关类

Java中线程池相关类的关系图如下:

Java线程池 - 图4

Executor是线程池的顶级接口,接口中只有一个方法void execute(Runnable command)。

ExecutorService继承了Executor接口,并且额外定义了shutdown()、shutdownNow()、isTerminated()等方法。

AbstracExecutorService实现了ExecutorService接口,并实现了其中的方法。

ThreadPoolSize类继承了AbstracExecutorService类,并新增了线程池的各种属性和相关方法。

Executors是线程池的相关工具类。

线程池状态

跟线程一样,线程池也有属于自己的几种状态,分别是:

  • RUNNING:接受新任务并处理排队任务
  • SHUTDOWN:不接受新任务,但处理 排队任务(调用shutdown方法后)
  • STOP:不接受新任务,也不处理排队任务,并中断正在进行的任务(调用shutdownNow方法时)
  • TIDYING:所有任务都已终止,wokerCount(工作线程)为0时,线程会转换到TIDYING状态,并将执行terminate()钩子方法
  • TERMINATED:terminate()运行完成
  1. SHUTDOWN -> TIDYING
  2. When both queue and pool are empty
  3. STOP -> TIDYING
  4. When pool is empty