0

0

Java中使用ThreadPoolExecutor并行执行独立的单线程任务的详细介绍

黄舟

黄舟

发布时间:2017-03-23 11:06:44

|

3088人浏览过

|

来源于php中文网

原创

java se 5.0中引入了任务执行框架,这是简化多线程程序设计开发的一大进步。使用这个框架可以方便地管理任务:管理任务的生命周期以及执行策略。

在这篇文章中,我们通过一个简单的例子来展现这个框架所带来的灵活与简单。

基础

执行框架引入了Executor接口来管理任务的执行。Executor是一个用来提交Runnable任务的接口。这个接口将任务提交与任务执行隔离起来:拥有不同执行策略的executor都实现了同一个提交接口。改变执行策略不会影响任务的提交逻辑。

如果你要提交一个Runnable对象来执行,很简单:

Executor exec = …;
exec.execute(runnable);

线程池

如前所述,executor如何去执行提交的runnable任务并没有在Executor接口中规定,这取决于你所用的executor的具体类型。这个框架提供了几种不同的executor,执行策略针对不同的场景而不同。

立即学习Java免费学习笔记(深入)”;

你可能会用到的最常见的executor类型就是线程池executor,也就是ThreadPoolExecutor类(及其子类)的实例。ThreadPoolExecutor管理着一个线程池和一个工作队列,线程池存放着用于执行任务的工作线程。

你肯定在其他技术中也了解过“池”的概念。使用“池”的一个最大的好处就是减少资源创建的开销,用过并释放后,还可以重用。另一个间接的好处是你可以控制使用资源的多少。比如,你可以调整线程池的大小达到你想要的负载,而不损害系统的资源。

这个框架提供了一个工厂类,叫Executors,来创建线程池。使用这个工程类你可以创建不同特性的线程池。尽管底层的实现常常是一样的(ThreadPoolExecutor),但工厂类可以使你不必使用复杂的构造函数就可以快速地设置一个线程池。工程类的工厂方法有:

  • newFixedThreadPool:该方法返回一个最大容量固定的线程池。它会按需创建新线程,线程数量不大于配置的数量大小。当线程数达到最大以后,线程池会一直维持这么多不变。

  • newCachedThreadPool:该方法返回一个无界的线程池,也就是没有最大数量限制。但当工作量减小时,这类线程池会销毁没用的线程。

  • newSingleThreadedExecutor:该方法返回一个executor,它可以保证所有的任务都在一个单线程中执行。

  • newScheduledThreadPool:该方法返回一个固定大小的线程池,它支持延时和定时任务的执行。

这仅仅是一个开端。Executor还有一些其他用法已超出了这篇文章的范围,我强烈推荐你研究以下内容:

  • 生命周期管理的方法,这些方法由ExecutorService接口声明(比如shutdown()和awaitTermination())。

  • 使用CompletionService来查询任务状态、获取返回值,如果有返回值的话。

ExecutorService接口特别重要,因为它提供了关闭线程池的方法,并确保清理了不再使用的资源。令人欣慰的是,ExecutorService接口相当简单、一目了然,我建议全面地学习下它的文档。

大致来说,当你向ExecutorService发送了一个shutdown()消息后,它就不会接收新提交的任务,但是仍在队列中的任务会被继续处理完。你可以使用isTerminated()来查询ExecutorService终止状态,或使用awaitTermination(…)方法来等待ExecutorService终止。如果传入一个最大超时时间作为参数,awaitTermination方法就不会永远等待。

警告: 对JVM进程永远不会退出的理解上,存在着一些错误和迷惑。如果你不关闭executorService,只是销毁了底层的线程,JVM就不会退出。当最后一个普通线程(非守护线程)退出后,JVM也会退出。

配置ThreadPoolExecutor

如果你决定不使用Executor的工厂类,而是手动创建一个 ThreadPoolExecutor,你需要使用构造函数来创建并配置。下面是这个类使用最广泛的一个构造函数:

public ThreadPoolExecutor(
    int corePoolSize,
    int maxPoolSize,
    long keepAlive,
    TimeUnit unit,
    BlockingQueue workQueue,
    RejectedExecutionHandler handler);

如你所见,你可以配置以下内容:

  • 核心池的大小(线程池将会使用的大小)

  • 最大池大小

  • 存活时间,空闲线程在这个时间后被销毁

  • 存放任务的工作队列

  • 任务提交拒绝后要执行的策略

    睿拓智能网站系统-网上商城
    睿拓智能网站系统-网上商城

    睿拓智能网站系统-网上商城1.0免费版软件大小:5M运行环境:asp+access本版本是永州睿拓信息专为电子商务入门级用户开发的网上电子商城系统,拥有产品发布,新闻发布,在线下单等全部功能,并且正式商用用户可在线提供多个模板更换,可实现一般网店交易所有功能,是中小企业和个人开展个人独立电子商务商城最佳的选择,以下为详细功能介绍:1.最新产品-提供最新产品发布管理修改,和最新产品订单查看2.推荐产

    下载

