点击上方 好好学java ,选择 星标 公众号

重磅资讯、干货,第一时间送达
今日推荐:2020,搞个 Mac 玩玩!个人原创+1博客:点击前往,查看更多
作者:IamHYN
链接:https://segmentfault.com/a/1190000021866282

一、为什么要手动创建线程池?

我们之所以要手动创建线程池,是因为 JDK 自带的工具类所创建的线程池存在一定的弊端,那究竟存在怎么样的弊端呢?首先来回顾一下 JDK 中线程池框架的继承关系:

Java线程池框架继承结构.png

JDK 线程池框架继承关系图

我们最常用的线程池实现类是ThreadPoolExecutor(红框里的那个),首先我们来看一下它最通用的构造方法:

/*** 各参数含义* corePoolSize    : 线程池中常驻的线程数量。核心线程数,默认情况下核心线程会一直存活,即使处于闲置状态也不会*                   受存活时间 keepAliveTime 的限制,除非将 allowCoreThreadTimeOut 设置为 true。* maximumPoolSize : 线程池所能容纳的最大线程数。超过这个数的线程将被阻塞。当任务队列为没有设置大小的*                           LinkedBlockingQueue时,这个值无效。* keepAliveTime   : 当线程数量多于 corePoolSize 时,空闲线程的存活时长,超过这个时间就会被回收* unit            : keepAliveTime 的时间单位* workQueue       : 存放待处理任务的队列* threadFactory   : 线程工厂* handler         : 拒绝策略,拒绝无法接收添加的任务*/
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler) { ... ... }

使用 JDK 自带的 Executors工具类 (图中蓝色框中的那个,这是独立于线程池继承关系图的工具类,类似于 Collections 和 Arrays) 可以直接创建以下种类的线程池:

  1. 线程数量固定的线程池,此方法返回 ThreadPoolExecutor

public static ExecutorService newFixedThreadPool(int nThreads) {... ...}
  1. 单线程线程池,此方法返回 ThreadPoolExecutor

public static ExecutorService newSingleThreadExecutor() {... ...}
  1. 可缓存线程的线程池,此方法返回 ThreadPoolExecutor

public static ExecutorService newCachedThreadPool() {... ...}
  1. 执行定时任务的线程池,此方法返回 ScheduledThreadPoolExecutor

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {... ...}
  1. 可以拆分执行子任务的线程池,此方法返回 ForkJoinPool

public static ExecutorService newWorkStealingPool() {... ...}

JDK 自带工具类创建的线程池存在的问题

直接使用这些线程池虽然很方便,但是存在两个比较大的问题:

  1. 有的线程池可以无限添加任务或线程,容易导致 OOM;

就拿我们最常用FixedThreadPoolCachedThreadPool来说,前者的详细创建方法如下:

public static ExecutorService newFixedThreadPool(int nThreads) {return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());}

可见其任务队列用的是LinkedBlockingQueue,且没有指定容量,相当于无界队列,这种情况下就可以添加大量的任务,甚至达到Integer.MAX_VALUE的数量级,如果任务大量堆积,可能会导致 OOM。

CachedThreadPool的创建方法如下:

public static ExecutorService newCachedThreadPool() {return new ThreadPoolExecutor(0, Integer.MAX_VALUE,60L, TimeUnit.SECONDS,new SynchronousQueue<Runnable>());}

这个虽然使用了有界队列SynchronousQueue,但是最大线程数设置成了Integer.MAX_VALUE,这就意味着可以创建大量的线程,也有可能导致 OOM。

  1. 还有一个问题就是这些线程池的线程都是使用 JDK 自带的线程工厂 (ThreadFactory)创建的,线程名称都是类似pool-1-thread-1的形式,第一个数字是线程池编号,第二个数字是线程编号,这样很不利于系统异常时排查问题。

如果你安装了“阿里编码规约”的插件,在使用Executors创建线程池时会出现以下警告信息:

Alibaba Java Coding Guidelines.png

阿里编码规约的警告信息

为避免这些问题,我们最好还是手动创建线程池。

二、 如何手动创建线程池

2.1 定制线程数量

首先要说明一点,定制线程池的线程数并不是多么高深的学问,也不是说一旦线程数设定不合理,你的程序就无法运行,而是要尽量避免以下两种极端条件:

  1. 线程数量过大

这会导致过多的线程竞争稀缺的 CPU 和内存资源。CPU 核心的数量和计算能力是有限的,在分配不到 CPU 执行时间的情况下,线程只能处于空闲状态。而在JVM 中,线程本身也是对象,也会占用内存,过多的空闲线程自然会浪费宝贵的内存空间。

  1. 线程数量过小

