Java代码中,线程是如何参与的


在java中,如何理解线程。

结合实际场景,假设前端发起一个请求开始,调后端的/getUser接口,到接口查到数据返回给前端这个过程中,线程是如何执行的。

先说结论:当一个前端请求进来,干活的就是线程。我们在代码中的写的逻辑(执行方法、判断、校验、查redis、查数据库等),都是线程在执行。

以一个具体的场景为例:前端发起一个HTTP请求到后端的 /getUser接口,后端查询数据库并返回数据。我们将详细描述在这个过程中,线程是如何工作的。

假设环境:Spring Boot应用,使用Tomcat作为嵌入式容器,使用JDBC连接数据库。

步骤分解:

  1. 前端发起请求:用户在浏览器中点击按钮,前端代码(如JavaScript)向后端发送一个HTTP GET请求到 /getUser
  2. 请求到达服务器:请求通过网络到达服务器,被服务器的网络接口接收。
  3. Tomcat线程池处理请求
    • Tomcat有一个线程池(默认最大线程数为200),当有请求到达时,Tomcat会从线程池中分配一个线程来处理这个请求。
    • 假设此时线程池中有一个线程(称为Thread-A)被分配来处理这个请求。
  4. 线程Thread-A开始执行
    • Thread-A首先读取HTTP请求,解析出请求的URL、参数等信息。
    • 根据URL,Tomcat会找到对应的Servlet(在Spring Boot中,DispatcherServlet是前端控制器)。
    • DispatcherServlet会根据URL映射到对应的Controller方法(例如getUser方法)。
  5. 进入Controller方法
    • 现在,Thread-A开始执行getUser方法中的代码。
    • getUser方法中,可能会调用Service层的方法,Service层再调用DAO层(如MyBatis的Mapper)来查询数据库。
  6. 数据库查询
    • 当Thread-A执行到数据库查询代码时,它会从数据库连接池中获取一个数据库连接(注意:数据库连接池也有自己的线程模型,但这里我们关注的是应用线程)。
    • 使用JDBC发送SQL查询到数据库。此时,Thread-A可能会等待数据库的响应(因为数据库查询通常是同步的,即Thread-A会阻塞直到数据库返回结果)。
  7. 数据库处理查询
    • 数据库接收到查询请求,执行查询,并将结果返回给应用。
    • 在此期间,Thread-A处于阻塞状态,等待数据库响应。
  8. 数据库返回结果
    • 数据库返回结果,Thread-A被唤醒,继续执行,将结果组装成需要的对象。
  9. 返回响应
    • Thread-A继续执行,将结果转换为JSON(或其他格式),然后通过HTTP响应返回给前端。
  10. 请求结束
    • 一旦响应发送完毕,Thread-A就可以被回收,回到线程池中,等待下一个请求。

在这个过程中,我们主要关注应用层的线程(即Tomcat的工作线程)。注意,数据库查询时,应用线程会阻塞,直到数据库返回结果。

但是,如果我们在应用中使用了异步处理(例如使用@Async注解或者返回CompletableFuture),那么线程模型会有所不同。例如,Controller方法可以立即返回一个CompletableFuture,然后由另一个线程(例如从Spring的异步线程池中获取的线程)来执行实际的数据库查询。这样,Tomcat的线程就可以立即释放,去处理其他请求,从而提高吞吐量。

另外,如果使用响应式编程(如Spring WebFlux),那么整个处理过程可能是非阻塞的,线程模型也会不同。

总结:在一个典型的Spring Boot应用中,每个HTTP请求通常由一个Tomcat工作线程从头到尾处理(除非使用了异步或响应式编程)。这个线程会执行Controller、Service、DAO等所有代码,直到返回响应。如果在这个过程中有阻塞操作(如数据库查询),那么这个线程就会被阻塞,直到操作完成。

为了更好地理解,我们可以用一个简单的时序图表示:

前端 -> Tomcat线程池 -> DispatcherServlet -> Controller -> Service -> DAO -> 数据库

然后,结果沿相反方向返回。

注意:在DAO层,我们使用数据库连接池(如HikariCP)来获取数据库连接,这个连接池也是多线程安全的,每个线程从连接池中获取自己的连接,使用完毕后归还。

因此,在这个场景中,线程的执行是同步的,一个请求对应一个线程(从Tomcat线程池中获取),直到请求处理完毕。

Java 线程执行全流程解析:从前端请求到数据返回

