Servlet 线程安全与并发编程深度解析
Servlet 线程安全与并发编程深度解析
一、Servlet 线程安全机制与风险场景
1.1 Servlet 容器工作原理
- 单实例多线程模型:每个Servlet在容器中只有一个实例,通过线程池处理并发请求
- 请求处理流程:
- 接收HTTP请求创建
HttpServletRequest
和HttpServletResponse
- 从线程池获取工作线程
- 调用
service()
方法处理请求 - 返回响应后线程归还线程池
- 接收HTTP请求创建
1.2 线程安全风险根源
风险类型 | 典型场景 | 后果示例 |
---|---|---|
实例变量共享 | Servlet中定义成员变量 | 数据竞争导致脏读/丢失更新 |
静态变量共享 | 全局计数器、缓存容器 | 统计结果不准确 |
共享对象传递 | 将非线程安全对象传递给其他组件 | 并发修改异常 |
资源未正确关闭 | 数据库连接未及时释放 | 连接池耗尽导致系统瘫痪 |
1.3 典型风险场景分析
场景1:成员变量共享
public class UnsafeServlet extends HttpServlet {private int counter = 0; // 危险!所有线程共享protected void doGet(HttpServletRequest req, HttpServletResponse resp) {counter++;// 多线程下counter值不可预测}
}
场景2:共享非线程安全工具类
public class DateServlet extends HttpServlet {private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); // 非线程安全protected void doGet(...) {Date date = sdf.parse(request.getParameter("date")); // 可能抛出异常}
}
场景3:缓存管理不当
public class CacheServlet extends HttpServlet {private static Map<String, Object> cache = new HashMap<>(); // HashMap非线程安全protected void doPost(...) {cache.put(key, value); // 并发put可能丢失数据}
}
二、线程安全解决方案体系
2.1 防御性编程原则
- 无状态化设计:业务处理不依赖实例变量
- 局部变量优先:方法内创建的变量线程安全
- 不可变对象:使用String、BigInteger等
- 线程封闭:通过ThreadLocal隔离状态
2.2 同步控制方案对比
方案 | 适用场景 | 优点 | 缺点 |
---|---|---|---|
synchronized | 临界区代码简单 | 实现简单 | 性能差,可能死锁 |
ReentrantLock | 需要高级功能(如超时) | 功能丰富 | 需手动释放锁 |
ReadWriteLock | 读多写少场景 | 提升读性能 | 实现复杂 |
ThreadLocal | 线程级状态隔离 | 无锁高性能 | 内存泄漏风险 |
2.3 实用解决方案示例
方案1:方法同步
public class SafeServlet extends HttpServlet {private int counter = 0;public synchronized void doGet(...) { // 方法级同步counter++;// 业务处理}
}
方案2:使用线程安全容器
public class CacheServlet extends HttpServlet {private static Map<String, Object> cache = new ConcurrentHashMap<>();protected void doPost(...) {cache.put(key, value); // 安全操作}
}
方案3:ThreadLocal隔离
public class DateFormatServlet extends HttpServlet {private static final ThreadLocal<SimpleDateFormat> formatters = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));protected void doGet(...) {SimpleDateFormat sdf = formatters.get(); // 每个线程独立实例Date date = sdf.parse(...);}
}
三、Spring Boot 中 Servlet 的线程安全体现在哪里?
1. Spring Boot 还是使用 Servlet 的单实例多线程模型
Spring Boot 底层是基于 嵌入式 Servlet 容器(如 Tomcat、Jetty、Undertow),这些容器依旧遵循 Servlet 规范,也就是:
-
一个
@RestController
(或者@Controller
)的 bean 实例是 单例(默认是 Spring 单例) -
每个请求来临时,由容器(如 Tomcat)分配线程处理,这些线程会调用 Controller 方法
2. Spring 会为每个请求注入 独立的 request/response 对象
你在 Controller 方法中这样写:
@GetMapping("/hello")
public String hello(HttpServletRequest request) {// request 是每个请求独立的对象return "Hello";
}
这个 request
实际是由 Spring MVC 使用 参数解析器 从当前线程上下文(ThreadLocal)中注入的,线程之间是互不影响的。
3. Spring Boot Controller/Service 中也存在线程安全问题!
Spring 帮你注入的是线程安全的 request,但你写的代码如果有 共享状态(比如成员变量),那依然会有线程安全问题:
@RestController
public class UnsafeController {private int count = 0;@GetMapping("/count")public String count() {count++; // 非线程安全,多线程下会有竞态条件return "count=" + count;}
}
和 Servlet 示例中本质上一样的危险。
四、Spring Boot 中线程安全的体现/处理方式有哪些?
场景 | Spring Boot 中的处理方式 | 安全性体现 |
---|---|---|
Controller 请求处理 | 每个请求分配独立线程 + 参数注入 | 请求无共享状态 |
多个请求共享字段 | 使用局部变量 / ThreadLocal / 并发容器 | 手动保证安全 |
Service 单例共享状态 | 避免使用成员变量做临时状态 | 防御性编程 |
并发控制需求 | 使用 @Async 、synchronized 、锁对象等 | 显式控制 |
五、实战举例:Spring Boot 安全与不安全对比
❌ 不安全示例(成员变量共享)
@RestController
public class BadController {private List<String> list = new ArrayList<>();@PostMapping("/add")public String add(@RequestParam String value) {list.add(value); // 非线程安全return "OK";}
}
✅ 安全改法(使用线程安全容器)
@RestController
public class GoodController {private List<String> list = Collections.synchronizedList(new ArrayList<>());@PostMapping("/add")public String add(@RequestParam String value) {list.add(value); // 安全return "OK";}
}
或者直接用 CopyOnWriteArrayList
。
六、补充:Spring 线程封闭的典型应用
Spring 中常用的 ThreadLocal
场景是:
-
RequestContextHolder
:将请求上下文与线程绑定 -
TransactionSynchronizationManager
:事务同步绑定 -
SecurityContextHolder
:Spring Security 用户上下文绑定
平时在 Controller 里能随便用这些上下文,其实都是 ThreadLocal
实现的线程封闭。
七、不可变对象与线程安全
不可变对象特征
- 所有字段final修饰
- 类声明为final
- 不暴露可变字段
- 构造后状态不可变
String类实现分析
public final class String {private final char value[];private final int hash;public String substring(int beginIndex) {return new String(value, beginIndex, subLen);}
}
final关键字的正确理解
- 引用不可变 vs 对象不可变
final List<String> list = new ArrayList<>(); // 可以修改list内容 list = new LinkedList<>(); // 编译错误final String s = "hello"; s = "world"; // 编译错误