限制队列中任务数

限制执行任务的并发数、限制线程池大小对应用程序以及程序执行结果的可预期性与稳定性有很大的好处。无尽地创建线程,最终会耗尽运行时资源。你的应用程序因此会产生严重的性能问题,甚至导致程序不稳定。

这只解决了部分问题:限制了并发任务数,但并没有限制提交到等待队列的任务数。如果任务提交的速率一直高于任务执行的速率,那么应用程序最终会出现资源短缺的状况。

解决方法是:

  • 为Executor提供一个存放待执行任务的阻塞队列。如果队列填满,以后提交的任务会被“拒绝”。

  • 当任务提交被拒绝时会触发RejectedExecutionHandler,这也是为什么这个类名中引用动词“rejected”。你可以实现自己的拒绝策略,或者使用框架内置的策略。

默认的拒绝策略可以让executor抛出一个RejectedExecutionException异常。然而,还有其他的内建策略:

  • 悄悄地丢弃一个任务

  • 丢弃最旧的任务,重新提交最新的

  • 在调用者的线程中执行被拒绝的任务

什么时候以及为什么我们才会这样配置线程池?让我们看一个例子。

示例:并行执行独立的单线程任务

最近,我被叫去解决一个很久以前的任务的问题,我的客户之前就运行过这个任务。大致来说,这个任务包含一个组件,这个组件监听目录树所产生的文件系统事件。每当一个事件被触发,必须处理一个文件。一个专门的单线程执行文件处理。说真的,根据任务的特点,即使我能把它并行化,我也不想那么做。一天的某些时候,事件到达率才很高,文件也没必要实时处理,在第二天之前处理完即可。

当前的实现采用了一些混合且匹配的技术,包括使用UNIX SHELL脚本扫描目录结构,并检测是否发生改变。实现完成后,我们采用了双核的执行环境。同样,事件的到达率相当低:目前为止,事件数以百万计,总共要处理1~2T字节的原始数据。

运行处理程序的主机是12核的机器:很好机会去并行化这些旧的单线程任务。基本上,我们有了食谱的所有原料,我们需要做的仅仅是把程序建立起来并调节。在写代码前,我们必须了解下程序的负载。我列一下我检测到的内容:

  • 有非常多的文件需要被周期性地扫描:每个目录包含1~2百万个文件

  • 扫描算法很快,可以并行化

  • 处理一个文件至少需要1s,甚至上升到2s或3s

  • 处理文件时,性能瓶颈主要是CPU

  • CPU利用率必须可调,根据一天时间的不同而使用不同的负载配置。

我需要这样一个线程池,它的大小在程序运行的时候通过负载配置来设置。我倾向于根据负载策略创建一个固定大小的线程池。由于线程的性能瓶颈在CPU,它的核心使用率是100%,不会等待其他资源,那么负载策略就很好计算了:用执行环境的CPU核心数乘以一个负载因子(保证计算的结果在峰值时至少有一个核心):

int cpus = Runtime.getRuntime().availableProcessors();
int maxThreads = cpus * scaleFactor;
maxThreads = (maxThreads > 0 ? maxThreads : 1);

然后我需要使用阻塞队列创建一个ThreadPoolExecutor,可以限制提交的任务数。为什么?是这样,扫描算法执行很快,很快就产生庞大数量需要处理的文件。数量有多庞大呢?很难预测,因为变动太大了。我不想让executor内部的队列不加选择地填满了要执行的任务实例(这些实例包含了庞大的文件描述符)。我宁愿在队列填满时,拒绝这些文件。

而且,我将使用ThreadPoolExecutor.CallerRunsPolicy作为拒绝策略。为什么?因为当队列已满时,线程池的线程忙于处理文件,我让提交任务的线程去执行它(被拒绝的任务)。这样,扫面会停止,转而去处理一个文件,处理结束后马上又会扫描目录。

下面是创建executor的代码:

ExecutorService executorService =
    new ThreadPoolExecutor(
        maxThreads, // core thread pool size
        maxThreads, // maximum thread pool size
        1, // time to wait before resizing pool
        TimeUnit.MINUTES, 
        new ArrayBlockingQueue(maxThreads, true),
        new ThreadPoolExecutor.CallerRunsPolicy());

 下面是程序的框架(极其简化版):

