前言

Java中的线程池是一个很重要的概念,它的应用场景十分广泛,可以被广泛的用于高并发的处理场景。J.U.C提供的线程池:ThreadPoolExecutor类,可以帮助我们管理线程并方便地并行执行任务。因此了解并合理使用线程池非常重要。

本文对线程池采用 3W 的策略结合源码进行思考逐层分析,即是什么为什么怎么做。

什么是线程池

线程池的本质是对任务和线程的管理,做到了将任务线程两者解耦。线程池对任务的管理可看作生产者消费者的关系,通过阻塞队列的存与取。阻塞队列缓存待执行的任务,工作线程从阻塞队列中获取任务。线程池对线程的管理,是结合线程池状态,已有线程的状态,核心线程数和最大线程数、阻塞队列状态做出增加、执行任务、回收、复用等操作,体现了享元模式和池化思想。

享元模式:

主要目的是实现对象的共享,运用共享技术有效地支持大量细粒度的对象,避免大量相类似的开销。当系统中对象多的时候可以减少内存的开销,通常与搭配工厂模式使用。

池化思想:

在多种使用对象的策略上,主张让使用的代价最小化。在重新创建对象的代价 远大于更换状态,复用对象的代价的前提下,将可以复用的对象放入池中待复用,以此降低使用的代价。

为什么要用线程池

线程池的优点,也是它为什么被流行使用的原因:

  • 重用线程池中的线程,避免因为线程的创建和销毁带来性能开销。
  • 能有效控制线程池的最大并发数,能提供定时执行以及定间隔循环执行等功能。
  • 线程池还提供了一种方法来约束和管理执行一组任务时消耗的资源(包括线程),避免大量的线程之间因互相抢占系统资源而导致的阻塞现象。
  • 可维护一些基本统计信息,比如已完成任务的数量。

主要的缺点:

  • 线程池的参数不存在完美的配置,高度依赖于开发者的经验,使用不当容易造成线上的危机
  • 线程池执行的情况和任务类型相关性较大,IO密集型和CPU密集型的任务运行起来的情况差异非常大,业界并没有一些成熟的经验策略帮助开发人员参考。

怎么用线程池

先了解线程池的相关重要概念:

Core and maximum pool sizes

核心线程数以及最大线程数,这是构造一个线程池所必需的参数。

不同的搭配会有不同效果的线程池,也是线程池判断在运行任务前是否需创建新线程的重要依据。

ThreadFactory

线程工厂,这是构造一个线程池的参数。

提供线程的创建,如果构建线程池不指定ThreadFactory,则使用默认线程工厂,创建的线程默认进入同一个ThreadGroup和默认线程优先级。

Keep-alive times

存活时间,这是构造一个线程池的参数。

如果线程池当前有超过corePoolSize大小的线程,如果非核心线程的空闲时间超过了keepAliveTime,则被视为可回收的多余线程,被终止

Queuing

任务/阻塞队列,这是构造一个线程池的参数。

不同类型的阻塞队列可以构造出适合不同场景的线程池。最常见的四种线程池就有着不同类型的阻塞队列。

作为任务的缓冲停留区,线程池管理线程的机制核心之一。

生产者消费者模式的体现,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。

  • 在队列为空时,获取元素的线程会等待队列变为非空再尝试获取
  • 当队列满时,存储元素的线程会等待队列可用再尝试存储

Rejected tasks

拒绝任务后的策略,这是构造一个线程池的参数

加入任务时,根据线程池当前状态是否停止销毁、线程数是否以及饱和,判断是否拒绝本次任务的加入。若拒绝任务就会执行拒绝任务后的策略。默认的拒绝后的策略是抛出运行期异常RejectedExecutionException

On-demand construction

需求到达才创建,默认情况下,即使是核心线程最初也只有在新任务到达时才创建和启动,但是可以使用prestartCoreThread。如果使用非空队列构造池,可能需要预启动线程。

Hook methods

钩子方法

可重写的方法,beforeExecute(Runnable)afterExecute(Runnable,Throwable)terminated ,在执行每个任务之前和之后,线程池被完全终止后会被回调。可以用来执行特殊任务:重新初始化ThreadLocal变量、收集统计信息或添加日志条目。

最常见最常用的线程池

Executors类提供的也是最常见的线程池种类,配置,以及它们维护的阻塞队列类型,使用场景如下:

