当前位置: 首页 > news >正文

技术演进中的开发沉思-138java-servlet篇:Servlet 多线程的

早期在电商项目中首次遭遇并发问题:同一时刻 100 个用户查询订单,页面竟显示了别人的订单信息。后来才明白,Servlet 的单例多线程特性就像一把 “双刃剑”—— 线程复用提升了效率,却也让多个请求共享同一实例,若处理不当,就会出现 “数据串台” 的混乱。

Servlet 容器默认采用 “单实例多线程” 模型:一个 Servlet 类仅创建一个实例,每次请求分配一个线程调用 service () 方法。这种设计就像一家餐厅只有一个厨房(Servlet 实例),却有多个厨师(线程)同时做菜,效率高但需严格的 “操作规范”,否则食材(数据)就会被弄混。

而多线程并发的核心矛盾,在于 “共享资源的访问控制”:当多个线程同时读写同一资源(如成员变量、数据库连接),若没有防护措施,就会出现 “线程安全问题”。接下来要讲的四种实战要点,本质都是解决这个矛盾的不同方案。

一、并发处理

1. 被废弃的 SingleThreadModel

早年为解决并发问题,有人会让 Servlet 实现 SingleThreadModel 接口,容器会为每个请求创建新实例,就像给每个顾客单独开一间厨房,虽安全但效率极低:


// 已废弃!不推荐使用public class OrderServlet extends HttpServlet implements SingleThreadModel {private Order currentOrder; // 即使是成员变量,也因多实例不会串数据@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) {currentOrder = queryOrder(req.getParameter("id"));// 处理订单...}}

这种方式会导致实例泛滥,服务器内存被快速耗尽 ——2008 年某 OA 系统用这种方案,并发 1000 人时直接宕机,这也是它被 Servlet 2.4 规范废弃的核心原因。

2. synchronized 的合理使用

当多个线程需操作同一关键资源(如库存更新),可用 synchronized 加锁,就像厨房的 “专用工具柜”,一次只允许一个厨师使用:


