线程池的7种创建方式,强烈推荐你用它...

作者 | 王磊

来源 | Java中文社群(ID:javacn666)

转载请联系授权(微信ID:GG_Stone)

根据摩尔定律所说:集成电路上可容纳的晶体管数量每 18 个月翻一番,因此 CPU 上的晶体管数量会越来越多。

但随着时间的推移,集成电路上可容纳的晶体管数量已趋向饱和,摩尔定律也渐渐失效,因此多核 CPU 逐渐变为主流,与之相对应的多线程编程也开始变得普及和流行起来,这当然也是很久之前的事了,对于现在而言多线程编程已经成为程序员必备的职业技能了,那接下来我们就来盘一盘“线程池”这个多线程编程中最重要的话题。

什么是线程池?

线程池(ThreadPool)是一种基于池化思想管理和使用线程的机制。它是将多个线程预先存储在一个“池子”内,当有任务出现时可以避免重新创建和销毁线程所带来性能开销,只需要从“池子”内取出相应的线程执行对应的任务即可。

池化思想在计算机的应用也比较广泛,比如以下这些:

  • 内存池(Memory Pooling):预先申请内存,提升申请内存速度,减少内存碎片。

  • 连接池(Connection Pooling):预先申请数据库连接,提升申请连接的速度,降低系统的开销。

  • 实例池(Object Pooling):循环使用对象,减少资源在初始化和释放时的昂贵损耗。

线程池的优势主要体现在以下 4 点:

  1. 降低资源消耗:通过池化技术重复利用已创建的线程,降低线程创建和销毁造成的损耗。

  2. 提高响应速度:任务到达时,无需等待线程创建即可立即执行。

  3. 提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。

  4. 提供更多更强大的功能:线程池具备可拓展性,允许开发人员向其中增加更多的功能。比如延时定时线程池ScheduledThreadPoolExecutor,就允许任务延期执行或定期执行。

同时阿里巴巴在其《Java开发手册》中也强制规定:线程资源必须通过线程池提供,不允许在应用中自行显式创建线程

说明:线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。

知道了什么是线程池以及为什要用线程池之后,我们再来看怎么用线程池。

线程池使用

线程池的创建方法总共有 7 种,但总体来说可分为 2 类:

  • 一类是通过 ThreadPoolExecutor 创建的线程池;

  • 另一个类是通过 Executors 创建的线程池。


线程池的创建方式总共包含以下 7 种(其中 6 种是通过 Executors 创建的,1 种是通过 ThreadPoolExecutor 创建的):

  1. Executors.newFixedThreadPool:创建一个固定大小的线程池,可控制并发的线程数,超出的线程会在队列中等待;

  2. Executors.newCachedThreadPool:创建一个可缓存的线程池,若线程数超过处理所需,缓存一段时间后会回收,若线程数不够,则新建线程;

  3. Executors.newSingleThreadExecutor:创建单个线程数的线程池,它可以保证先进先出的执行顺序;

  4. Executors.newScheduledThreadPool:创建一个可以执行延迟任务的线程池;

  5. Executors.newSingleThreadScheduledExecutor:创建一个单线程的可以执行延迟任务的线程池;

  6. Executors.newWorkStealingPool:创建一个抢占式执行的线程池(任务执行顺序不确定)【JDK 1.8 添加】。

  7. ThreadPoolExecutor:最原始的创建线程池的方式,它包含了 7 个参数可供设置,后面会详细讲。

单线程池的意义从以上代码可以看出 newSingleThreadExecutor 和 newSingleThreadScheduledExecutor 创建的都是单线程池,那么单线程池的意义是什么呢?答:虽然是单线程池,但提供了工作队列,生命周期管理,工作线程维护等功能。

那接下来我们来看每种线程池创建的具体使用。

1.FixedThreadPool

创建一个固定大小的线程池,可控制并发的线程数,超出的线程会在队列中等待。

使用示例如下:

public static void fixedThreadPool() {// 创建 2 个数据级的线程池ExecutorService threadPool = Executors.newFixedThreadPool(2);// 创建任务Runnable runnable = new Runnable() {@Overridepublic void run() {System.out.println("任务被执行,线程:" + Thread.currentThread().getName());}};// 线程池执行任务(一次添加 4 个任务)// 执行任务的方法有两种:submit 和 executethreadPool.submit(runnable);  // 执行方式 1:submitthreadPool.execute(runnable); // 执行方式 2:executethreadPool.execute(runnable);threadPool.execute(runnable);
}