线程池存在的意义,或者说并发编程的意义就是为了“压榨”计算机的运算能力,说白了就是别让 CPU 闲着。如果线程数量比 CPU 核心数量还小的话,那么必定有 CPU 核心将处于空闲状态,这是极大的浪费。

所以在实际开发中我们需要根据实际的业务场景合理设定线程池的线程数量,那又如何分析业务场景呢?我们的业务场景大致可以分为以下两大类:

  1. CPU (计算)密集型

这种场景需要大量的 CPU 计算,比如加密、计算 hash 等,最佳线程数为 (CPU 核心数 + 1)。比如8核 CPU,可以把线程数设置为 9,这样就足够了,因为在 CPU 密集型的场景中,每个线程都会在比较大的负荷下工作,很少出现空闲的情况,正好每个线程对应一个 CPU 核心,然后不停地工作,这样就实现了最优利用率。多出的一个线程起到了备胎的作用,在其他线程意外中断时顶替上去,确保 CPU 不中断工作。其实也大可不必这么死板,线程数量设置为 CPU 核心数的 1 到 2 倍都是可以接受的。

  1. I/O 密集型

比如读写数据库,读写文件或者网络读写等场景。各种 I/O 设备 (比如磁盘)的速度是远低于 CPU 执行速度的,所以在 I/O 密集型的场景下,线程大部分时间都在等待资源而非 CPU 时间片,这样的话一个 CPU 核心就可以应付很多线程了,也就可以把线程数量设置大一点。线程具体数量的计算方法可以参考 Brain Goetz 的建议:

假设有以下变量:

*   N<sub>threads</sub> = 线程数量
*   N<sub>cpu</sub> = CPU 核心数
*   U<sub>cpu</sub> = 期望的CPU 的使用率 ,因为 CPU 可能还要执行其他任务
*   W = 线程的平均等待资源时间
*   C = 线程平均使用 CPU 的计算时间
*   W / C = 线程等待时间与计算时间的比率

这样为了让 CPU 达到期望的使用率,最优的线程数量计算公式如下:

Nthreads = Ncpu Ucpu ( 1 + W / C )

CPU 核心数可以通过以下方法获取:

int N_CPUS = Runtime.getRuntime().availableProcessors();</code>

当然,除了 CPU,线程数量还会受到很多其他因素的影响,比如内存和数据库连接等,需要具体问题具体分析。

2.2 使用可自定义线程名称的线程工厂

这个就简单多了,可以借助大名鼎鼎的谷歌开源工具库 Guava,首先引入如下依赖:

<!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
<dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId><version>28.2-jre</version>
</dependency>

然后我就可以使用其提供的ThreadFactoryBuilder类来创建线程工厂了,Demo 如下:

public class ThreadPoolDemo {// 线程数public static final int THREAD_POOL_SIZE = 16;public static void main(String[] args) throws InterruptedException {// 使用 ThreadFactoryBuilder 创建自定义线程名称的 ThreadFactoryThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("hyn-demo-pool-%d").build();// 创建线程池,其中任务队列需要结合实际情况设置合理的容量ThreadPoolExecutor executor = new ThreadPoolExecutor(THREAD_POOL_SIZE,THREAD_POOL_SIZE,0L,TimeUnit.MILLISECONDS,new LinkedBlockingQueue<>(1024),namedThreadFactory,new ThreadPoolExecutor.AbortPolicy());// 新建 1000 个任务,每个任务是打印当前线程名称for (int i = 0; i < 1000; i++) {executor.execute(() -> System.out.println(Thread.currentThread().getName()));}// 优雅关闭线程池executor.shutdown();executor.awaitTermination(1000L, TimeUnit.SECONDS);// 任务执行完毕后打印"Done"System.out.println("Done");}
}

控制台打印结果如下:

... ...
hyn-demo-pool-2
hyn-demo-pool-6
hyn-demo-pool-13
hyn-demo-pool-12
hyn-demo-pool-15
Done

可见这样的线程名称相比pool-1-thread-1更有辨识度,可以为不同用途的线程池设定不同的名称,便于系统出故障时排查问题。

三、总结

本文为大家介绍了手动创建线程池的详细方法,不过这些都是理论性的内容,而多线程编程是非常注重实践的一门学问,在实际生产环境中要综合考虑各种因素并不断尝试,才能实现最佳实践。