public class InventoryServlet extends HttpServlet {private int stock = 100; // 共享库存(关键资源)@Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) {int buyCount = Integer.parseInt(req.getParameter("count"));// 对库存更新加锁,确保同一时刻只有一个线程修改synchronized (this) {if (stock >= buyCount) {stock -= buyCount;resp.getWriter().write("下单成功,剩余库存:" + stock);} else {resp.getWriter().write("库存不足");}}}}

但要注意:锁的范围不能过大,若把整个 doPost () 都加锁,就像让所有厨师排队用厨房,效率会骤降。

3. ThreadLocal 的妙用

对于每个线程独有的数据(如用户登录信息),用 ThreadLocal 存储,就像给每个厨师发一本 “专属笔记本”,数据互不干扰:

public class UserServlet extends HttpServlet {// 定义ThreadLocal,存储当前线程的用户信息private static final ThreadLocal<User> currentUser = new ThreadLocal<>();@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) {// 1. 解析用户登录信息(每个线程的信息不同)User user = parseLoginUser(req);// 2. 存储到ThreadLocal,供当前线程后续使用currentUser.set(user);try {// 3. 后续业务逻辑可直接获取,无需参数传递checkPermission();showUserInfo(resp);} finally {// 4. 必须移除,避免线程复用导致数据残留currentUser.remove();}}private void checkPermission() {User user = currentUser.get(); // 直接获取当前线程的用户// 权限校验逻辑...}}

这在电商订单处理中尤为实用:从登录到下单的整个流程,用户信息无需反复传递,且不会被其他线程篡改。

4. 无状态设计(最优解)

无状态设计是并发处理的 “终极方案”——Servlet 不存储任何可变数据,所有请求相关数据都通过参数传递或从外部获取,就像厨房不存放任何食材,每次做菜都从仓库取,自然不会混乱:


// 无状态Servlet:无成员变量,所有数据通过参数和外部服务获取public class StatelessOrderServlet extends HttpServlet {// 依赖注入无状态的Service(Service也不存可变数据)@Autowiredprivate OrderService orderService;@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) {String orderId = req.getParameter("id");// 1. 从请求参数获取数据(线程私有)// 2. 调用无状态Service处理(Service仅做计算,不存数据)OrderDTO order = orderService.getOrderById(orderId);// 3. 返回结果,不存储任何中间数据resp.getWriter().write(JSON.toJSONString(order));}}

这种设计在微服务架构中极为常见,也是 Spring MVC 推荐的最佳实践。

二、电商秒杀的并发攻防战

回到电商那个并发请求的场景中,假设面临 “1 秒 10 万请求” 的并发压力,仅用 Servlet 就实现了稳定支撑,关键在于分场景使用上述方案:

库存更新(synchronized + 数据库锁)

秒杀的核心是库存扣减,我们在 Servlet 层用 synchronized 控制请求进入频率,再在数据库层加行锁(SELECT ... FOR UPDATE),双重防护避免超卖。就像 “先在餐厅门口限流,再让厨师按顺序做菜”,既保证安全又不浪费资源。

用户登录信息(ThreadLocal)

秒杀时需验证用户是否有资格(如是否实名认证),我们用 ThreadLocal 存储用户 Token 解析后的信息,避免每次校验都查 Redis,响应时间从 50ms 降到 10ms。

整体架构(无状态设计)

所有 Servlet 都设计为无状态,可横向扩容 —— 秒杀前将 Servlet 部署到 10 台服务器,通过 Nginx 负载均衡分发请求,就像 “多开几家相同的餐厅”,轻松应对海量客流。

而早期我们曾因用了有状态设计(Servlet 存库存缓存),导致多服务器间库存数据不一致,出现 “超卖” 事故。这也让我深刻体会:无状态设计是高并发场景的 “基石”。

三、对比分析:四种并发方案的 “优劣对决”

方案

核心特点

像什么场景

电商 / OA 适配性

痛点

SingleThreadModel

多实例,线程隔离

每个顾客一间厨房

仅适合极低并发的 Demo

内存占用高,效率极低,已废弃

synchronized

加锁保护共享资源

工具柜一次只允许一人使用

库存更新、订单状态修改等场景

锁范围过大导致效率低,易死锁

ThreadLocal

线程私有数据存储

每个厨师一本专属笔记本

用户登录信息、请求上下文传递

忘记 remove 导致数据残留

无状态设计

不存可变数据,依赖外部

厨房不存食材,按需取货

高并发秒杀、微服务接口

需依赖外部存储(如 Redis),开发成本略高

印象最深的是 曾用过四种方案处理 10 万次订单查询,无状态设计的吞吐量是 SingleThreadModel 的 50 倍,是 synchronized(全方法锁)的 8 倍。这也印证了:越简洁的并发方案,在高并发场景下表现越出色。

四、常见陷阱

synchronized 锁错对象

有个同事在 Servlet 中用 synchronized (new Object())加锁,结果每个线程都拿到新锁,完全起不到保护作用。这就像给每个厨师发一把不同的钥匙,谁都能开工具柜,自然会混乱。

解法:锁对象必须是全局唯一的(如this、静态常量)。

ThreadLocal 忘记 remove

某 OA 系统的用户 Servlet 中,ThreadLocal 未在 finally 块移除,导致线程池复用后,A 用户的信息被 B 用户获取。这就像厨师用完笔记本没带走,下一个厨师接着用,信息肯定会串。

解法:所有 ThreadLocal 操作必须在 finally 块中调用 remove ()。

无状态设计 “伪无状态”

有人认为 “没有成员变量就是无状态”,却在 Service 中用了静态成员变量存缓存。这就像厨房虽不存食材,却在墙上贴了 “今日库存”,多厨房场景下数据依然会不一致。

解法:确保所有层级(Servlet、Service、DAO)都不存可变数据,缓存依赖 Redis 等分布式存储。

五、最佳实践:高并发 Servlet 的 “避坑指南”

坚决不用 SingleThreadModel

即使是新手,也应从源头杜绝这种废弃方案,优先选择无状态设计或 ThreadLocal。

synchronized 使用三原则

  • 锁范围最小化:只锁 “修改共享资源的代码块”,不锁整个方法;
  • 锁对象全局化:用静态常量(如private static final Object LOCK = new Object())作为锁对象,避免 this 被其他地方使用;
  • 避免嵌套锁:嵌套锁容易导致死锁,就像 “厨师拿着刀等锅,另一厨师拿着锅等刀”,互相卡住。

ThreadLocal 最佳实践

