Java线程池详解

22 篇文章 0 订阅

背景

Java 中线程的创建以及上下文切换是比消耗性能的,因此引入了偏向锁、轻量级锁等优化技术,目的就是减少用户态和核心态之间的切换频率。但是在这些优化基础之上,创建和销毁线程非常损耗性能,那有没有可能复用一些已经被创建好的线程呢?答案是肯定的,那就是线程池。

另外,线程的创建需要开辟虚拟机栈、本地方法栈、程序计数器等线程私有的内存空间,在线程销毁时需要回收这些系统资源,频繁地创建销毁线程会浪费大量资源,而通过复用已有线程可以更好地管理和协调线程的工作。

线程池主要解决两个问题:

  • 当执行大量异步任务时线程池能够提供很好的性能。
  • 线程池提供了一种资源限制和管理的手段,比如可以限制线程的个数,动态新增线程等。

线程池体系

用一张图来表示线程池体系如下:
在这里插入图片描述解释说明:

  • Executor 是线程池最顶层的接口,在 Executor 中只有一个 execute方法,用于执行任务。至于线程的创建、调度等细节由子类实现。

  • ExecutorService 继承并拓展了 Executor,在 ExecutorService
    内部提供了更全面的任务提交机制以及线程池关闭方法。

  • ThreadPoolExecutor 是 ExecutorService
    的默认实现,所谓的线程池机制也大多封装在此类当中,因此它是本文分析的重点。

  • ScheduledExecutorService 继承自 ExecutorService,增加了定时任务相关方法。

  • ScheduledThreadPoolExecutor 继承自 ThreadPoolExecutor,并实现了ScheduledExecutorService 接口。

  • ForkJoinPool 是一种支持任务分解的线程池,一般要配合可分解任务接口 ForkJoinTask 来使用。

创建线程池

为了开发者可以更方便地使用线程池,JDK 中给我们提供了一个线程池的工厂类—Executors。在 Executors 中定义了多个静态方法,用来创建不同配置的线程池。常见有以下几种。

newSingleThreadExecutor

创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按先进先出的顺序执行。

执行上述代码结果如下,可以看出所有的 task 始终是在同一个线程中被执行的。
在这里插入图片描述
newCachedThreadPool

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

执行效果如下:
在这里插入图片描述从上面日志中可以看出,缓存线程池会创建新的线程来执行任务。但是如果将代码修改一下,在提交任务之前休眠 1 秒钟,如下:
在这里插入图片描述再次执行则打印日志同 SingleThreadPool 一模一样,原因是提交的任务只需要 500 毫秒即可执行完毕,休眠 1 秒导致在新的任务提交之前,线程 “pool-1-thread-1” 已经处于空闲状态,可以被复用执行任务。

newFixedThreadPool

创建一个固定数目的、可重用的线程池。
在这里插入图片描述上述代码创建了一个固定数量 3 的线程池,因此虽然向线程池提交了 10 个任务,但是这 10 个任务只会被 3 个线程分配执行,执行效果如下:
在这里插入图片描述
newScheduledThreadPool

创建一个定时线程池,支持定时及周期性任务执行。
在这里插入图片描述上面代码创建了一个线程数量为 2 的定时任务线程池,通过 scheduleAtFixedRate 方法,指定每隔 500 毫秒执行一次任务,并且在 5 秒钟之后通过 shutdown 方法关闭定时任务。执行效果如下:
在这里插入图片描述上面这几种就是常用到的线程池使用方式

线程池工作原理分析

线程池结构
在这里插入图片描述从上图中可以大体看出,在线程池内部主要包含以下几个部分:

worker 集合:保存所有的核心线程和非核心线程,其本质是一个HashSet。

等待任务队列:当核心线程的个数达到 corePoolSize 时,新提交的任务会被先保存在等待队列中,其本质是一个阻塞队列 BlockingQueue。
在这里插入图片描述ctl:是一个 AtomicInteger 类型,二进制高 3 位用来标识线程池的状态,低 29 位用来记录池中线程的数量。

获取线程池状态、工作线程数量、修改 ctl 的方法分别如下:
在这里插入图片描述线程池主要有以下几种运行状态:
在这里插入图片描述
参数分析

