一、Undertow线程阻塞的常见应用场景

很多Java后端服务在上线后遇到高并发问题,比如电商平台的秒杀活动、短视频平台的热点内容接口、或者日常大流量的查询接口,这个时候如果使用Undertow作为Web容器,很容易出现线程阻塞的情况。举个例子,双11时商家的商品详情页接口,每秒请求量达到几万次,此时如果服务内部的处理逻辑没有优化,就会导致线程被占住,新的请求只能等待,出现服务响应慢、超时甚至503错误的情况。线程阻塞的核心表现就是,服务的线程池里很多线程处于BLOCKED或者WAITING状态,无法及时处理新的请求,导致整体吞吐量下降。

二、线程阻塞的排查思路和具体示例

遇到线程阻塞的问题,第一步是找到到底哪些线程在阻塞,以及阻塞的原因是什么。我们可以借助JDK自带的工具,比如jps、jstack,Undertow的日志也能给我们一些线索。下面通过一个具体的示例来演示:

2.1 导致线程阻塞的错误代码示例

我们先写一个简单的Undertow服务,里面的处理逻辑会导致线程阻塞,技术栈是Java 11 + Undertow 2.2,代码如下:

package com.example;

import io.undertow.Undertow;
import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;

// 技术栈:Java 11 + Undertow 2.2
public class BlockedDemoApp {
    public static void main(String[] args) {
        // 启动Undertow服务,默认配置下工作线程数是200
        Undertow server = Undertow.builder()
            .addHttpListener(8080, "localhost")
            .setHandler(new SyncDbHandler())
            .build();
        server.start();
        System.out.println("服务启动,端口8080,等待请求...");
    }

    // 同步调用数据库的Handler,会占用线程较长时间
    static class SyncDbHandler implements HttpHandler {
        @Override
        public void handleRequest(HttpServerExchange exchange) throws Exception {
            // 直接同步连接数据库,查询商品数据
            Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test_db", "root", "123456");
            Statement stmt = conn.createStatement();
            ResultSet rs = stmt.executeQuery("SELECT * FROM goods WHERE category='electronics'");
            while (rs.next()) {
                // 模拟业务处理,比如整理数据返回
            }
            // 关闭资源
            rs.close();
            stmt.close();
            conn.close();
            // 响应客户端
            exchange.getResponseSender().send("商品查询成功");
        }
    }
}

这个服务的问题在于,Undertow默认的I/O线程会被同步的数据库操作占住,因为数据库查询是耗时的IO操作,会让线程等待资源,当高并发时,大量请求进入后,线程池里的线程很快被占满,新的请求只能排队等待,出现阻塞。

2.2 排查线程阻塞的命令示例

当服务出现阻塞时,我们可以用jps找到服务的进程ID,再用jstack查看线程的状态:

# 查找Java进程ID,找到BlockedDemoApp对应的PID,比如是1567
jps -l | grep BlockedDemoApp
# 导出线程栈信息,查看哪些线程处于BLOCKED状态
jstack 1567 | grep -A 30 "BLOCKED"

如果看到大量线程处于BLOCKED状态,且栈信息里显示在等待数据库连接或者JDBC相关的锁,就说明是同步IO导致的线程阻塞。

三、导致线程阻塞的核心原因及对应解决方法

通过排查,我们可以把线程阻塞的常见原因分为几类,每类都有对应的解决办法:

3.1 线程池配置不合理

Undertow默认的工作线程数是200,这个数值对于低并发场景足够,但高并发、IO密集型的场景就不够用了,当所有线程都在阻塞等待时,新的请求无法获得线程,只能排队。解决方法是调整Undertow的线程池参数,比如设置更大的工作线程数,或者调整队列长度,避免线程不够用。

3.2 同步IO操作占用I/O线程

Undertow的I/O线程是用来处理请求和响应的,这些线程的职责是快速处理请求,不能做耗时的阻塞操作(比如同步数据库调用、同步RPC调用)。如果在I/O线程里做这些操作,就会导致I/O线程被占住,无法处理新的请求,高并发下就会出现阻塞。解决方法是把耗时操作提交到自定义的工作线程池,释放I/O线程,代码优化示例如下:

package com.example;

import io.undertow.Undertow;
import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange;
import io.undertow.util.StatusCodes;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