执行结果如下:

如果觉得以上方法比较繁琐,还用更简单的使用方法,如下代码所示:

public static void fixedThreadPool() {// 创建线程池ExecutorService threadPool = Executors.newFixedThreadPool(2);// 执行任务threadPool.execute(() -> {System.out.println("任务被执行,线程:" + Thread.currentThread().getName());});
}

2.CachedThreadPool

创建一个可缓存的线程池,若线程数超过处理所需,缓存一段时间后会回收,若线程数不够,则新建线程。

使用示例如下:

public static void cachedThreadPool() {// 创建线程池ExecutorService threadPool = Executors.newCachedThreadPool();// 执行任务for (int i = 0; i < 10; i++) {threadPool.execute(() -> {System.out.println("任务被执行,线程:" + Thread.currentThread().getName());try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {}});}
}

执行结果如下:从上述结果可以看出,线程池创建了 10 个线程来执行相应的任务。

3.SingleThreadExecutor

创建单个线程数的线程池,它可以保证先进先出的执行顺序。

使用示例如下:

public static void singleThreadExecutor() {// 创建线程池ExecutorService threadPool = Executors.newSingleThreadExecutor();// 执行任务for (int i = 0; i < 10; i++) {final int index = i;threadPool.execute(() -> {System.out.println(index + ":任务被执行");try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {}});}
}

执行结果如下:

4.ScheduledThreadPool

创建一个可以执行延迟任务的线程池。

使用示例如下:

public static void scheduledThreadPool() {// 创建线程池ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(5);// 添加定时执行任务(1s 后执行)System.out.println("添加任务,时间:" + new Date());threadPool.schedule(() -> {System.out.println("任务被执行,时间:" + new Date());try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {}}, 1, TimeUnit.SECONDS);
}

执行结果如下:从上述结果可以看出,任务在 1 秒之后被执行了,符合我们的预期。

5.SingleThreadScheduledExecutor

创建一个单线程的可以执行延迟任务的线程池。

使用示例如下:

public static void SingleThreadScheduledExecutor() {// 创建线程池ScheduledExecutorService threadPool = Executors.newSingleThreadScheduledExecutor();// 添加定时执行任务(2s 后执行)System.out.println("添加任务,时间:" + new Date());threadPool.schedule(() -> {System.out.println("任务被执行,时间:" + new Date());try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {}}, 2, TimeUnit.SECONDS);
}

执行结果如下:从上述结果可以看出,任务在 2 秒之后被执行了,符合我们的预期。

6.newWorkStealingPool

创建一个抢占式执行的线程池(任务执行顺序不确定),注意此方法只有在 JDK 1.8+ 版本中才能使用。

使用示例如下:

public static void workStealingPool() {// 创建线程池ExecutorService threadPool = Executors.newWorkStealingPool();// 执行任务for (int i = 0; i < 10; i++) {final int index = i;threadPool.execute(() -> {System.out.println(index + " 被执行,线程名:" + Thread.currentThread().getName());});}// 确保任务执行完成while (!threadPool.isTerminated()) {}
}

执行结果如下:从上述结果可以看出,任务的执行顺序是不确定的,因为它是抢占式执行的。

7.ThreadPoolExecutor

最原始的创建线程池的方式,它包含了 7 个参数可供设置。

使用示例如下:

public static void myThreadPoolExecutor() {// 创建线程池ThreadPoolExecutor threadPool = new ThreadPoolExecutor(5, 10, 100, TimeUnit.SECONDS, new LinkedBlockingQueue<>(10));// 执行任务for (int i = 0; i < 10; i++) {final int index = i;threadPool.execute(() -> {System.out.println(index + " 被执行,线程名:" + Thread.currentThread().getName());try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}});}
}

执行结果如下:

ThreadPoolExecutor 参数介绍

