1. 概述

本文将探讨警告:*"在非阻塞上下文中可能存在阻塞调用,可能导致线程饥饿"*。首先,我们通过简单示例重现该警告,并探讨在无关紧要时如何抑制它。

接着,我们将讨论忽略该警告的风险,并探索两种有效解决问题的方法。

2. 非阻塞上下文中的阻塞方法

当我们在响应式上下文中使用阻塞操作时,IntelliJ IDEA会提示"在非阻塞上下文中可能存在阻塞调用,可能导致线程饥饿"的警告

假设我们正在使用Spring WebFluxNetty服务器开发响应式Web应用。如果在处理本应保持非阻塞的HTTP请求时引入阻塞操作,就会遇到此警告:

intellij警告截图

该警告源于IntelliJ IDEA的静态分析。如果我们确信它不会影响应用,可以使用"BlockingMethodInNonBlockingContext"检查名称轻松抑制:

@SuppressWarnings("BlockingMethodInNonBlockingContext")
@GetMapping("/warning")
Mono<String> warning() { 
    // ...
 }

然而,理解底层问题并评估其影响至关重要。在某些情况下,这可能导致负责处理HTTP请求的线程被阻塞,引发严重后果

3. 理解警告

让我们展示一个忽略警告可能导致线程饥饿并阻塞传入HTTP流量的场景。为此,我们将添加另一个接口,并在响应式上下文中故意使用Thread.sleep()阻塞线程2秒:

@GetMapping("/blocking")
Mono<String> getBlocking() {
    return Mono.fromCallable(() -> {
        try {
            Thread.sleep(2_000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        return "foo";
    });
}

这种情况下,处理传入HTTP请求的Netty事件循环线程可能被快速阻塞,导致应用无响应。例如,发送200个并发请求时,即使没有涉及计算,应用也需要32秒才能响应所有请求。更糟糕的是,这还会影响其他接口——即使它们不需要阻塞操作。

延迟发生的原因是Netty HTTP线程池大小为12,因此一次只能处理12个请求。查看IntelliJ分析器,我们会发现线程大部分时间处于阻塞状态,且测试期间CPU使用率极低:

Netty阻塞调用分析图

4. 解决问题

理想情况下,我们应该切换到响应式API来解决问题。但当不可行时,应为这类操作使用独立线程池,避免阻塞HTTP线程。

4.1 使用响应式替代方案

首先,我们应尽可能采用响应式方法。这意味着为阻塞操作寻找响应式替代方案。

例如,我们可以尝试使用响应式数据库驱动配合Spring Data Reactive Repositories,或使用WebClient等响应式HTTP客户端。在简单场景中,可以使用Mono的API延迟响应2秒,而非依赖阻塞的Thread.sleep()

@GetMapping("/non-blocking")
Mono<String> getNonBlocking() {
    return Mono.just("bar")
      .delayElement(Duration.ofSeconds(2));
}

采用此方法,应用能处理数百个并发请求,并在我们引入的2秒延迟后发送所有响应。

4.2 为阻塞操作使用专用调度器

另一方面,某些情况下我们无法使用响应式API。常见场景是使用非响应式驱动查询数据库,这会导致阻塞操作:

@GetMapping("/blocking-with-scheduler")
Mono<String> getBlockingWithDedicatedScheduler() {
    String data = fetchDataBlocking();
    return Mono.just("retrieved data: " + data);
}

这时,我们可以将阻塞操作包装在Mono中,并使用subscribeOn()指定执行调度器。这会生成一个Mono<String>,后续可映射为所需的响应格式:

@GetMapping("/blocking-with-scheduler")
Mono<String> getBlockingWithDedicatedScheduler() {
    return Mono.fromCallable(this::fetchDataBlocking)
      .subscribeOn(Schedulers.boundedElastic())
      .map(data -> "retrieved data: " + data);
}

5. 总结

本教程介绍了IntelliJ静态分析器生成的*"在非阻塞上下文中可能存在阻塞调用,可能导致线程饥饿"*警告。通过代码示例,我们演示了忽略此警告如何阻塞Netty处理传入HTTP请求的线程池,导致应用无响应。

随后,我们了解了优先使用响应式API如何帮助解决问题。此外,当没有响应式替代方案时,我们应使用独立线程池处理阻塞操作。

本教程的源代码可在GitHub获取。


原始标题:Handling the Blocking Method in Non-blocking Context Warning | Baeldung