  • 用 static 修饰:确保 ThreadLocal 是类级别的,而非实例级别;
  • 封装工具类:将 ThreadLocal 的 set、get、remove 封装成工具方法,避免重复代码;
  • 配合 try-with-resources:Java 9 + 可用try (ThreadLocal<T> tl = ...) {}自动清理,更低成本避免残留。

无状态设计落地技巧

  • 所有可变数据存储在 Redis、MySQL 等外部系统;
  • Servlet 仅做 “请求解析 - 调用 Service - 返回结果” 三件事,不做任何业务逻辑处理;
  • 用 Spring 的 @Scope ("singleton")(默认)确保 Service 也是无状态的,与 Servlet 保持一致。

六、最后小结

现在看看Spring Cloud Gateway 的源码,其实其底层依然用了 Servlet 的并发模型 —— 只是现在的 “厨房”(Servlet)被装在 Docker 容器里,由 K8s 调度 “厨师”(线程),但核心的并发控制逻辑没变。

云原生时代的 Servlet 并发,有了新的 “升级”:

  • 分布式锁:在多服务器场景下,用 Redis 分布式锁替代 synchronized,解决跨实例共享资源保护问题;
  • 异步 Servlet:结合 Servlet 3.0 异步特性,让线程不再阻塞等待数据库响应,吞吐量提升 3-5 倍;
  • 容器化部署:无状态 Servlet 可快速扩容,K8s 根据并发量自动调整实例数,就像 “餐厅根据客流实时增减分店”。

由此可见Servlet 的并发方案演进,其实是 “软件设计从复杂到简洁” 的缩影 —— 从用多实例规避问题,到用锁保护资源,再到用无状态彻底消除问题,每一步都是对 “简单高效” 的追求。

就像老木匠做家具,早年用复杂的榫卯结构解决稳固问题,后来发现 “少用零件、精准拼接” 才是更优解。并发设计也一样:最好的并发方案,不是用复杂的技术 “堵漏洞”,而是从设计源头 “避免漏洞”。理解这一点,才能在高并发的浪潮中,写出稳定、高效的 Servlet 代码。

http://www.dtcms.com/a/498747.html

相关文章:

  • 快速上手大模型:机器学习3
  • 代替VB6的TWINBASIC ide和开源商业模式分析-VB7
  • 网站图片移动怎么做网页设计图片居右代码
  • 东莞整站优化推广公司找火速用广州seo推广获精准访问量
  • c# .NET core多线程的详细讲解
  • Python机器学习---2.算法:逻辑回归
  • solidity的变量学习小结
  • 【Java 开发日记】MySQL 与 Redis 如何保证双写一致性?
  • 基于知识图谱(Neo4j)和大语言模型(LLM)的图检索增强(GraphRAG)的台风灾害知识问答系统(vue+flask+AI算法)
  • 短剧APP开发性能优化专项:首屏加载提速技术拆解
  • 2025年远程控制软件横评:UU远程、ToDesk、向日葵
  • 前端核心理论深度解析:从基础到实践的关键知识点
  • 合肥官方网站建设有哪些公司
  • 大模型-高效优化技术全景解析:微调 量化 剪枝 梯度裁剪与蒸馏 下
  • 微信个人号开发中如何高效实现API二次开发
  • 网页设计与网站建设实战大全wordpress文章页实现图片幻灯展现
  • Ubuntu22.04 VMware虚拟机文件拖放问题:文字复制正常但文件拖放失效
  • Vue Router 路由守卫钩子详解
  • 开源 Linux 服务器与中间件(三)服务器--Nginx
  • Java 大视界 -- Java 大数据在智能农业无人机植保作业路径规划与药效评估中的应用
  • 【OpenGL】模板测试(StencilTest)
  • 文本描述驱动的可视化工具在IDE中的应用与实践
  • C#程序实现将MySQL的存储过程转换成Oracle的存储过程
  • IDEA 中 Tomcat 部署 Java Web 项目
  • 全景网站模版校园微网站建设方案ppt模板
  • 东莞公司网站建设公司哪家好制作网站链接
  • 【Linux】Socket编程UDP
  • “桌面自动化”解救“浏览器自动化”受阻(反爬虫检测)(pywinauto、pyautogui、playwright)
  • 线程安全集合源码速读:Hashtable、Vector、Collections.synchronizedMap
  • 大文件上传与文件下载