// 技术栈:Java 11 + Undertow 2.2
public class FixedDemoApp {
    // 自定义工作线程池,处理耗时的IO操作,线程数设置为CPU核数的2倍(适合IO密集型)
    private static final ExecutorService WORKER_POOL = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2);

    public static void main(String[] args) {
        Undertow server = Undertow.builder()
            .addHttpListener(8080, "localhost")
            .setHandler(new NonBlockedHandler())
            .build();
        server.start();
        System.out.println("优化后的服务启动,支持高并发...");
    }

    static class NonBlockedHandler implements HttpHandler {
        @Override
        public void handleRequest(HttpServerExchange exchange) throws Exception {
            // 立即返回接受状态,避免I/O线程被阻塞
            exchange.setStatusCode(StatusCodes.ACCEPTED);
            exchange.endExchange();

            // 异步提交耗时的数据库操作到自定义线程池
            WORKER_POOL.submit(() -> {
                Connection conn = null;
                Statement stmt = null;
                ResultSet rs = null;
                try {
                    conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test_db", "root", "123456");
                    stmt = conn.createStatement();
                    rs = stmt.executeQuery("SELECT * FROM goods WHERE category='electronics'");
                    StringBuilder result = new StringBuilder();
                    while (rs.next()) {
                        result.append(rs.getString("name")).append(",");
                    }
                    // 把结果返回给客户端
                    exchange.getResponseSender().send(result.toString());
                } catch (Exception e) {
                    exchange.setStatusCode(StatusCodes.INTERNAL_SERVER_ERROR);
                    exchange.getResponseSender().send("查询失败,请稍后再试");
                } finally {
                    // 手动释放所有资源,避免连接泄漏
                    if (rs != null) rs.close();
                    if (stmt != null) stmt.close();
                    if (conn != null) conn.close();
                }
                return null;
            });
        }
    }

    // 应用关闭时关闭线程池,释放资源
    public static void shutdown() {
        WORKER_POOL.shutdown();
    }
}

3.3 数据库连接池资源不足

如果应用用的数据库连接池(比如HikariCP)的默认连接数不够(HikariCP默认是10),大量线程等待获取数据库连接,就会导致线程阻塞。解决方法是调整连接池的最大连接数,比如设置为20或者30,根据数据库的承载能力来定,避免连接太多导致数据库压力过大。

3.4 第三方依赖的同步调用

调用第三方接口时用了同步调用,比如调用另一个微服务的接口,同步等待响应,这样也会占用线程,高并发下导致阻塞。解决方法是用异步RPC调用,比如用Reactor或者Spring Cloud的异步调用,避免线程被占住。

四、技术优缺点与注意事项

4.1 Undertow的技术优缺点

Undertow是Java生态里的高性能Web容器,它的优点包括:轻量,体积小,启动速度快;支持全异步IO,适合高并发场景;模块化设计,可扩展性强;和Spring Boot等框架集成良好。缺点是:默认配置偏向低并发,高并发需要手动调优;文档相对于Tomcat来说不够丰富,新手入门难度稍高;对于复杂的线程池配置,需要一定的经验。

4.2 使用Undertow的注意事项

  1. 不要在Undertow的I/O线程里做任何耗时的阻塞操作,必须放到自定义工作线程池或者用异步处理;
  2. 及时关闭所有资源,比如数据库连接、文件句柄,避免资源泄漏导致的线程阻塞;
  3. 线程池参数要根据业务类型调整:CPU密集型任务,线程数设置为CPU核数+1;IO密集型任务,线程数设置为CPU核数的2-3倍;
  4. 高并发场景下,建议启用Undertow的异步处理机制,比如使用AsyncContext,或者Undertow自带的非阻塞Handler;
  5. 定期监控线程状态,用jstack、Prometheus等工具查看线程的阻塞情况,及时发现问题。

五、总结

Undertow在高并发场景下出现线程阻塞的问题,本质是线程资源没有被合理利用,或者被耗时的阻塞操作占住。排查的思路是先用工具找到阻塞的线程,定位到具体的代码位置,然后分析原因,调整线程池配置、优化IO操作、解决资源泄漏等问题。通过合理的调优,Undertow可以很好地支撑高并发场景,提升服务的吞吐量和响应速度。对于开发者来说,在开发时要注意区分I/O线程和工作线程,避免阻塞核心线程,这样就能减少线程阻塞的问题,让服务更稳定。