线程池的构造器如下:
在这里插入图片描述构造参数说明。

  • corePoolSize:表示核心线程数量。

  • maximumPoolSize:表示线程池最大能够容纳同时执行的线程数,必须大于或等于 1。如果和 corePoolSize相等即是固定大小线程池。

  • keepAliveTime:表示线程池中的非核心线程空闲时间,当空闲时间达到此值时,线程会被销毁直到剩下 corePoolSize 个线程。

  • unit:用来指定 keepAliveTime 的时间单位,有 MILLISECONDS、SECONDS、MINUTES、HOURS 等。

  • workQueue:等待队列,BlockingQueue 类型。当请求任务数大于 corePoolSize 时,任务将被缓存在此BlockingQueue 中。

  • threadFactory:线程工厂,线程池中使用它来创建线程,如果传入的是 null,则使用默认工厂类
    DefaultThreadFactory。

  • handler:执行拒绝策略的对象。当 workQueue 满了之后并且活动线程数大于 maximumPoolSize 的时候,线程池通过该策略处理请求。

  • 注意:当 ThreadPoolExecutor 的 allowCoreThreadTimeOut 设置为 true 时,核心线程超时后也会被销毁。

流程解析

当我们调用 execute 或者 submit,将一个任务提交给线程池,线程池收到这个任务请求后,有以下几种处理情况:

  • 1.当前线程池中运行的线程数量还没有达到 corePoolSize 大小时,线程池会创建一个新线程执行提交的任务,无论之前创建的线程是否处于空闲状态。
  • 2.当前线程池中运行的线程数量已经达到 corePoolSize 大小时,线程池会把任务加入到等待队列中,直到某一个线程空闲了,线程池会根据我们设置的等待队列规则,从队列中取出一个新的任务执行。
  • 3.如果线程数大于 corePoolSize 数量但是还没有达到最大线程数 maximumPoolSize,并且等待队列已满,则线程池会创建新的线程来执行任务。
  • 4.最后如果提交的任务,无法被核心线程直接执行,又无法加入等待队列,又无法创建“非核心线程”直接执行,线程池将根据拒绝处理器定义的策略处理这个任务。比如在 ThreadPoolExecutor 中,如果你没有为线程池设置 RejectedExecutionHandler。这时线程池会抛出 RejectedExecutionException 异常,即线程池拒绝接受这个任务。

程序会报异常 RejectedExecutionException,拒绝策略是线程池的一种保护机制,目的就是当这种无节制的线程资源申请发生时,拒绝新的任务保护线程池。默认拒绝策略会直接报异常,但是 JDK 中一共提供了 4 种保护策略,如下:
在这里插入图片描述实际上拒绝策略都是实现自接口 RejectedExecutionException,开发者也可以通过实现此接口,定制自己的拒绝策略。

为何禁止使用 Executors

现在再回头看一下为何在阿里 Java 开发手册中严禁使用 Executors 工具类来创建线程池。尤其是 newFixedThreadPool 和 newCachedThreadPool 这两个方法。

比如如下使用 newFixedThreadPool 方法创建线程的案例:
在这里插入图片描述上述代码创建了一个固定数量为 2 的线程池,并通过 for 循环向线程池中提交 100 万个任务。
通过 java -Xms4m -Xmx4m FixedThreadPoolOOM 执行上述代码:
在这里插入图片描述可以发现当任务添加到 7 万多个时,程序发生 OOM。这是为什么呢? 看一下newSingleThreadExecutor 和 newFixedThreadPool() 的具体实现,如下:
在这里插入图片描述可以看到传入的是一个无界的阻塞队列,理论上可以无限添加任务到线程池。当核心线程执行时间很长(比如 sleep10s),则新提交的任务还在不断地插入到阻塞队列中,最终造成
OOM。

再看下 newCachedThreadPool 会有什么问题。
在这里插入图片描述上述代码同样会报 OOM,只是错误的 log 信息有点区别:无法创建新的线程。
在这里插入图片描述看一下 newCachedThreadPool 的实现:
在这里插入图片描述可以看到,缓存线程池的最大线程数为 Integer 最大值。当核心线程耗时很久,线程池会尝试创建新的线程来执行提交的任务,当内存不足时就会报无法创建线程的错误。

以上两种情况如果发生在生产环境将会是致命的,从阿里手册中严禁使用 Executors 的态度上也能看出,阿里也是经历过血淋淋的教训。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值