ThreadPoolExecutor 最多可以设置 7 个参数,如下代码所示:

 public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler) {// 省略...}

7 个参数代表的含义如下:

参数 1:corePoolSize

核心线程数,线程池中始终存活的线程数。

参数 2:maximumPoolSize

最大线程数,线程池中允许的最大线程数,当线程池的任务队列满了之后可以创建的最大线程数。

参数 3:keepAliveTime

最大线程数可以存活的时间,当线程中没有任务执行时,最大线程就会销毁一部分,最终保持核心线程数量的线程。

参数 4:unit:

单位是和参数 3 存活时间配合使用的,合在一起用于设定线程的存活时间 ,参数 keepAliveTime 的时间单位有以下 7 种可选:

  • TimeUnit.DAYS:天

  • TimeUnit.HOURS:小时

  • TimeUnit.MINUTES:分

  • TimeUnit.SECONDS:秒

  • TimeUnit.MILLISECONDS:毫秒

  • TimeUnit.MICROSECONDS:微妙

  • TimeUnit.NANOSECONDS:纳秒

参数 5:workQueue

一个阻塞队列,用来存储线程池等待执行的任务,均为线程安全,它包含以下 7 种类型:

  • ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。

  • LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列。

  • SynchronousQueue:一个不存储元素的阻塞队列,即直接提交给线程不保持它们。

  • PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。

  • DelayQueue:一个使用优先级队列实现的无界阻塞队列,只有在延迟期满时才能从中提取元素。

  • LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。与SynchronousQueue类似,还含有非阻塞方法。

  • LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。

较常用的是 LinkedBlockingQueueSynchronous,线程池的排队策略与 BlockingQueue 有关。

参数 6:threadFactory

线程工厂,主要用来创建线程,默认为正常优先级、非守护线程。

参数 7:handler

拒绝策略,拒绝处理任务时的策略,系统提供了 4 种可选:

  • AbortPolicy:拒绝并抛出异常。

  • CallerRunsPolicy:使用当前调用的线程来执行此任务。

  • DiscardOldestPolicy:抛弃队列头部(最旧)的一个任务,并执行当前任务。

  • DiscardPolicy:忽略并抛弃当前任务。

默认策略为 AbortPolicy

线程池的执行流程

ThreadPoolExecutor 关键节点的执行流程如下:

  • 当线程数小于核心线程数时,创建线程。

  • 当线程数大于等于核心线程数,且任务队列未满时,将任务放入任务队列。

  • 当线程数大于等于核心线程数,且任务队列已满:若线程数小于最大线程数,创建线程;若线程数等于最大线程数,抛出异常,拒绝任务。

线程池的执行流程如下图所示:

线程拒绝策略

我们来演示一下 ThreadPoolExecutor 的拒绝策略的触发,我们使用 DiscardPolicy  的拒绝策略,它会忽略并抛弃当前任务的策略,实现代码如下:

public static void main(String[] args) {// 任务的具体方法Runnable runnable = new Runnable() {@Overridepublic void run() {System.out.println("当前任务被执行,执行时间:" + new Date() +" 执行线程:" + Thread.currentThread().getName());try {// 等待 1sTimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}}};// 创建线程,线程的任务队列的长度为 1ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 1,100, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1),new ThreadPoolExecutor.DiscardPolicy());// 添加并执行 4 个任务threadPool.execute(runnable);threadPool.execute(runnable);threadPool.execute(runnable);threadPool.execute(runnable);
}

我们创建了一个核心线程数和最大线程数都为 1 的线程池,并且给线程池的任务队列设置为 1,这样当我们有 2 个以上的任务时就会触发拒绝策略,执行的结果如下图所示:从上述结果可以看出只有两个任务被正确执行了,其他多余的任务就被舍弃并忽略了。其他拒绝策略的使用类似,这里就不一一赘述了。

自定义拒绝策略

除了 Java 自身提供的 4 种拒绝策略之外,我们也可以自定义拒绝策略,示例代码如下:

public static void main(String[] args) {// 任务的具体方法Runnable runnable = new Runnable() {@Overridepublic void run() {System.out.println("当前任务被执行,执行时间:" + new Date() +" 执行线程:" + Thread.currentThread().getName());try {// 等待 1sTimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}}};// 创建线程,线程的任务队列的长度为 1ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 1,100, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1),new RejectedExecutionHandler() {@Overridepublic void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {// 执行自定义拒绝策略的相关操作System.out.println("我是自定义拒绝策略~");}});// 添加并执行 4 个任务threadPool.execute(runnable);threadPool.execute(runnable);threadPool.execute(runnable);threadPool.execute(runnable);
}

程序的执行结果如下:

究竟选用哪种线程池?

经过以上的学习我们对整个线程池也有了一定的认识了,那究竟该如何选择线程池呢?

我们来看下阿里巴巴《Java开发手册》给我们的答案:

【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

说明:Executors 返回的线程池对象的弊端如下:

1) FixedThreadPool 和 SingleThreadPool:允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。

