粗略理解并避免并发在Web系统中的问题

本文通过两个具体案例探讨了Web系统中的并发问题,包括变量作用域类型引发的问题及多线程同步问题,并提供了相应的解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

初步理解并避免并发在Web系统中的问题

对于我们每个Web系统来说,我们的系统会被来自五湖四海的用户并发的访问(同时访问),并且很有可能这些用户并发访问的是Web应用中的同一个Servlet。
Servlet容器就会为每个请求分配一个工作线程,这些工作线程并发的来执行同一个Servlet对象的service()方法。

第一种并发问题:变量作用域类型导致的并发问题

package test1;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebServlet(name = "HelloServlet1")
public class HelloServlet1 extends HttpServlet {
    private String username = null;
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doGet(request,response);
    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        username = request.getParameter("username");//把username请求参数赋值给username变量

        try {
            Thread.sleep(2000);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        response.setContentType("text/html;charset=UTF-8");
        response.getWriter().write("username"+": "+username);
    }
}

xml映射配置为/helloservlet1
当我们配置好环境之后,打开两个浏览器同时输入两个URL:
+ http://localhost:8080/helloservlet1?username=Lucy

同时不一定要绝对的同时,时间相近就可以了(一个一个进网址,然后刷新也是可以的)

结果如下:


我们可以发现:请求参数明明是Mark,为什么返回的却是Lucy呢?

我们应该关注HTTP请求和线程,以及HTTP请求和username变量之间的对应关系。
+ 一个HTTP请求对应着一个工作线程
+ 一个HTTP请求对应着一个username变量

结论:一个工作线程一个要对应一个username变量。所以应该吧username变量定义为Service方法内部的局部变量:

 protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String username = null;
        username = request.getParameter("username");
        try {
            Thread.sleep(2000);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        response.setContentType("text/html;charset=UTF-8");
        response.getWriter().write("username"+": "+username);
    }

我们再来详细分析下刚刚的例子中时序操作过程:

时刻点响应第一个工作请求的工作线程响应第二个工作请求的线程
T1请求读取username = Mark
T2请求读取username = Lucy
T3将实例变量username赋值为Mark
T4将实例变量username赋值为Lucy
T5Sleep(3000)
T6Sleep(3000)
T7username = Lucy
T8username = Lucy

这里的时间很短,类似于CPU中的时间片我们近似认为操作是并发的

第二种并发问题:多线程同步带来的并发问题

package test2;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebServlet(name = "HelloServlet2")
public class HelloServlet2 extends HttpServlet {
    private int number=100;
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doGet(request,response);
    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        int increase= 1;
        increase = Integer.parseInt(request.getParameter("increase"));
        response.getWriter().write(number + " + " + increase + " = ");
        try {
            Thread.sleep(2000);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        number += increase;
        response.getWriter().write(number + "");
    }
}

当我们配置好环境后同时打开浏览器输入两个URL:
+ http://localhost:8080/helloservlet2?increase=100

结果如下:我们会发现100+100=400

发生错误的原因:number的初始值为100,当它先接受到第二个请求increase=200之后它将会变为300,那么对于第一个请求的number来说则变成了number(300)+100=number(400)。
+ 一个HTTP请求对应的是一个工作线程。
+ 但是所有的HTTP请求对应的是同一个number变量。

解决方法:利用Java的同步机制

package test2;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebServlet(name = "HelloServlet2")
public class HelloServlet2 extends HttpServlet {
    private int number=100;
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doGet(request,response);
    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        int increase= 1;
        increase = Integer.parseInt(request.getParameter("increase"));
        synchronized (this) {//同步代码版块
            response.getWriter().write(number + " + " + increase + " = ");
            try {
                Thread.sleep(2000);
            } catch (Exception ex) {
                ex.printStackTrace();
            }
            number += increase;
            response.getWriter().write(number + "");
        }
    }
}

这样就可以确保在任意的一个时刻,只允许一个工作线程执行servlet的service()方法,从而确保两个浏览器都可以得到当前匹配的响应结果。

SingleThreadModel接口(不推荐使用)

这是一个废弃的接口,该接口是专门用来避免并发问题而提供的。可以解决上述的两种并发问题,但均没有符合本意,仅仅是解决而已。
工作原理:
+ 在任意时刻,只允许有一个工作线程执行service()方法,如果有多可客户访问该servlet,那么这些客户的请求即将被放到一个等待队列中,容器会一次来响应等待队列中的每个客户的请求。

这种方法实际上禁止了多个客户端对同一个servlet的并发访问。

  • 第一个例子如果使用SingleThreadModel接口,则还是没有改变全局变量为局部变量,这与实际应用需求不合。
  • 第二个例子如果使用SingleThreadModel接口,对于两次同时访问servlet,尽管对于单次的客户请求可以返回正确的结果,但是无法用于累计客户多次请求的加法操作,使得实例失去意义。

所以在实际开发中使用SingelThreadModel不能很好的解决问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值