// scanning loop: fake scanning
while (!dirsToProcess.isEmpty()) {
    File currentDir = dirsToProcess.pop();

    // listing children
    File[] children = currentDir.listFiles();

    // processing children
    for (final File currentFile : children) {
        // if it's a directory, defer processing
        if (currentFile.isDirectory()) {
            dirsToProcess.add(currentFile);
            continue;
        }

        executorService.submit(new Runnable() {
            @Override
            public void run() {
                try {
                    // if it's a file, process it
                    new ConvertTask(currentFile).perform();
                } catch (Exception ex) {
                    // error management logic
                }
            }
        });
    }
}

// ...
// wait for all of the executor threads to finish
executorService.shutdown();
try {
    if (!executorService.awaitTermination(60, TimeUnit.SECONDS)) {
        // pool didn't terminate after the first try
        executorService.shutdownNow();
    }

    if (!executorService.awaitTermination(60, TimeUnit.SECONDS)) {
        // pool didn't terminate after the second try
    }
} catch (InterruptedException ex) {
    executorService.shutdownNow();
    Thread.currentThread().interrupt();
}

总结

看到了吧,Java并发API非常简单易用,十分灵活,也很强大。真希望我多年前可以多花点功夫写一个这样简单的程序。这样我就可以在几小时内解决由传统单线程组件所引发的扩展性问题。

相关文章

java速学教程(入门到精通)
java速学教程(入门到精通)

java怎么学习?java怎么入门?java在哪学?java怎么学才快?不用担心,这里为大家提供了java速学教程(入门到精通),有需要的小伙伴保存下载就能学习啦!

下载

相关标签:

本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
全国统一发票查询平台入口合集
全国统一发票查询平台入口合集

本专题整合了全国统一发票查询入口地址合集,阅读专题下面的文章了解更多详细入口。

19

2026.02.03

短剧入口地址汇总
短剧入口地址汇总

本专题整合了短剧app推荐平台,阅读专题下面的文章了解更多详细入口。

27

2026.02.03

植物大战僵尸版本入口地址汇总
植物大战僵尸版本入口地址汇总

本专题整合了植物大战僵尸版本入口地址汇总,前往文章中寻找想要的答案。

15

2026.02.03

c语言中/相关合集
c语言中/相关合集

本专题整合了c语言中/的用法、含义解释。阅读专题下面的文章了解更多详细内容。

3

2026.02.03

漫蛙漫画网页版入口与正版在线阅读 漫蛙MANWA官网访问专题
漫蛙漫画网页版入口与正版在线阅读 漫蛙MANWA官网访问专题

本专题围绕漫蛙漫画(Manwa / Manwa2)官网网页版入口进行整理,涵盖漫蛙漫画官方主页访问方式、网页版在线阅读入口、台版正版漫画浏览说明及基础使用指引,帮助用户快速进入漫蛙漫画官网,稳定在线阅读正版漫画内容,避免误入非官方页面。

13

2026.02.03

Yandex官网入口与俄罗斯搜索引擎访问指南 Yandex中文登录与网页版入口
Yandex官网入口与俄罗斯搜索引擎访问指南 Yandex中文登录与网页版入口

本专题汇总了俄罗斯知名搜索引擎 Yandex 的官网入口、免登录访问地址、中文登录方法与网页版使用指南,帮助用户稳定访问 Yandex 官网,并提供一站式入口汇总。无论是登录入口还是在线搜索,用户都能快速获取最新稳定的访问链接与使用指南。

114

2026.02.03

Java 设计模式与重构实践
Java 设计模式与重构实践

本专题专注讲解 Java 中常用的设计模式,包括单例模式、工厂模式、观察者模式、策略模式等,并结合代码重构实践,帮助学习者掌握 如何运用设计模式优化代码结构,提高代码的可读性、可维护性和扩展性。通过具体示例,展示设计模式如何解决实际开发中的复杂问题。

3

2026.02.03

C# 并发与异步编程
C# 并发与异步编程

本专题系统讲解 C# 异步编程与并发控制,重点介绍 async 和 await 关键字、Task 类、线程池管理、并发数据结构、死锁与线程安全问题。通过多个实战项目,帮助学习者掌握 如何在 C# 中编写高效的异步代码,提升应用的并发性能与响应速度。

2

2026.02.03

Python 强化学习与深度Q网络(DQN)
Python 强化学习与深度Q网络(DQN)

本专题深入讲解 Python 在强化学习(Reinforcement Learning)中的应用,重点介绍 深度Q网络(DQN) 及其实现方法,涵盖 Q-learning 算法、深度学习与神经网络的结合、环境模拟与奖励机制设计、探索与利用的平衡等。通过构建一个简单的游戏AI,帮助学习者掌握 如何使用 Python 训练智能体在动态环境中作出决策。

3

2026.02.03

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Kotlin 教程
Kotlin 教程

共23课时 | 3.2万人学习

C# 教程
C# 教程

共94课时 | 8.4万人学习

Java 教程
Java 教程

共578课时 | 56.6万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号