2)CachedThreadPool:允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。

所以综上情况所述,我们推荐使用 ThreadPoolExecutor 的方式进行线程池的创建,因为这种创建方式更可控,并且更加明确了线程池的运行规则,可以规避一些未知的风险。

总结

本文我们介绍了线程池的 7 种创建方式,其中最推荐使用的是 ThreadPoolExecutor 的方式进行线程池的创建,ThreadPoolExecutor 最多可以设置 7 个参数,当然设置 5 个参数也可以正常使用,ThreadPoolExecutor 当任务过多(处理不过来)时提供了 4 种拒绝策略,当然我们也可以自定义拒绝策略,希望本文的内容能帮助到你。原创不易,觉得不错就点个赞再走吧!

参考 & 鸣谢

https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html

https://www.cnblogs.com/pcheng/p/13540619.html


往期推荐

求求你,别再用wait和notify了!


2020年终总结:新的“开始”


提高生产力,最全 MyBatisPlus 讲解!


关注我,每天陪你进步一点点!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/545118.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

SQL调用C# dll(第一中DLL,没使用强名称密匙,默认是 safe)

https://msdn.microsoft.com/zh-cn/library/ms345106(es-es).aspx 1、新建项目名称SQLDllTest&#xff0c;类代码如下&#xff0c;没有用Using引用其他类&#xff1a; &#xff08;框架必须改为.NET3.5及3.5以下&#xff0c;因为SQL Server 2008只是支持.NET 3.5及一下&#xf…

Linux系统下启动MySQL的命令及相关知识

一、总结一下&#xff1a; 1.Linux系统下启动MySQL的命令&#xff1a; /ect/init.d/mysql start (前面为mysql的安装路径) 2.linux下重启mysql的命令&#xff1a; /ect/init.d/mysql restart (前面为mysql的安装路径) 3.linux下关闭mysql的命令&#xff1a; /ect/init.d/mysql …

线性代数向量乘法_标量乘法属性1 | 使用Python的线性代数

线性代数向量乘法Prerequisite: Linear Algebra | Defining a Vector 先决条件&#xff1a; 线性代数| 定义向量 Linear algebra is the branch of mathematics concerning linear equations by using vector spaces and through matrices. In other words, a vector is a mat…

Synchronized 的 8 种使用场景!

blog.csdn.net/x541211190/article/details/106272922简介本文将介绍8种同步方法的访问场景&#xff0c;我们来看看这8种情况下&#xff0c;多线程访问同步方法是否还是线程安全的。这些场景是多线程编程中经常遇到的&#xff0c;而且也是面试时高频被问到的问题&#xff0c;所…

Python的threadpool模块

2019独角兽企业重金招聘Python工程师标准>>> Python的threadpool模块 这是一个使用python实现的线程池库。 安装 pip install threadpool 文档 http://gashero.yeax.com/?p44 http://www.chrisarndt.de/projects/threadpool/ 测试 使用一个20个线程的线程池进行测试…

MySql常用命令总结

1:使用SHOW语句找出在服务器上当前存在什么数据库&#xff1a;mysql> SHOW DATABASES;2:2、创建一个数据库MYSQLDATAmysql> CREATE DATABASE MYSQLDATA;3:选择你所创建的数据库mysql> USE MYSQLDATA; (按回车键出现Database changed 时说明操作成功&#xff01;)4:查看…

硬核Redis总结,看这篇就够了!

高清思维导图已同步Git&#xff1a;https://github.com/SoWhat1412/xmindfile总感觉哪里不对&#xff0c;但是又说不上来1、基本类型及底层实现1.1、String用途&#xff1a;适用于简单key-value存储、setnx key value实现分布式锁、计数器(原子性)、分布式全局唯一ID。底层&…

sql 数字减去null_减去两个16位数字| 8086微处理器

sql 数字减去nullProblem: Write a program to subtract two 16-bit numbers where starting address is 2000 and the numbers are at 3000 and 3002 memory address and store result into 3004 and 3006 memory address. 问题&#xff1a;编写一个程序以减去两个16位数字(起…

