结合实际场景,假设前端发起一个请求开始,调后端的/getUser接口,到接口查到数据返回给前端这个过程中,线程是如何执行的。
先说结论:当一个前端请求进来,干活的就是线程。我们在代码中的写的逻辑(执行方法、判断、校验、查redis、查数据库等),都是线程在执行。
以一个具体的场景为例:前端发起一个HTTP请求到后端的
/getUser接口,后端查询数据库并返回数据。我们将详细描述在这个过程中,线程是如何工作的。假设环境:Spring Boot应用,使用Tomcat作为嵌入式容器,使用JDBC连接数据库。
步骤分解:
- 前端发起请求:用户在浏览器中点击按钮,前端代码(如JavaScript)向后端发送一个HTTP GET请求到
/getUser。- 请求到达服务器:请求通过网络到达服务器,被服务器的网络接口接收。
- Tomcat线程池处理请求:
- Tomcat有一个线程池(默认最大线程数为200),当有请求到达时,Tomcat会从线程池中分配一个线程来处理这个请求。
- 假设此时线程池中有一个线程(称为Thread-A)被分配来处理这个请求。
- 线程Thread-A开始执行:
- Thread-A首先读取HTTP请求,解析出请求的URL、参数等信息。
- 根据URL,Tomcat会找到对应的Servlet(在Spring Boot中,DispatcherServlet是前端控制器)。
- DispatcherServlet会根据URL映射到对应的Controller方法(例如
getUser方法)。- 进入Controller方法:
- 现在,Thread-A开始执行
getUser方法中的代码。- 在
getUser方法中,可能会调用Service层的方法,Service层再调用DAO层(如MyBatis的Mapper)来查询数据库。- 数据库查询:
- 当Thread-A执行到数据库查询代码时,它会从数据库连接池中获取一个数据库连接(注意:数据库连接池也有自己的线程模型,但这里我们关注的是应用线程)。
- 使用JDBC发送SQL查询到数据库。此时,Thread-A可能会等待数据库的响应(因为数据库查询通常是同步的,即Thread-A会阻塞直到数据库返回结果)。
- 数据库处理查询:
- 数据库接收到查询请求,执行查询,并将结果返回给应用。
- 在此期间,Thread-A处于阻塞状态,等待数据库响应。
- 数据库返回结果:
- 数据库返回结果,Thread-A被唤醒,继续执行,将结果组装成需要的对象。
- 返回响应:
- Thread-A继续执行,将结果转换为JSON(或其他格式),然后通过HTTP响应返回给前端。
- 请求结束:
- 一旦响应发送完毕,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线程池中获取),直到请求处理完毕。
前端请求 → Tomcat 接收 → Tomcat 工作线程 → Spring MVC 处理 → 业务线程执行 → 数据库查询 → 返回响应
GET /getUser?id=123@GetMapping("/getUser")// 浏览器/前端应用
fetch('/getUser?id=123')
.then(response => response.json())
.then(data => console.log(data));
线程状态:前端有自己的线程(浏览器渲染线程),与后端无关。
网络数据包 → 操作系统内核 → Tomcat 连接器(Connector)
Tomcat 线程模型:
// Tomcat 默认使用线程池
Executor executor = new ThreadPoolExecutor(
corePoolSize = 10, // 核心线程数
maximumPoolSize = 200, // 最大线程数
keepAliveTime = 60s, // 空闲线程存活时间
workQueue = new LinkedBlockingQueue<>(100) // 等待队列
);
// Tomcat 从线程池中获取一个线程
Thread tomcatWorkerThread = executor.poll(); // 获取线程
// 这个线程开始执行 HTTP 请求处理
tomcatWorkerThread.setName("http-nio-8080-exec-1");
线程信息:
线程名:http-nio-8080-exec-1
线程ID:12345
线程状态:RUNNABLE
// 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 工作线程在执行。
@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() 方法
@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
@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 工作线程会阻塞等待
// 但不会创建新线程,仍然是同一个线程
// 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 并写入响应
| 时间点 | 线程状态 | 执行操作 |
|---|---|---|
| T0 | NEW | Tomcat 线程池创建线程 |
| T1 | RUNNABLE | 接收 HTTP 请求 |
| T2 | RUNNABLE | 执行 DispatcherServlet |
| T3 | RUNNABLE | 执行 Controller 方法 |
| T4 | RUNNABLE | 执行 Service 业务逻辑 |
| T5 | BLOCKED | 等待数据库查询结果(阻塞) |
| T6 | RUNNABLE | 继续处理业务逻辑 |
| T7 | RUNNABLE | 将结果转换为 JSON |
| T8 | TERMINATED | 请求完成,线程返回线程池 |
// 同一个线程处理多个请求
请求1 → http-nio-8080-exec-1 → 处理完成 → 返回线程池
请求2 → http-nio-8080-exec-1 → 处理完成 → 返回线程池
请求3 → http-nio-8080-exec-1 → 处理完成 → 返回线程池
// 传统 Spring MVC 是同步阻塞的
public User getUser(Long id) {
// 线程会一直阻塞在这里,直到数据库返回结果
User user = userMapper.selectById(id);
return user; // 数据库返回后才会执行
}
@RestController
public class UserController {
// ❌ 错误:使用成员变量(线程不安全)
private User currentUser; // 多个请求会共享这个变量
// ✅ 正确:使用局部变量(线程安全)
public User getUser(Long id) {
User currentUser = userService.getUserById(id);
return currentUser;
}
}
Tomcat线程 → 全程处理 → 返回结果
@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响应 -------|<-- 响应完成 ----------|
| |--- 线程返回线程池 --->|
@Async时,会由 Spring 的异步线程池处理,Tomcat 线程可以快速释放一句话理解:
前端的一个 HTTP 请求,在后端是由 Tomcat 线程池中的一个工作线程从头到尾处理的,包括 Controller、Service、DAO 等所有环节,直到返回响应。