根据摩尔定律所说:集成电路上可容纳的晶体管数量每 18 个月翻一番,因此 CPU 上的晶体管数量会越来越多。
成都创新互联公司专业为企业提供武义网站建设、武义做网站、武义网站设计、武义网站制作等企业网站建设、网页设计与制作、武义企业网站模板建站服务,十载武义做网站经验,不只是建网站,更提供有价值的思路和整体网络服务。
但随着时间的推移,集成电路上可容纳的晶体管数量已趋向饱和,摩尔定律也渐渐失效,因此多核 CPU 逐渐变为主流,与之相对应的多线程编程也开始变得普及和流行起来,这当然也是很久之前的事了,对于现在而言多线程编程已经成为程序员必备的职业技能了,那接下来我们就来盘一盘“线程池”这个多线程编程中最重要的话题。
什么是线程池?
线程池(ThreadPool)是一种基于池化思想管理和使用线程的机制。它是将多个线程预先存储在一个“池子”内,当有任务出现时可以避免重新创建和销毁线程所带来性能开销,只需要从“池子”内取出相应的线程执行对应的任务即可。
池化思想在计算机的应用也比较广泛,比如以下这些:
线程池的优势主要体现在以下 4 点:
同时阿里巴巴在其《Java开发手册》中也强制规定:线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。
知道了什么是线程池以及为什要用线程池之后,我们再来看怎么用线程池。
线程池使用
线程池的创建方法总共有 7 种,但总体来说可分为 2 类:
线程池的创建方式总共包含以下 7 种(其中 6 种是通过 Executors 创建的,1 种是通过 ThreadPoolExecutor 创建的):
单线程池的意义从以上代码可以看出 newSingleThreadExecutor 和 newSingleThreadScheduledExecutor 创建的都是单线程池,那么单线程池的意义是什么呢?答:虽然是单线程池,但提供了工作队列,生命周期管理,工作线程维护等功能。
那接下来我们来看每种线程池创建的具体使用。
1.FixedThreadPool
创建一个固定大小的线程池,可控制并发的线程数,超出的线程会在队列中等待。
使用示例如下:
- public static void fixedThreadPool() {
- // 创建 2 个数据级的线程池
- ExecutorService threadPool = Executors.newFixedThreadPool(2);
- // 创建任务
- Runnable runnable = new Runnable() {
- @Override
- public void run() {
- System.out.println("任务被执行,线程:" + Thread.currentThread().getName());
- }
- };
- // 线程池执行任务(一次添加 4 个任务)
- // 执行任务的方法有两种:submit 和 execute
- threadPool.submit(runnable); // 执行方式 1:submit
- threadPool.execute(runnable); // 执行方式 2:execute
- threadPool.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
workQueue, - ThreadFactory threadFactory,
- RejectedExecutionHandler handler) {
- // 省略...
- }
7 个参数代表的含义如下:
参数 1:corePoolSize
核心线程数,线程池中始终存活的线程数。
参数 2:maximumPoolSize
最大线程数,线程池中允许的最大线程数,当线程池的任务队列满了之后可以创建的最大线程数。
参数 3:keepAliveTime
最大线程数可以存活的时间,当线程中没有任务执行时,最大线程就会销毁一部分,最终保持核心线程数量的线程。
参数 4:unit:
单位是和参数 3 存活时间配合使用的,合在一起用于设定线程的存活时间 ,参数 keepAliveTime 的时间单位有以下 7 种可选:
参数 5:workQueue
一个阻塞队列,用来存储线程池等待执行的任务,均为线程安全,它包含以下 7 种类型:
较常用的是 LinkedBlockingQueue 和 Synchronous,线程池的排队策略与 BlockingQueue 有关。
参数 6:threadFactory
线程工厂,主要用来创建线程,默认为正常优先级、非守护线程。
参数 7:handler
拒绝策略,拒绝处理任务时的策略,系统提供了 4 种可选:
默认策略为 AbortPolicy。
线程池的执行流程
ThreadPoolExecutor 关键节点的执行流程如下:
线程池的执行流程如下图所示:
线程拒绝策略
我们来演示一下 ThreadPoolExecutor 的拒绝策略的触发,我们使用 DiscardPolicy 的拒绝策略,它会忽略并抛弃当前任务的策略,实现代码如下:
- public static void main(String[] args) {
- // 任务的具体方法
- Runnable runnable = new Runnable() {
- @Override
- public void run() {
- System.out.println("当前任务被执行,执行时间:" + new Date() +
- " 执行线程:" + Thread.currentThread().getName());
- try {
- // 等待 1s
- TimeUnit.SECONDS.sleep(1);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- };
- // 创建线程,线程的任务队列的长度为 1
- ThreadPoolExecutor 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() {
- @Override
- public void run() {
- System.out.println("当前任务被执行,执行时间:" + new Date() +
- " 执行线程:" + Thread.currentThread().getName());
- try {
- // 等待 1s
- TimeUnit.SECONDS.sleep(1);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- };
- // 创建线程,线程的任务队列的长度为 1
- ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 1,
- 100, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1),
- new RejectedExecutionHandler() {
- @Override
- public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
- // 执行自定义拒绝策略的相关操作
- System.out.println("我是自定义拒绝策略~");
- }
- });
- // 添加并执行 4 个任务
- threadPool.execute(runnable);
- threadPool.execute(runnable);
- threadPool.execute(runnable);
- threadPool.execute(runnable);
- }
程序的执行结果如下:
究竟选用哪种线程池?
经过以上的学习我们对整个线程池也有了一定的认识了,那究竟该如何选择线程池呢?
我们来看下阿里巴巴《Java开发手册》给我们的答案:
总结
本文我们介绍了线程池的 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
文章名称:线程池的7种创建方式,强烈推荐你用它...
网页路径:http://www.shufengxianlan.com/qtweb/news43/199043.html
网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等
声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联