Java 解决采集UTF-8网页空格变成问号乱码

http://blog.csdn.net/bob007/article/details/27098875 使用此方法转换后&#xff0c;在列表中看到的正常&#xff0c;但是在详情页的文本框中查看到的就是 了&#xff0c;只好过滤掉所有的空格 html html.replaceAll(UTFSpace, " ");改为html html.replaceAll(UT…

linux中如何改IP

修改IP永久生效按以下方法vi /etc/sysconfig/network-scripts/ifcfg-eth0&#xff08;eth0&#xff0c;第一块网卡&#xff0c;如果是第二块则为eth1&#xff09;按如下修改ipDEVICEeth0&#xff08;如果是第二块刚为eth1&#xff09;BOOTPROTOstaticIPADDR192.168.0.11(改成要…

文件写入的6种方法,这种方法性能最好

作者 | 王磊来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;在 Java 中操作文件的方法本质上只有两种&#xff1a;字符流和字节流&#xff0c;而字节流和字符流的实现类又有很多&#x…

单位矩阵属性(I ^ k = I)| 使用Python的线性代数

Prerequisites: 先决条件&#xff1a; numpy.matmul( ) matrix multiplication numpy.matmul()矩阵乘法 Identity matrix 身份矩阵 In linear algebra, the identity matrix, of size n is the n n square matrix with ones on the main diagonal and zeros elsewhere. It is…

linux 更改文件权限(子文件夹)

加入-R 参数&#xff0c;就可以将读写权限传递给子文件夹例如chmod -R 777 /home/mypackage那么mypackage 文件夹和它下面的所有子文件夹的属性都变成了777.777是读、写、执行权限...

JavaScript中带有示例的Math.cos()方法

JavaScript | Math.cos()方法 (JavaScript | Math.cos() Method) Math.cos() is a function in math library of JavaScript that is used to find the value of cosine of a number and return the value in radians. Math.cos()是JavaScript数学库中的一个函数&#xff0c;用…

JDK 16 即将发布,新特性速览!

你还能追上 Java 的更新速度吗&#xff1f;当开发者深陷 Java 8 版本之际&#xff0c;这边下一版本 Java 16 有了最新的消息&#xff0c;与 Java 15 一样&#xff0c;作为短期版本&#xff0c;Oracle 仅提供 6 个月的支持。根据发布计划&#xff0c;JDK 16 将在 12 月 10 日和 …

SQL手动注入

随着B/S模式应用开发的发展&#xff0c;使用这种模式编写应用程序的程序员也越来越多。但是由于这个行业的入门门槛不高&#xff0c;程序员的水平及经验也参差不齐&#xff0c;相当大一部分程序员在编写代码的时候&#xff0c;没有对用户输入数据的合法性进行判断&#xff0c;使…

最牛逼的 Java 项目实战,没有之一!

想要成长为高级开发&#xff0c;掌握更多层面的技术&#xff0c;兼顾深度和广度是毋庸置疑的。你肯定认为&#xff0c;我要认真努力的学习技术&#xff0c;丰富自己的技术栈&#xff0c;然后就可以成为一个优秀的高级开发了。但当你真正去学习之后就会发现&#xff0c;技术栈异…

ArcPad 10 的安装部署

ArcPad是安装在手持设备或者移动终端的一个外业ArcGIS产品&#xff0c;也就是说ArcPad是Esri的一款软件产品&#xff0c;而不是硬件设备哦。尽管不比ArcGIS Desktop功能复杂缤纷&#xff0c;可是对于野外作业、数据採集等工作来说&#xff0c;算是功能十分丰富了。 说到安装&am…

python 获取当前目录_如何在Python中获取当前的工作目录?

python 获取当前目录To get the current working directory in Python, there is a library function getcwd() in the os module. 为了在Python中获得当前的工作目录&#xff0c; os模块中有一个库函数getcwd() 。 getcwd() function does not accept any parameter and retu…

定时任务的实现原理,看完就能手撸一个!

一、摘要在很多业务的系统中&#xff0c;我们常常需要定时的执行一些任务&#xff0c;例如定时发短信、定时变更数据、定时发起促销活动等等。在上篇文章中&#xff0c;我们简单的介绍了定时任务的使用方式&#xff0c;不同的架构对应的解决方案也有所不同&#xff0c;总结起来…