一、整体线程执行流程概览

前端请求 → Tomcat 接收 → Tomcat 工作线程 → Spring MVC 处理 → 业务线程执行 → 数据库查询 → 返回响应

二、详细执行步骤(结合具体场景)

场景设定

  • 前端发起:GET /getUser?id=123
  • 后端接口:@GetMapping("/getUser")
  • 业务逻辑:查询用户信息并返回

步骤1:前端发起请求

// 浏览器/前端应用
fetch('/getUser?id=123')
  .then(response => response.json())
  .then(data => console.log(data));

线程状态:前端有自己的线程(浏览器渲染线程),与后端无关。


步骤2:Tomcat 接收请求

网络数据包 → 操作系统内核 → Tomcat 连接器(Connector)

Tomcat 线程模型

// Tomcat 默认使用线程池
Executor executor = new ThreadPoolExecutor(
    corePoolSize = 10,      // 核心线程数
    maximumPoolSize = 200,  // 最大线程数
    keepAliveTime = 60s,     // 空闲线程存活时间
    workQueue = new LinkedBlockingQueue<>(100) // 等待队列
);

步骤3:Tomcat 分配工作线程

// Tomcat 从线程池中获取一个线程
Thread tomcatWorkerThread = executor.poll(); // 获取线程

// 这个线程开始执行 HTTP 请求处理
tomcatWorkerThread.setName("http-nio-8080-exec-1");

线程信息

线程名:http-nio-8080-exec-1
线程ID:12345
线程状态:RUNNABLE

步骤4:进入 Spring MVC 处理

// DispatcherServlet 开始处理
public class DispatcherServlet extends FrameworkServlet {
    
    protected void doDispatch(HttpServletRequest request, HttpServletResponse response) {
        // 获取处理器(Controller方法)
        HandlerExecutionChain mappedHandler = getHandler(request);
        
        // 获取适配器
        HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
        
        // 执行处理器(仍然是同一个 Tomcat 线程)
        ModelAndView mv = ha.handle(request, response, mappedHandler.getHandler());
    }
}

关键点仍然是同一个 Tomcat 工作线程在执行


步骤5:执行 Controller 方法

@RestController
public class UserController {
    
    @GetMapping("/getUser")
    public User getUser(@RequestParam Long id) {
        System.out.println("当前线程: " + Thread.currentThread().getName());
        // 输出:当前线程: http-nio-8080-exec-1
        
        // 调用 Service
        return userService.getUserById(id);
    }
}

线程状态

线程名:http-nio-8080-exec-1
正在执行:UserController.getUser() 方法

步骤6:Service 层业务处理

@Service
public class UserService {
    
    @Autowired
    private UserMapper userMapper;
    
    public User getUserById(Long id) {
        System.out.println("Service线程: " + Thread.currentThread().getName());
        // 输出:Service线程: http-nio-8080-exec-1
        
        // 参数校验
        if (id == null || id <= 0) {
            throw new IllegalArgumentException("用户ID无效");
        }
        
        // 调用 DAO 层
        User user = userMapper.selectById(id);
        
        // 业务逻辑处理
        user.setLastLoginTime(LocalDateTime.now());
        return user;
    }
}

仍然是同一个线程http-nio-8080-exec-1


步骤7:MyBatis 数据库查询

@Mapper
public interface UserMapper {
    
    @Select("SELECT * FROM user WHERE id = #{id}")
    User selectById(Long id);
}

线程执行过程

http-nio-8080-exec-1 线程
    ↓
MyBatis 创建 SqlSession
    ↓
从数据库连接池获取连接(HikariCP)
    ↓
执行 SQL 查询
    ↓
等待数据库返回结果(线程阻塞)
    ↓
数据库返回结果
    ↓
MyBatis 映射结果到 User 对象

数据库连接池线程

// HikariCP 连接池
HikariDataSource dataSource = new HikariDataSource();
dataSource.setMaximumPoolSize(10); // 最大连接数

// 注意:数据库查询时,Tomcat 工作线程会阻塞等待
// 但不会创建新线程,仍然是同一个线程

步骤8:返回响应

// Spring MVC 将结果转换为 JSON
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
converter.write(user, MediaType.APPLICATION_JSON, response);

// 通过 Tomcat 工作线程写回响应
response.getWriter().write(jsonString);

线程状态