类型 核心线程数 最大线程数 阻塞队列 说明/使用场景
FixedThreadPool 构造时传入 与核心线程数相同 LinkedBlockingQueue 线程数量固定,只有核心线程并且不会被回收,没有超时机制
CachedThreadPool 0 Integer.MAX_VALUE SynchronousQueue 线程数量不固定的线程池,只有非核心的线程,当线程都处于活动状态时,直接创建新线程来处理新任务,否则就利用空闲的线程。处于空闲状态超过60s的线程被回收
ScheduledThreadPool 构造时传入 Integer.MAX_VALUE DelayedWorkQueue 非核心线程在闲置时立刻回收,主要用于执行定时任务和固定周期的重复任务
SingleThreadExecutor 1 1 LinkedBlockingQueue 只有一个核心线程,确保所有任务在同一线程中按顺序执行

分析创建这四个线程池的方法的源码,最后都来到了ThreadPoolExecutor类的ThreadPoolExecutor构造方法,由此可见ThreadPoolExecutor才是真正的线程池。Executors作为线程池工厂,提供的四种线程池是利用不同参数创建的适应不同使用场景的线程池。

//ThreadPoolExecutor.java

/**
* @param corePoolSize 核心线程数
* @param maximumPoolSize 最大线程数
* @param keepAliveTime 非核心线程闲置的超时时长
* @param unit 用于指定 keepAliveTime 参数的时间单位
* @param 任务队列,通过线程池的 execute 方法提交的 Runnable 对象会存储在这个参数中
* @param threadFactory 线程工厂,用于提供新线程
* @param handler 任务队列已满或者是无法成功执行任务时调用
*/
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
//···
}

线程池的简单使用

以手动创建一个核心数为5,最大线程数为7,空闲超时为20s,阻塞队列为数组实现的有界队列的ThreadPoolExecutor为例子:

        ExecutorService executor = new ThreadPoolExecutor(
5, 7, 20L, TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(8)
);
for(int i = 0; i < 9; i++){
final int index = i;
executor.execute(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(String.valueOf(index)+ " " +Thread.currentThread().getName());
}
});
}

手动创建线程池的好处

阿里巴巴Java开发手册中使用强制标注:需通过手动创建 ThreadPoolExecutor 取代使用 Executors 提供的工厂方法。数据量并发量很大或难以把握时,应避免直接使用 Executors 提供的线程池,防止资源被耗尽

以CachedThreadPool为例子,CachedThreadPool将空闲线程销毁前的等待时间设置成了60s,同时阻塞队列类型是SynchronousQueue,不存储元素的队列。 CachedThreadPool 在一定程度上能够应对不断突增的并发任务,但是一旦任务量远远大于处理量,会造成线程数量的激增和资源的消耗,容易引发OOM。

手动创建线程池可以更好规范该线程池的职责,更好地管理这个线程池,让线程池在合适的场景下,可以用来处理适当的任务,而不是一颗随时会被引爆的炸弹。

总结

线程池,基于池化思想,体现了享元模式,可以用来管理线程并方便地并行执行任务的工具。本质上是对任务和线程解耦后进行管理,利用不同的构造参数可以构造出适合不同场景的线程池。优点是降低资源消耗提高响应速度提高线程的可管理性可拓展性良好。缺点是参数不易配置,出错后易造成OOM。

篇幅问题,对线程池的设计和管理机制的分析安排在下一篇文章~

参考资料

