在日常的 Java 开发中,并发编程一直是一个让开发者头疼的问题 😩。随着系统规模的扩大,异步任务越来越多,如何管理和协调这些任务变得尤为复杂。错误处理、超时控制、以及异步任务间的依赖关系,常常让代码变得杂乱无章,维护起来也更加困难。
不过,从 JDK 9 开始,随着 JEP 266 (多并发更新)的引入,异步编排迎来了一个大的变革 ✨。本文将从常见的异步编排难点入手,讲解 JEP 266 给我们带来的改变,并展示一些新特性如何让异步编排变得更简洁高效。
并发编程的常见难点 😖
在 JDK 9 之前,处理异步任务的超时通常需要手动管理定时器。例如:
java
ExecutorService executor = Executors.newFixedThreadPool(2);
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> slowService(), executor);
ScheduledFuture<?> timeout = scheduler.schedule(() -> {
future.completeExceptionally(new TimeoutException("Timeout after 2 seconds"));
}, 2, TimeUnit.SECONDS);
future.whenComplete((result, ex) -> timeout.cancel(true));
如上所示,我们需要为每个异步任务手动创建超时逻辑,且需要关注错误处理、任务取消等细节。虽然Java8引入了CompletableFuture
,但是开发这仍需要面临以下这些困扰:
1. 任务超时管理 ⏳
在异步任务中,超时是常见问题。如果没有一个好的超时控制机制,任务可能会无限期等待下去,导致程序卡死。以前,我们需要编写定时器,检查每个任务是否超时。
例如:我们有一个支付服务,它通过异步调用第三方支付接口进行支付。为了避免支付过程中的死锁或长时间等待,我们通常需要设置超时。但是,如果没有超时控制机制,可能会发生支付请求卡住的情况。
但是在JDK8中我们只能进行超时异常中断:
java
import java.util.concurrent.CompletableFuture;
public class PaymentService {
public static void main(String[] args) {
CompletableFuture<Void> paymentFuture = initiatePaymentAsync();
// 这里我们想要进行支付的逻辑,但没有超时控制
paymentFuture.join(); // 如果支付请求没有成功,程序将永远卡在这里
System.out.println("支付完成");
}
public static CompletableFuture<Void> initiatePaymentAsync() {
return CompletableFuture.runAsync(() -> {
try {
// 模拟调用第三方支付接口
System.out.println("正在向第三方支付接口发送请求...");
Thread.sleep(5000); // 模拟支付接口响应延迟
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
}
2. 复杂的链式调用 🔗
异步编排通常涉及多个任务的顺序执行。例如,thenApply
, thenCompose
, whenComplete
等方法,虽然能够将异步操作串联起来,但也容易导致代码层级过多,逻辑混乱,维护成本增加。
例如有下面五个任务:
Task 1 :启动异步任务,返回一个初始值 5
。
Task 2:对任务结果进行处理,乘以 2。
Task 3 :继续处理,将结果加上 10
。
Task 4 :使用 thenCompose
执行另一个异步任务,将之前的结果乘以 3
。
Task 5 :继续处理,结果加上 20
。
whenComplete:在所有异步任务完成后,检查是否有错误,并输出最终结果。
代码示例:
java
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
public class AsyncTaskExample {
public static void main(String[] args) throws ExecutionException, InterruptedException {
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
System.out.println("Task 1: Start");
try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println("Task 1: End");
return 5;
})
.thenApply(result -> {
System.out.println("Task 2: Start");
try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println("Task 2: End");
return result * 2;
})
.thenApply(result -> {
System.out.println("Task 3: Start");
try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println("Task 3: End");
return result + 10;
})
.thenCompose(result -> {
System.out.println("Task 4: Start");
try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println("Task 4: End");
return CompletableFuture.supplyAsync(() -> result * 3);
})
.thenApply(result -> {
System.out.println("Task 5: Start");
try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println("Task 5: End");
return result + 20;
})
.whenComplete((result, throwable) -> {
if (throwable != null) {
System.out.println("Error: " + throwable.getMessage());
} else {
System.out.println("Final Result: " + result);
}
});
// Block and get the final result
future.get();
}
}
3. 错误处理 ⚠️
异步编排中的错误处理往往容易被忽略,尤其是当任务链中的某一环节发生错误时,整个任务链可能会中断。如何优雅地捕获和处理异步任务中的异常,成为开发者的一大难题。
例如:当任务链中的某一环节发生异常时,如果没有恰当的错误处理机制,整个任务链会中断,后续的任务无法继续执行,例如下面三个任务:
- Task 1 :执行第一个异步任务,在任务中抛出异常
RuntimeException
。 - Task 2 :如果前面的任务成功执行,它会继续进行计算。在这个例子中,它没有执行,因为
Task 1
抛出了异常。 - exceptionally() :当
Task 1
抛出异常时,exceptionally()
方法会捕获到该异常并处理它。在这个例子中,返回了一个默认值-1
,继续执行后续任务。 - Task 3 :由于
exceptionally()
返回了-1
,Task 3
会继续执行,基于默认值进行计算。 - whenComplete() :最终,在所有任务完成后,无论是正常完成还是出现异常,都会执行
whenComplete()
来报告最终结果或错误。
代码示例:
java
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
public class AsyncErrorHandling {
public static void main(String[] args) throws ExecutionException, InterruptedException {
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
System.out.println("Task 1: Start");
if (true) {
throw new RuntimeException("Something went wrong in Task 1");
}
return 5;
})
.thenApply(result -> {
System.out.println("Task 2: Start");
return result * 2;
})
.exceptionally(ex -> {
System.out.println("Caught exception in Task 2: " + ex.getMessage());
return -1; // return a fallback value in case of an error
})
.thenApply(result -> {
System.out.println("Task 3: Start");
return result + 10;
})
.whenComplete((result, throwable) -> {
if (throwable != null) {
System.out.println("Error during the chain: " + throwable.getMessage());
} else {
System.out.println("Completed with result: " + result);
}
});
// Block and get the final result
Integer finalResult = future.get();
System.out.println("Final Result: " + finalResult);
}
}
4. 背压问题 ⬇️
当多个异步任务同时执行时,如果没有合理的流量控制,可能会导致资源的过度消耗,甚至出现内存泄漏或性能瓶颈。**背压(Backpressure)**管理就是为了应对这种情况,它要求开发者能灵活控制数据流的速度。
假设我们有多个异步任务需要执行,每个任务都需要从数据库或远程服务获取数据。如果这些任务执行得太快,可能会造成数据库连接池或网络带宽耗尽,导致性能瓶颈,甚至崩溃。
问题场景:
- 假设我们有一个异步任务池,其中任务从数据库中查询数据。
- 如果这些任务同时执行并且没有流量控制,那么它们可能会消耗过多的数据库连接,导致数据库连接池枯竭。
- 这个时候,背压机制就变得尤为重要,它可以限制并发执行的任务数,从而避免过度加载数据库
示例代码:
java
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class BackpressureExample {
public static void main(String[] args) throws InterruptedException {
ExecutorService executor = Executors.newFixedThreadPool(5); // 限制最大并发任务数为 5
CompletableFuture<Void>[] futures = new CompletableFuture[10]; // 模拟 10 个任务
for (int i = 0; i < 10; i++) {
int taskId = i;
futures[i] = CompletableFuture.supplyAsync(() -> {
System.out.println("Task " + taskId + ": Start");
simulateDatabaseQuery(); // 模拟数据库查询
return "Data from task " + taskId;
}, executor)
.thenApply(result -> {
System.out.println("Task " + taskId + ": Processing " + result);
return result;
})
.exceptionally(ex -> {
System.out.println("Task " + taskId + ": Exception - " + ex.getMessage());
return null;
});
}
// 等待所有任务完成
CompletableFuture.allOf(futures).join();
// 关闭线程池
executor.shutdown();
}
private static void simulateDatabaseQuery() {
try {
// 模拟数据库查询时间
TimeUnit.SECONDS.sleep(2); // 假设每个查询需要 2 秒
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
JDK 9 给异步编排带来了哪些变化? ✨
JDK 9 通过 JEP 266 引入了一系列新的并发编程特性,这些特性在减少代码复杂度、增强异步流控制、以及提升性能等方面,提供了有效的支持。
1. CompletableFuture
异步能力大增 🏃♂️
JDK 9 增强了 CompletableFuture
的能力,新增了多个方法,让我们更容易控制异步任务的执行:
orTimeout
:为异步任务设置超时。如果任务超时,会自动抛出TimeoutException
⚡️。completeOnTimeout
:如果任务超时,会自动返回给定的默认值,而不是抛出异常 💡。delayedExecutor
:支持延迟执行异步任务,省去了手动编写定时器的麻烦 ⏰。
这些新方法直接简化了以前我们需要手动编写的超时管理和错误处理代码,极大地提高了开发效率。
最后我们一张图总结一下:

2. Flow API:标准化异步流 🌊
JEP 266 还引入了 java.util.concurrent.Flow
,为 Java 提供了官方的异步流处理支持。这个类库遵循 Reactive Streams 标准,提供了对数据流的正式支持:
- Publisher:发布数据的角色 📤。
- Subscriber:订阅数据的角色 📥。
- Subscription:订阅关系 🔗。
- Processor:在发布者和订阅者之间进行数据转换的中间处理器 🔄。
通过这种标准化的方式,开发者不再需要自己设计复杂的 API,而是能直接使用统一接口来处理异步流。
一张图总结Flow api功能:
3. 并发集合的性能提升 ⚡️
JDK 9 对并发集合(例如 ConcurrentHashMap
)进行了优化,减少了内部锁的使用,从而提高了性能。这对于高并发环境中的异步任务非常有帮助,减少了线程竞争,提高了吞吐量 🚀。
JDK 9 新特性如何简化异步编排? 🤖
java 9 的 JEP 266 对并发编程引入了一些新特性,极大地简化了异步编排过程,让开发者可以更轻松地管理异步任务,避免了传统方式中的许多繁琐操作。在 JDK 9 后,通过 CompletableFuture
的新方法(如 orTimeout
)和 Flow API
,异步任务的管理变得更加简洁,减少了手动编写的繁琐代码。
1. CompletableFuture
的新方法 📝
JDK 9 中,CompletableFuture
新增了以下方法来处理异步任务的超时和错误:
-
orTimeout
:为异步任务设置超时。如果超时,任务会自动报错。这使得开发者不再需要编写额外的超时检查代码。示例:
javaCompletableFuture.supplyAsync(() -> slowService()) .orTimeout(2, TimeUnit.SECONDS) .exceptionally(ex -> fallback());
-
completeOnTimeout
:如果超时,自动返回给定的值,而不是抛出异常。这适用于对超时不做特殊处理的情况。示例:
javaCompletableFuture.supplyAsync(() -> slowService()) .completeOnTimeout("Timeout Result", 2, TimeUnit.SECONDS);
-
delayedExecutor
:延迟执行任务,不再需要手动实现定时器。示例:
javaCompletableFuture.supplyAsync(() -> slowService(), CompletableFuture.delayedExecutor(1, TimeUnit.SECONDS));
所以当我们再遇到并发任务编排的场景时,我们便可以像这样编写避免任务链中断:
java
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
public class AsyncErrorHandlingJDK9 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
System.out.println("Task 1: Start");
if (true) {
throw new RuntimeException("Something went wrong in Task 1");
}
return 5;
})
.thenApply(result -> {
System.out.println("Task 2: Start");
return result * 2;
})
.exceptionally(ex -> {
System.out.println("Caught exception in Task 2: " + ex.getMessage());
return -1; // return a fallback value in case of an error
})
.thenApply(result -> {
System.out.println("Task 3: Start");
return result + 10;
})
.orTimeout(2, TimeUnit.SECONDS) // Set a timeout for the entire chain
.completeOnTimeout(-1, 2, TimeUnit.SECONDS) // Provide a fallback result on timeout
.whenComplete((result, throwable) -> {
if (throwable != null) {
System.out.println("Error during the chain: " + throwable.getMessage());
} else {
System.out.println("Completed with result: " + result);
}
});
// Block and get the final result
Integer finalResult = future.get();
System.out.println("Final Result: " + finalResult);
}
}
2. Flow API:简化异步流处理 🌐
通过 Flow API,Java 提供了标准化的异步流处理方式。当我们遇到并发查询数据库时便可以这样编写:
java
import java.util.concurrent.Flow.*;
import java.util.concurrent.SubmissionPublisher;
import java.util.concurrent.TimeUnit;
public class FlowBackpressureExample {
public static void main(String[] args) throws InterruptedException {
// 创建一个 SubmissionPublisher 作为 Publisher
SubmissionPublisher<String> publisher = new SubmissionPublisher<>();
// 创建一个 Subscriber,控制流量
Subscriber<String> subscriber = new Subscriber<>() {
private Subscription subscription;
@Override
public void onSubscribe(Subscription subscription) {
this.subscription = subscription;
subscription.request(1); // 初始请求 1 个数据
}
@Override
public void onNext(String item) {
System.out.println("Received: " + item);
simulateDatabaseQuery(); // 模拟处理时间
subscription.request(1); // 请求下一个数据
}
@Override
public void onError(Throwable throwable) {
System.err.println("Error occurred: " + throwable.getMessage());
}
@Override
public void onComplete() {
System.out.println("All data has been processed.");
}
};
// 订阅 Publisher
publisher.subscribe(subscriber);
// 发布 10 个任务
for (int i = 1; i <= 10; i++) {
publisher.submit("Data " + i);
}
// 等待任务完成
TimeUnit.SECONDS.sleep(5); // 等待一段时间确保任务被处理完
publisher.close();
}
// 模拟数据库查询,假设每个查询需要 2 秒钟
private static void simulateDatabaseQuery() {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
通过flow api,我们便可以精确背压:
背压控制 :通过 Subscription.request(n)
来控制订阅者每次可以处理的数据量。这里使用了 request(1)
,意味着每次订阅者只能处理一个数据项,确保任务不会过多并行执行。
流速的调节 :在消费者处理数据时,request(1)
会确保当一个任务处理完毕之后,才会继续处理下一个任务。这保证了在高并发环境中,系统不会因为任务过载而崩溃。
这种方式使得我们能够灵活地处理异步数据流,同时有效避免了传统方式中的复杂操作。
总结:JEP 266 的影响 🌟
JEP 266 带来的新特性,不仅让异步编排变得更加简洁和标准化,还提高了代码的可维护性和性能。通过使用 orTimeout
、Flow API
等新方法,我们能够更加方便地管理异步任务、错误处理以及超时控制。
如果你还没有使用这些新特性,建议从简单的 orTimeout
和 Flow API
开始,逐步提升你的异步编排能力。相信你会发现,异步编排再也不需要那么痛苦了! 🎉
想了解更多 JDK 新特性,提升开发效率?欢迎关注我的专栏 👉 JDK 新特性专栏!一起来变得更强大吧 🚀