:Spring 运行机制与并发执行)
Agent 应用进入后端工程阶段以后模型调用通常只占整个执行过程的一部分。一个完整请求还需要经过接口接收、任务调度、模型与工具调用、状态记录以及异常处理等环节。Java 后端在这一过程中主要承担程序组织和并发执行两类职责。本篇集中说明与 Agent 开发关系较强的四个基础机制反射、注解与动态代理解释 Spring 如何组织程序线程同步解释多个任务同时执行时如何保护共享状态ThreadLocal解释请求上下文如何在调用链中传递虚拟线程则用于理解大量网络等待任务的并发执行方式。目录一、反射、注解与动态代理二、并发执行与共享状态三、ThreadLocal 与请求上下文四、虚拟线程与 Agent 的 I/O 等待一、反射、注解与动态代理1. 反射Java 反射是指程序在运行期间获取类的结构信息并在需要时动态访问构造器、字段和方法的机制。通常编写 Java 代码时调用关系已经在源代码中确定例如创建TaskService对象以后直接调用execute()方法。反射提供了另一种处理方式程序可以先获得某个类对应的Class对象然后查询这个类包含哪些方法、字段和注解随后再根据运行时信息决定如何处理【倒是有点像Skills渐进式披露】。Spring、Hibernate、Jackson 等 Java 框架都大量使用了这一机制因此反射的实际价值主要体现在框架基础设施中而不是要求业务代码频繁手工操作Method或Field。Class? clazz TaskService.class; // 获取 TaskService 当前声明的全部方法 Method[] methods clazz.getDeclaredMethods(); for (Method method : methods) { // 运行时读取每个方法的名称 System.out.println(method.getName()); }2. 注解注解与反射之间存在直接联系。注解的正式作用是为类、方法、字段、参数等程序元素附加结构化元数据。例如Service表示某个类承担 Spring 服务组件的角色RestController表示类中的方法可以参与 HTTP 请求处理Transactional则描述某个方法需要事务语义。注解本身只保存信息不会主动创建对象、开启事务或处理请求。Spring 启动以后会扫描类路径通过反射读取这些元数据再执行相应的框架逻辑。因此可以将这一过程概括为“注解提供描述反射负责读取框架根据描述执行对应操作”。3. 动态代理动态代理进一步解决方法增强问题。代理对象位于调用者与真实对象之间可以在原有业务方法执行前后插入额外逻辑。例如模型调用方法原本只负责向模型服务发送请求系统还希望统一记录调用耗时、验证权限或者控制事务。将这些公共逻辑直接复制到每一个业务方法中会增加大量重复代码代理机制则可以统一拦截方法调用【方法增强使得对象变成代理对象必须先完成事务或者切面处理】。Spring AOP、声明式事务以及部分方法级权限控制都建立在这种思想上。当代码调用一个由 Spring 管理的 Service 时实际获得的对象在某些情况下可能已经是代理对象代理先完成事务或切面处理再调用真正的业务方法。对于 Agent 后端理解这一组机制的意义在于能够解释 Spring AI 或普通 Spring Boot 项目中的大量自动行为。例如一个模型工具方法可能通过注解声明为可调用工具框架在启动时读取方法签名和注解信息再生成工具描述并注册到 Agent 运行环境中业务方法上的事务、权限和日志切面也可能通过代理完成。初学阶段首先应建立清晰关系注解描述程序语义反射读取运行时结构动态代理在调用边界增加统一行为。二、并发执行与共享状态并发是指系统在同一时间段内推进多个任务的执行。Agent 后端通常具有明显的并发特征因为每个用户请求都可能经历知识检索、模型调用、数据库访问以及工具调用多个用户的任务又会同时存在。并发本身并不会自动产生错误真正需要控制的是共享状态。例如多个任务同时修改同一个任务计数器、缓存对象或者本地资源状态时如果没有明确的同步机制就可能因为操作交叉执行而得到错误结果。一个容易理解的例子是runningTasks。从源代码看它只有一行但实际执行过程至少包含读取旧值、计算新值和写回结果三个步骤。如果两个线程同时读取到相同旧值最终可能只完成一次有效增加。synchronized提供了 Java 最基础的互斥机制它可以保证同一个监视器保护的代码在同一时刻只由一个线程执行同时建立必要的内存可见性关系。对于简单的共享状态修改使用synchronized往往已经足够清晰。private int runningTasks 0; public synchronized void increaseRunningTasks() { // 同一时刻只有一个线程能够执行这里 runningTasks; }ReentrantLock同样能够实现互斥但它把获取锁和释放锁显式暴露给开发者因此可以进一步支持可中断获取、超时获取以及多个条件队列等控制方式。使用显式锁时必须保证释放动作位于finally中否则业务代码抛出异常后可能导致锁长期无法释放。private final ReentrantLock lock new ReentrantLock(); public void increaseRunningTasks() { lock.lock(); try { // 修改共享状态 runningTasks; } finally { // 即使业务代码异常也必须释放锁 lock.unlock(); } }对于初学阶段可以将两者理解为不同复杂度的同步工具synchronized适合常规互斥需求ReentrantLock适合需要更细控制能力的场景。实际工程中不应为了使用更“高级”的 API 而主动扩大锁机制的复杂度。Agent 工程中更值得掌握的是共享资源控制这一思想。模型并发额度、数据库连接、任务队列和外部工具调用次数都属于有限资源但这些资源通常不会直接通过synchronized解决。Java 锁主要用于保护单个进程内部的共享状态数据库事务、Redis、消息队列或者限流器则负责更大范围的资源协调。理解线程同步以后后续学习幂等、分布式锁和任务状态机时就能够看到同一问题在不同系统范围内的延伸。三、ThreadLocal 与请求上下文ThreadLocal是一种线程局部变量机制它为每个线程维护相互独立的变量值。同一个ThreadLocal对象可以被多个线程访问但不同线程读取到的是各自保存的数据。它适合保存与当前执行上下文绑定、又不希望在每一层方法参数中持续显式传递的信息例如用户编号、租户编号、请求编号以及日志链路中的traceId。假设一个 Agent 请求会依次经过 Controller、任务 Service、模型 Client 和工具 Client。如果每个方法都增加traceId参数调用链会逐渐出现大量与业务逻辑无关的参数传递。使用ThreadLocal后可以在请求进入系统时保存一次traceId随后同一线程上的业务代码可以随时读取。日志系统中的 MDC 也使用了类似思路使一次请求产生的多条日志能够携带相同链路标识。private static final ThreadLocalString TRACE_ID new ThreadLocal(); public void handle(String traceId) { try { // 请求开始时写入当前线程上下文 TRACE_ID.set(traceId); executeAgentTask(); } finally { // 请求结束以后清除线程局部数据 TRACE_ID.remove(); } }这里最关键的操作是remove()。传统 Spring Web 应用通常由线程池处理请求一个工作线程完成请求 A 后还可能继续处理请求 B。线程本身不会因为一次请求结束而立即销毁因此保存在线程中的局部数据也可能继续存在。如果请求结束后没有清理后续任务可能读取到前一次请求留下的用户信息或链路编号。这类问题通常十分隐蔽因此ThreadLocal的生命周期必须与业务上下文保持一致清理操作应放入finally等能够可靠执行的位置。在 Agent 系统中ThreadLocal适合保存单次同步调用链中的上下文但它并不能天然解决异步任务和跨服务上下文传播。任务进入消息队列以后消费者可能运行在另一台机器或另一个线程中调用下游微服务时请求上下文也需要通过 HTTP Header、消息属性或链路追踪协议显式传递。因此应把ThreadLocal理解为单个进程、当前线程中的上下文载体。跨线程、跨消息和跨服务传播属于下一层工程问题通常需要 OpenTelemetry、MDC 上下文复制或业务字段共同完成。四、虚拟线程与 Agent 的 I/O 等待虚拟线程是 Java 提供的轻量级线程实现主要用于提升大量阻塞式 I/O 任务的并发承载能力。传统 Java 平台线程与操作系统线程关系较为紧密线程数量持续增加时会带来较高的线程栈内存和调度成本。虚拟线程由 JVM 进行更轻量的调度使开发者仍然可以按照普通同步代码的方式编写网络调用同时承载远多于传统平台线程数量的等待任务。Agent 后端非常符合这一使用特征。一次任务可能先访问向量数据库然后调用大模型 API再访问搜索服务和外部工具。真正执行本地 CPU 计算的时间可能很短大量时间都消耗在等待网络响应。虚拟线程能够让这类程序保持直观的顺序代码结构同时降低大量等待线程的资源成本。例如使用newVirtualThreadPerTaskExecutor()后每个任务可以获得一个虚拟线程不需要为了提高并发能力立即改写为复杂的异步回调结构。try (var executor Executors.newVirtualThreadPerTaskExecutor()) { executor.submit(() - { // 阻塞式模型调用可以直接写成普通同步代码 String result modelClient.call(prompt); // 继续处理模型返回结果 saveResult(result); }); }虚拟线程需要与真实资源容量区分开来。假设应用能够同时创建数千个虚拟线程但数据库连接池只有 50 个连接那么任意时刻真正执行数据库操作的任务仍然受到这 50 个连接限制如果模型服务只允许 20 个并发请求增加虚拟线程也不会扩大模型提供商的处理能力。虚拟线程解决的是 Java 线程承载成本连接池、信号量、限流器和外部服务配额解决的是资源容量问题。这两类机制必须共同存在。因此在 Agent 后端中可以形成一个较清晰的执行认识Spring 通过注解、反射和代理组织应用结构Java 并发机制负责单个服务内部多个任务的安全执行ThreadLocal保存当前线程中的请求上下文虚拟线程降低大量阻塞式调用的线程成本。这些能力共同构成单个 Java Agent 服务的运行基础下一阶段再进入多个服务之间的超时、重试、幂等和故障治理问题。