Worker API:让Gradle真正跑起来
说实话,很多Java开发者用Gradle好几年,都没意识到一个关键问题——你的构建任务,其实一直在「单线程」里打转。我刚开始带团队时也踩过这个坑,项目一大了,构建时间从几十秒飙到几分钟,大家干等着。后来我仔细研究了Worker API,才明白原来Gradle早就给了我们一把利器,只是很多人没用上。
什么是Worker API?
Worker API,说白了就是Gradle提供的一套并行任务执行框架。它允许你把一个任务拆成多个独立的工作单元,然后让Gradle自动调度这些单元在多个线程里跑。
你想想看,如果你的项目有10个模块需要编译,每个模块之间没有依赖关系,那为什么不让它们同时编译呢?Worker API就是干这个的。
核心思想:将任务拆分为可并行执行的「工作单元」(Work Unit),由Gradle的Worker Daemon统一调度。
为什么需要Worker API?
我见过太多人用Gradle的parallel属性,以为开了并行就万事大吉。其实parallel只是让不同任务并行,但一个任务内部如果有多步操作,它还是串行的。Worker API解决的是「任务内部的并行」问题。
举个例子:
- 你有一个代码生成任务,需要生成100个文件
- 每个文件的生成逻辑完全独立
- 用普通方式:循环100次,一次生成一个
- 用Worker API:拆成100个工作单元,同时跑
效果很明显——时间从100个单位降到10个单位(假设10个线程)。
Worker API的核心组件
要理解Worker API,你得先认识这几个角色:
| 组件 | 作用 |
|---|---|
| WorkQueue | 工作队列,你把任务提交到这里 |
| WorkAction | 实际干活的人,定义每个工作单元做什么 |
| WorkParameters | 干活需要的参数,比如输入文件、输出目录 |
| WorkerExecutor | 调度器,管理WorkQueue和Worker Daemon |
它们的关系是这样的:
实战:写一个并行代码生成器
光说不练假把式。我直接给你看一个我在项目中用过的例子——一个代码生成任务,需要生成多个Java文件。
先定义参数:
interface CodeGenParams extends WorkParameters {
// 要生成的类名
Property<String> getClassName();
// 输出目录
DirectoryProperty getOutputDir();
// 模板内容
Property<String> getTemplate();
}
然后定义干活的人:
abstract class CodeGenAction implements WorkAction<CodeGenParams> {
@Override
public void execute() {
String className = getParameters().getClassName().get();
File outputDir = getParameters().getOutputDir().getAsFile().get();
String template = getParameters().getTemplate().get();
// 生成Java文件
String content = template.replace("${CLASS_NAME}", className);
File outputFile = new File(outputDir, className + ".java");
outputFile.getParentFile().mkdirs();
Files.write(outputFile.toPath(), content.getBytes());
// 我习惯在这里加个日志,方便排查
System.out.println("生成文件: " + outputFile.getAbsolutePath());
}
}
最后在任务里提交:
abstract class CodeGenTask extends DefaultTask {
@Inject
abstract WorkerExecutor getWorkerExecutor();
@Input
List<String> classNames = ["UserService", "OrderService", "ProductService"];
@OutputDirectory
DirectoryProperty outputDir = ...;
@TaskAction
void generate() {
// 创建一个WorkQueue,允许并行
WorkQueue queue = getWorkerExecutor()
.noIsolation()
.processIsolation(settings -> {
settings.forkOptions(jvmArgs -> {
jvmArgs.add("-Xmx256m");
});
});
for (String name : classNames) {
queue.submit(CodeGenAction.class, params -> {
params.getClassName().set(name);
params.getOutputDir().set(outputDir);
params.getTemplate().set(getTemplate());
});
}
// 等待所有任务完成
queue.await();
}
}
我的经验:这里有个细节——noIsolation()和processIsolation()的区别。前者在同一个JVM里跑,速度快但隔离性差;后者会fork新进程,安全但开销大。我个人建议:如果任务不涉及类加载冲突,用noIsolation()就够了。
隔离与线程安全
说到隔离,我得提醒你一个坑。Worker API虽然帮你管理了线程,但它不保证你的代码是线程安全的。
我曾经遇到过一个案例:两个WorkAction同时往同一个文件里写内容,结果文件内容乱掉了。排查了半天,才发现是共享了同一个输出流。
避坑指南:
- 每个WorkAction的输入参数必须是不可变的,或者至少是线程安全的
- 输出文件路径要确保唯一,不要两个Action写同一个文件
- 不要在WorkAction里访问外部的可变状态,比如全局变量
- 如果必须共享资源,用
synchronized或者ReentrantLock保护
Gradle提供了三种隔离级别:
| 隔离级别 | 说明 | 适用场景 |
|---|---|---|
| noIsolation() | 在同一个JVM里运行,共享类加载器 | 简单任务,无类冲突风险 |
| classLoaderIsolation() | 同一个JVM,但使用独立的类加载器 | 需要隔离类库的任务 |
| processIsolation() | fork新进程,完全隔离 | 需要独立JVM参数或极度安全 |
性能提升案例
说个真实数据。我之前维护的一个微服务项目,有12个模块。每次构建时,有一个「代码检查」任务,需要对每个模块运行PMD和Checkstyle。
改造前:
- 串行执行12个模块的检查
- 每个模块耗时约8秒
- 总耗时:96秒
改造后(用Worker API,8个线程):
- 并行执行12个模块的检查
- 每个模块耗时约8秒
- 总耗时:16秒(8个模块同时跑,剩下4个再跑一轮)
提升了整整6倍。你想想看,如果每次构建省下80秒,一天构建10次就是800秒,一个月就是6个多小时。这效率提升,谁用谁知道。
关键配置:别忘了在gradle.properties里设置org.gradle.workers.max=8,否则Worker API默认只开4个线程。
什么时候不该用Worker API?
嗯,这里要注意。Worker API不是万能药。我见过有人滥用它,反而让构建变慢了。
- 任务本身很快:如果每个工作单元执行时间不到100ms,线程调度的开销可能超过执行本身
- IO密集型任务:如果任务主要是在读写磁盘,并行反而可能造成IO争抢
- 有严格顺序依赖:如果任务B必须等任务A的结果,那就别强行并行
说白了,Worker API最适合的是CPU密集型的、相互独立的任务。比如代码生成、静态分析、单元测试(如果测试之间无依赖)等。
最后说一句,Worker API是Gradle 4.0引入的,到现在已经很成熟了。如果你还在用老版本的Gradle,建议升级。我见过太多团队守着旧版本,错过了这么好的性能优化手段。
公众号:蓝海资料掘金营,微信deep3321