线程名:http-nio-8080-exec-1
线程状态:RUNNABLE
执行操作:将 User 对象转换为 JSON 并写入响应

三、线程状态变化全过程

时间点线程状态执行操作
T0NEWTomcat 线程池创建线程
T1RUNNABLE接收 HTTP 请求
T2RUNNABLE执行 DispatcherServlet
T3RUNNABLE执行 Controller 方法
T4RUNNABLE执行 Service 业务逻辑
T5BLOCKED等待数据库查询结果(阻塞)
T6RUNNABLE继续处理业务逻辑
T7RUNNABLE将结果转换为 JSON
T8TERMINATED请求完成,线程返回线程池

四、关键理解点

1. 线程复用

// 同一个线程处理多个请求
请求1 → http-nio-8080-exec-1 → 处理完成 → 返回线程池
请求2 → http-nio-8080-exec-1 → 处理完成 → 返回线程池
请求3 → http-nio-8080-exec-1 → 处理完成 → 返回线程池

2. 同步阻塞模型

// 传统 Spring MVC 是同步阻塞的
public User getUser(Long id) {
    // 线程会一直阻塞在这里,直到数据库返回结果
    User user = userMapper.selectById(id);
    return user; // 数据库返回后才会执行
}

3. 线程安全考虑

@RestController
public class UserController {
    
    // ❌ 错误:使用成员变量(线程不安全)
    private User currentUser; // 多个请求会共享这个变量
    
    // ✅ 正确:使用局部变量(线程安全)
    public User getUser(Long id) {
        User currentUser = userService.getUserById(id);
        return currentUser;
    }
}

五、异步处理场景(对比理解)

同步处理(当前场景)

Tomcat线程 → 全程处理 → 返回结果

异步处理(@Async)

@Service
public class UserService {
    
    @Async("taskExecutor")
    public CompletableFuture<User> getUserAsync(Long id) {
        System.out.println("异步线程: " + Thread.currentThread().getName());
        // 输出:异步线程: task-executor-1
        
        User user = userMapper.selectById(id);
        return CompletableFuture.completedFuture(user);
    }
}

@RestController
public class UserController {
    
    @GetMapping("/getUserAsync")
    public CompletableFuture<User> getUserAsync(@RequestParam Long id) {
        System.out.println("Controller线程: " + Thread.currentThread().getName());
        // 输出:Controller线程: http-nio-8080-exec-1
        
        // Tomcat 线程立即返回,异步线程执行查询
        return userService.getUserAsync(id);
    }
}

异步线程模型

Tomcat线程(http-nio-8080-exec-1) → 快速返回
Spring异步线程(task-executor-1) → 执行数据库查询

六、完整时序图

前端线程           Tomcat线程池           Spring MVC           Service层           MyBatis层           数据库
   |                     |                     |                     |                     |
   |--- GET /getUser --->|                     |                     |                     |
   |                     |--- 分配线程 --->|                     |                     |                     |
   |                     |   (http-nio-8080-exec-1) |                     |                     |
   |                     |                     |--- DispatcherServlet -->|                     |                     |
   |                     |                     |                     |--- UserService --->|                     |
   |                     |                     |                     |                     |--- selectById --->|                     |
   |                     |                     |                     |                     |                     |--- 执行SQL --->|
   |                     |                     |                     |                     |                     |<-- 返回结果 ---|
   |                     |                     |                     |                     |<-- User对象 -----|
   |                     |                     |                     |<-- User对象 ------|
   |                     |                     |<-- User对象 ---------|
   |<-- JSON响应 -------|<-- 响应完成 ----------|
   |                     |--- 线程返回线程池 --->|

七、核心总结

  1. 一个请求对应一个线程:从 Tomcat 接收请求到返回响应,通常由同一个线程处理
  2. 线程来自线程池:Tomcat 维护一个线程池,避免频繁创建销毁线程
  3. 同步阻塞模型:数据库查询时,线程会阻塞等待,直到数据库返回结果
  4. 线程安全重要:避免在 Controller/Service 中使用成员变量存储请求相关数据
  5. 异步处理不同:使用 @Async时,会由 Spring 的异步线程池处理,Tomcat 线程可以快速释放

一句话理解

前端的一个 HTTP 请求,在后端是由 Tomcat 线程池中的一个工作线程从头到尾处理的,包括 Controller、Service、DAO 等所有环节,直到返回响应。

SpringBoot
JAVA-技能点
多线程