手把手教你手动创建线程池相关推荐

  1. 手动创建线程池 效果会更好_创建更好的,可访问的焦点效果

    手动创建线程池 效果会更好 Most browsers has their own default, outline style for the :focus psuedo-class. 大多数浏览器 ...

  2. 阿里代码规约:手动创建线程池,效果会更好哦

    项目中创建多线程时,使用常见的三种线程池创建方式,单一.可变.定长都有一定问题,原因是FixedThreadPool和SingleThreadExecutor底层都是用LinkedBlockingQu ...

  3. Java_Java多线程_Java线程池核心参数 与 手动创建线程池

    参考文章: 1.浅谈线程池ThreadPoolExecutor核心参数 https://www.cnblogs.com/stupid-chan/p/9991307.html 2.Java线程池 Thr ...

  4. 从原理到实现丨手把手教你写一个线程池丨源码分析丨线程池内部组成及优化

    人人都能学会的线程池 手写完整版 1. 线程池的使用场景 2. 线程池的内部组成 3. 线程池优化 [项目实战]从原理到实现丨手把手教你写一个线程池丨源码分析丨线程池内部组成及优化 内容包括:C/C+ ...

  5. 手动创建线程池,效果会更好哦

    今天在回顾线程池的创建时,使用Executors创建线程池报错了,出现了以下问题:手动创建线程池,效果会更好哦. 查阅了阿里巴巴Java开发手册 回顾一下,通过ThreadPoolExecutor来创 ...

  6. 手把手教你看懂线程池源码!

    编辑:业余草 来源:https://www.xttblog.com/?p=4978 世上无难事,只怕有心人! 今天我们来说说线程池! 线程池简介 使用线程池,一般会使用JDK提供的几种封装类型,即:n ...

  7. 为什么阿里Java规约禁止使用Java内置Executors创建线程池?

    IDEA导入阿里规约插件,当你这样写代码时,插件就会自动监测出来,并给你红线提醒. 告诉你手动创建线程池,效果会更好. 在探秘原因之前我们要先了解一下线程池 ThreadPoolExecutor 都有 ...

  8. 面试突击32:为什么创建线程池一定要用ThreadPoolExecutor?

    Python微信订餐小程序课程视频 https://edu.csdn.net/course/detail/36074 Python实战量化交易理财系统 https://edu.csdn.net/cou ...

  9. 为什么创建线程池一定要用ThreadPoolExecutor?

    作者 | 磊哥 来源 | Java面试真题解析(ID:aimianshi666) 转载请联系授权(微信ID:GG_Stone) 在 Java 语言中,并发编程都是依靠线程池完成的,而线程池的创建方式又 ...

最新文章

  1. 快的打车联合创始人兼技术副总裁闻诚:CTO要有“334”能力
  2. xml配置linux启动脚本,linux中利用Shell脚本实现自动安装部署weblogic服务
  3. tensorflows十五 再探Momentum和Nesterov's accelerated gradient descent 利用自动控制PID概念引入误差微分控制超参数改进NAGD,速度快波动小
  4. Codewars-Snail(Javascript实现螺旋数组)
  5. 只能输入正整数 已经常用的正则表达式
  6. 5.5使用Cucumber来测试
  7. Orcad CIS本地库添加器件
  8. android 拨号隐藏号码,技巧:手机隐藏代码大集合 知道五个以上是大神
  9. 放量跌与缩量跌的区别是什么?
  10. java1.8 Lambda拉姆达表达式深入探究
  11. 【OpenPCDet】Kitti数据集下训练PointPillars并评估可视化
  12. 【FPGA】串口以命令控制温度采集
  13. 收货地址参数校验:收货人、邮编、地址、手机、固话等
  14. 《富爸爸,穷爸爸》读后感——怎么实现财务自由
  15. 蓝桥杯C/C++省赛:振兴中华
  16. JavaScript中立即执行函数实例详解 转载 作者:李牧羊
  17. cmake 版本 arm_nRF52832开发丶开发环境搭建(ubuntu 18.04+arm-none-eabi-gcc)
  18. 产品设计及运营的难言之隐。
  19. JAVA语言知识点总结
  20. 基于小波变换的单幅彩色图像去雾增强

热门文章

  1. WebBrowser页面与WinForm交互技巧
  2. 明早1点去青岛,可能要两天不能写博客了
  3. InterlockedIncrement函数详解
  4. STM32F103CB 芯片FLASH DOWNLOAD编程地址范围设置相关问题记录
  5. Qt中定时器使用的两种方法
  6. 文本挖掘预处理流程总结(1)— 中文
  7. [hypervisor]-AArch64 (hypervisor)Virtualization学习笔记
  8. 2021-07-15
  9. Prison Architect 64位逃脱模式穿墙代码
  10. 「安全技术」针对常见混淆技术的反制措施