Java线程池的了解使用—筑基篇的更多相关文章

  1. Java线程池详解(二)

    一.前言 在总结了线程池的一些原理及实现细节之后,产出了一篇文章:Java线程池详解(一),后面的(一)是在本文出现之后加上的,而本文就成了(二).因为在写完第一篇关于java线程池的文章之后,越发觉 ...

  2. 深入浅出Java线程池:源码篇

    前言 在上一篇文章深入浅出Java线程池:理论篇中,已经介绍了什么是线程池以及基本的使用.(本来写作的思路是使用篇,但经网友建议后,感觉改为理论篇会更加合适).本文则深入线程池的源码,主要是介绍Thr ...

  3. Java线程池详解,看这篇就够了!

    构造一个线程池为什么需要几个参数?如果避免线程池出现OOM?Runnable和Callable的区别是什么?本文将对这些问题一一解答,同时还将给出使用线程池的常见场景和代码片段. 基础知识 Execu ...

  4. java 线程池第一篇 之 ThreadPoolExcutor

    一:什么是线程池? java 线程池是将大量的线程集中管理的类,包括对线程的创建,资源的管理,线程生命周期的管理.当系统中存在大量的异步任务的时候就考虑使用java线程池管理所有的线程.减少系统资源的 ...

  5. Java 线程池框架核心代码分析--转

    原文地址:http://www.codeceo.com/article/java-thread-pool-kernal.html 前言 多线程编程中,为每个任务分配一个线程是不现实的,线程创建的开销和 ...

  6. Java线程池的那些事

    熟悉java多线程的朋友一定十分了解java的线程池,jdk中的核心实现类为java.util.concurrent.ThreadPoolExecutor.大家可能了解到它的原理,甚至看过它的源码:但 ...

  7. Java线程池的原理及几类线程池的介绍

    刚刚研究了一下线程池,如果有不足之处,请大家不吝赐教,大家共同学习.共同交流. 在什么情况下使用线程池? 单个任务处理的时间比较短 将需处理的任务的数量大 使用线程池的好处: 减少在创建和销毁线程上所 ...

  8. Java线程池与java.util.concurrent

    Java(Android)线程池 介绍new Thread的弊端及Java四种线程池的使用,对Android同样适用.本文是基础篇,后面会分享下线程池一些高级功能. 1.new Thread的弊端执行 ...

  9. Java 线程池框架核心代码分析

    前言 多线程编程中,为每个任务分配一个线程是不现实的,线程创建的开销和资源消耗都是很高的.线程池应运而生,成为我们管理线程的利器.Java 通过Executor接口,提供了一种标准的方法将任务的提交过 ...

随机推荐

  1. 阐述Fetch.ai的能源市场优化

    原文链接:https://fetch.ai/explaining-fetch-ais-energy-market-optimization/ 阐述Fetch.ai的能源市场优化 2019年11月4日 ...

  2. html2canvas截图问题,图片跨域导致截图空白

    年前的一个项目,要做一个H5截屏分享的功能,使用的是html2canvas插件,截图功能是实现了,但是跨域的图片死活不出来, 经过几天谷歌百度和不断的尝试,终于找到解决办法了,一共经历了让人心力憔悴的 ...

  3. day76 vue框架入门

    目录 一.vue.js快速入门使用 1 vue.js库的下载 2 vue.js库的使用 3 vue.js的M-V-VM思想 4 显示数据 二.常用指令 1 操作属性 2 事件的绑定 3 样式操作 3. ...

  4. 删除排序数组中的重复项--leetcode算法题

    题目来自于leetcode 题目描述: 给定一个排序数组,你需要在原地删除重复出现的元素,使得每个元素只出现一次,返回移除后数组的新长度. 不要使用额外的数组空间,你必须在原地修改输入数组并在使用 O ...

  5. Unity-内存

    editor 和runtime的内存管理分开的 unity检测不到native内存容量 如c++,lua 一个asset一个ab的问题在于 每个asset都有对应的文件头,并不划算 IL2CPP抛弃了 ...

  6. Kubernetes的10个基本事实!你知道几个?k8s与Docker又有何不同?

    无论您是Kubernetes的新手还是只是想获得更多知识,这篇文章都会帮到您! Kubernetes是一个增长的趋势.近年来,K8s技术经历了从小型开源Google项目到Cloud Native Co ...

  7. ES6标准中的import和export

    在ES6前, 前端使用RequireJS或者seaJS实现模块化, requireJS是基于AMD规范的模块化库,  而像seaJS是基于CMD规范的模块化库,  两者都是为了为了推广前端模块化的工具 ...

  8. [jvm] -- 引用篇

    四种引用及其应用场景 强引用 强引用是平常中使用最多的引用,强引用在程序内存不足(OOM)的时候也不会被回收. 使用场景:啥时候都在使用 软引用 软引用在程序内存不足时,会被回收. 使用场景:创建缓存 ...

  9. consul++ansible+shell批量下发注册node_exporter

    --日期:2020年7月21日 --作者:飞翔的小胖猪  文档功能说明: 文档通过ansible+shell+consul的方式实现批量下发安装Linux操作系统监控的node_exporter软件, ...

  10. LeetCode 85 | 如何从矩阵当中找到数字围成的最大矩形的面积?

    本文始发于个人公众号:TechFlow,原创不易,求个关注 今天是LeetCode专题53篇文章,我们一起来看看LeetCode中的85题,Maximal Rectangle(最大面积矩形). 今天的 ...