DRP(四)——线程安全的Servlet

多线程的Seervlet模型

Servlet规范定义,在默认情况下(Servlet不是在分布式的环境中部署),Servlet容器对声明的每一个Servlet,只创建一个实例。如果有多个客户请求同时访问这个Servlet,Servlet容器如何处理这多个请求呢?答案是采用多线程,Servlet容器维护一个线程池来服务请求。线程池实际上是等待执行代码的一组线程,这些线程叫做工作者线程。Servlet容器使用一个调度者线程来管理工作者线程。当容器接收到一个访问Servlet的请求,调度者线程从线程池中选取一个工作者线程,将请求传递给该线程,然后由这个线程执行Servlet的service方法,如下图:

当容器接收到另一个请求时,调度者线程将从池中选取另一个线程来服务新的请求。

由于Servlet容器采用单实例多线程的方式(这是Servlet容器默认的行为),最大限度地减少了产生Servlet实例的开销,显著地提升了对请求的响应时间。对于Tomcat,可以在server.xml文件中<Connector>元素中设置线程池中线程的数目。

变量的线程安全

<span style="font-size:24px;">package org.sunxin.ch02.servlet;

import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class WelcomeServlet extends HttpServlet{
	private String greeting;
	String username="";

	public void init(){
		greeting = getInitParameter("greeting");
	}

	public void doGet(HttpServletRequest req,HttpServletResponse resp) throws ServletException,IOException{
		req.setCharacterEncoding("gb2312");
		username= req.getParameter("username");
		String welcomeInfo=greeting + "," + username;

		resp.setContentType("text/html");

		PrintWriter out = resp.getWriter();

		out.println("<html><head><title>");
		out.println("Welcome page");
		out.println("</title><head>");
		out.println("<body>");
		out.println(welcomeInfo);
		out.println("</body></html>");
		out.close();

	}

	public void doPost(HttpServletRequest req,HttpServletResponse resp) throws ServletException,IOException{
		doGet(req,resp);
	}
}</span><span style="font-size: 18px;">
</span>

这段代码主要是向用户显示欢迎信息,然而这段代码有一个潜在的线程安全问题。当用户A和B同时访问这个Servlet时,会出现:

(1)Servlet容器分配一个工作者线程T1来服务用户A的请求,分配另一个工作者线程T2服务用户B的请求。

(2)操作系统首先调度T1运行。

(3)T1执行代码后,从请求对象中获取用户的姓名,保存到变量user中,现在user的值是A。

(4)当T1试图执行下面的代码时,时间片到期,操作系统调度T2运行。

(5)T2执行代码后,从请求对象中获取用户的姓名,保存到变量user中,现在user的值是A.

(6)T2继续执行后面的代码,向用户B输出“Welcome you ,B”。

(7)T2执行完毕,操作系统重新调度T1执行,T1从上次执行的代码中断处继续往下执行,因为这个时候user变量的值已经变成了B,所以T1向用户A发送“Welcome you,B”。

解决这个问题,可以采取两种方式:第一种是将username定义为本地变量,

<span style="font-size:24px;">package org.sunxin.ch02.servlet;

import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class WelcomeServlet extends HttpServlet{
	private String greeting;

	public void init(){
		greeting = getInitParameter("greeting");
	}

	public void doGet(HttpServletRequest req,HttpServletResponse resp) throws ServletException,IOException{
		req.setCharacterEncoding("gb2312");
		String  username= req.getParameter("username");
		String welcomeInfo=greeting + "," + username;

		resp.setContentType("text/html");

		PrintWriter out = resp.getWriter();

		out.println("<html><head><title>");
		out.println("Welcome page");
		out.println("</title><head>");
		out.println("<body>");
		out.println(welcomeInfo);
		out.println("</body></html>");
		out.close();

	}

	public void doPost(HttpServletRequest req,HttpServletResponse resp) throws ServletException,IOException{
		doGet(req,resp);
	}

}</span>

第二种方式是同步doGet()方法

<span style="font-size:24px;">package org.sunxin.ch02.servlet;

import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class WelcomeServlet extends HttpServlet{
	private String greeting;
	String  username="";
	public void init(){
		greeting = getInitParameter("greeting");
	}

	public synchronized void doGet(HttpServletRequest req,HttpServletResponse resp) throws ServletException,IOException{
		req.setCharacterEncoding("gb2312");
		username= req.getParameter("username");
		String welcomeInfo=greeting + "," + username;

		resp.setContentType("text/html");

		PrintWriter out = resp.getWriter();

		out.println("<html><head><title>");
		out.println("Welcome page");
		out.println("</title><head>");
		out.println("<body>");
		out.println(welcomeInfo);
		out.println("</body></html>");
		out.close();

	}

	public void doPost(HttpServletRequest req,HttpServletResponse resp) throws ServletException,IOException{
		doGet(req,resp);
	}
}</span>

因为使用了同步,就可以防止多个线程同时调用doGet()方法,也就避免了在请求处理过程中,user实例变量被其他线程修改的可能。不过对doGet()方法使用同步,意味着访问同一个Servlet的请求将排队,一个线程处理完请求后,才能执行另一个线程,这将严重影响性能,所以我们几乎不采用这种方式。

举例:

在Tomcat文档中描述过的“Connection ClosedException”,代码如下

package org.sunxin.ch02.servlet;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.Statement;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.sql.DataSource;
public class testServlet extends HttpServlet {
    DataSource ds = null;
    public void init(){
        try{
            Context ctx = new InitialContext();
            ds=(DataSource)ctx.lookup("java:comp/env/jdbc/bookstore");
        }catch(Exception e){
            e.printStackTrace();
        }
    }

    public void service (HttpServletRequest request, HttpServletResponse response) throws ServletException,java.io.IOException{
        Connection conn = null;
        Statement stmt = null;
        ResultSet rs = null;
        try{
            conn=ds.getConnection();//从连接池中得到连接
            stmt = conn.createStatement();
            rs = stmt.executeQuery("....");
            //.....省略
            rs.close();
            stmt.close();
            conn.close();
        }catch(Exception e){
            System.out.println(e);
        }finally{
            if(rs != null){
                try{
                    rs.close();
                }catch(Exception e){
                    System.out.println(e);
                }
            }

            if(stmt != null){
                try{
                    stmt.close();
                }catch(Exception e){
                    System.out.println(e);
                }
            }

            if(conn != null){
                try{
                    conn.close();
                }catch(Exception e){
                    System.out.println(e);
                }
            }

        }
    }
}

这段代码导致异常产生过程如下:

(1)当服务一个请求的线程T1运行时,从连接池中得到一个数据库连接

(2)在线程T1中,当执行完数据库访问操作后,关闭数据库

(3)此时,操作系统调度另一个线程T2运行

(4)T2为另一个访问该Servlet的请求服务,从连接池中得到一个数据库连接,而这个连接郑浩是刚才在T1线程中调用close()方法后,放回池中的连接

(5)此时,操作系统调度线程T1运行

(6)T1继续执行后面的代码,在finally语句中,再次关闭数据库连接。要注意,调用Connection对象后的close()方法只是关闭数据库连接,而对象本身并不为空,所以finally语句中的关闭操作才又一次执行

(7)此时,操作系统调度线程T2运行。

(8)线程T2视图使用数据库连接,但却失败了,因为T1关闭了该连接

(有篇文章推荐:java.sql.Connection的close方法究竟干了啥)

要避免上述的情况,就要求我们正确的编写代码,在关闭数据库对象后,将该对象设为null。正确代码如下:

<span style="font-size:24px;">package org.sunxin.ch02.servlet;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.Statement;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.sql.DataSource;
public class testServlet extends HttpServlet {
    DataSource ds = null;
    public void init(){
        try{
            Context ctx = new InitialContext();
            ds=(DataSource)ctx.lookup("java:comp/env/jdbc/bookstore");
        }catch(Exception e){
            e.printStackTrace();
        }
    }

    public void service (HttpServletRequest request, HttpServletResponse response) throws ServletException,java.io.IOException{
        Connection conn = null;
        Statement stmt = null;
        ResultSet rs = null;
        try{
            conn=ds.getConnection();//从连接池中得到连接
            stmt = conn.createStatement();
            rs = stmt.executeQuery("....");
            //.....省略
            rs.close();
            rs=null;
            stmt.close();
            stmt=null;
            conn.close();//连接被放回连接池
            conn=null;   //确保我们不会关闭连接两次
        }catch(Exception e){
            System.out.println(e);
        }finally{
            if(rs != null){
                try{
                    rs.close();
                }catch(Exception e){
                    System.out.println(e);
                }
            }

            if(stmt != null){
                try{
                    stmt.close();
                }catch(Exception e){
                    System.out.println(e);
                }
            }

            if(conn != null){
                try{
                    conn.close();
                }catch(Exception e){
                    System.out.println(e);
                }
            }
        }
    }
}</span>

属性的线程安全

在Servlet中,可以访问保存在SeervletContext,HttpSession和ServletRequest对象中的属性,这三种对象都提供了getAttribute()和setAttribute()方法用于读取和设置属性。

ServletContext

ServletContext对象可以被Web应用程序中所有的Servlet访问,多个线程可以同时在Servlet上下文中设置或读取属性,这将导致存储数据的不一致。例如:有两个Servlet,LoginServlet和DisplayUsersServlet。LoginServlet负责验证用户,并将用户名添加到保存在Servlet上下文中的列表中,当用户退出的时候,LoginServlet从列表中删除用户名。代码如下:

LoginServlet:

<span style="font-size:24px;">public void service (HttpServletRequest request, HttpServletResponse response) throws ServletException,java.io.IOException{
        String username =  //验证用户
        if(authenticated){
            List list= (List)getServletContext().getAttribute("usersList");
        }else if(logout){
            //从用户列表中删除用户名
            List list = (List)getServletContext().getAttribute("usersList");
            list.remove(username);
        }
    } </span>

DisplayUsersServlet:

<span style="font-size:24px;">public void service (HttpServletRequest request, HttpServletResponse response) throws ServletException,java.io.IOException{
        PrintWriter out = res.getWriter;
        List list = (List)getServletContext().getAttribute("usersList");
        int count = list.size();
        out.println("<html><body>");
        for(int i=0; i<count;i++){
            out.println(list.get(i)+"<br>");
        }
        out.println("</body></html>");
        out.close();
    } </span>

usersList属性在任何时候都可以被所有的Servlet访问,因此,当DisplayUsersServlet在循环输出用户名的时候,LoginServlet可能从用户列表中删除了一个用户名,这将导致抛出IndexOutOfBoundsExcetption异常。ServletContext属性的访问不是线程安全的,为了避免出现问题,可以对用户列表的访问进行同步或者对用户列表产生一个拷贝。

HttpSession

用户可以打开多个同属于一个进程的浏览器窗口,在这些窗口中的访问请求,属于同一个Session,为了同时处理多个这样的请求,Servlet容器会创建多个线程,而在这些线程中,就可以同时访问到Sesion对象的属性。

举一个购物车例子,如果用户一个浏览器中删除一个条目,同时又在另一个浏览器窗口中查看购物车中的所有条目,这将导致抛出IndexOutOfBoundsExcetption,要避免这个问题,而已对Session的访问进行同步。

ServletRequest

因为Servlet容器对它所接收到的每一个请求,都创建一个新的ServletRequest对象,所以ServletRequest对象只在一个线程中被访问。因为只有一个线程服务请求,所以请求对象的属性访问是线程安全的。

总结

真是大开眼界,从中学到很多,分享给大家。

时间: 2024-10-06 17:23:35

DRP(四)——线程安全的Servlet的相关文章

Java多线程(四) 线程池

一个优秀的软件不会随意的创建很销毁线程,因为创建和销毁线程需要耗费大量的CPU时间以及需要和内存做出大量的交互.因此JDK5提出了使用线程池,让程序员把更多的精力放在业务逻辑上面,弱化对线程的开闭管理. JDK提供了四种不同的线程池给程序员使用 首先使用线程池,需要用到ExecutorService接口,该接口有个抽象类AbstractExecutorService对其进行了实现,ThreadPoolExecutor进一步对抽象类进行了实现.最后JDK封装了一个Executor类对ThreadP

当多个客户请求一个servlet时,引擎为每个客户启动一个线程,那么servlet类的成员变量被所有的线程共享?

因为servlet的实现是单例,多线程也就是说,N个客户端请求同一个servlet,他们所请求的是同一个对象,成员变量是属于这个对象的,因此成员变量也被共享了因此在servlet编程中,无状态的servlet(就是不写属性,所以变量都在dopost或者doget里面)是线程安全的,否则,由于会共享到成员变量,因此就有可能不是线程安全的.

tomcat线程池与servlet

①客户端向服务器端发出请求: ②这个过程比较重要,这时候Tomcat会创建两个对象:HttpServletResponse和HttpServletRequest.并将它们的引用(注意:是引用!)传给刚分配的线程中: ③线程开始着手接洽servlet: ④servlet根据传来的是GET和POST,分别调用doGet()和doPost()方法进行处理: ⑤和⑥servlet将处理后的结果通过线程传回Tomcat,并在之后将这个线程销毁或者送还线程池: ⑦Tomcat将处理后的结果变成一个HTTP响

Java多线程(四) —— 线程并发库之Atomic

一.从原子操作开始 从相对简单的Atomic入手(java.util.concurrent是基于Queue的并发包,而Queue,很多情况下使用到了Atomic操作,因此首先从这里开始). 很多情况下我们只是需要一个简单的.高效的.线程安全的递增递减方案.注意,这里有三个条件: 简单,意味着程序员尽可能少的操作底层或者实现起来要比较容易: 高效意味着耗用资源要少,程序处理速度要快: 线程安全也非常重要,这个在多线程下能保证数据的正确性. 这三个条件看起来比较简单,但是实现起来却难以令人满意. 通

java多线程学习(四)——线程的交互

线程交互中用到的三个基本函数: void notify():唤醒在此对象监视器上等待的单个线程. void notifyAll():唤醒在此对象监视器上等待的所有线程. void wait();导致当前的线程等待,直到其他线程调用此对象的notify()或者notifyAll()方法. void wait(long timeout);wait()的重载版本,同样导致当前线程等待,直到其他线程调用此对象的notify()或者notifyAll()方法,或者等待超过指定的时间后不再等待. void

C# 线程--第四线程实例

概述 在前面几节中和大家分享了线程的一些基础使用方法,本章结合之前的分享来编写一些日常开发中应用实例,和编写多线程时一些注意点.如大家有好的实例也欢迎分享.. 应用实例 应用:定时任务程序 场景:系统中常常会有一些需要定时去循环执行的存储过程或方法等,这时就出现了定时任务小程序. 模型:查询需定时执行的计划任务-->插入线程池-->执行任务 static void MainMethod() { Thread thead; thead = new Thread(QueryTask); thead

重学JAVA基础(四):线程的创建与执行

1.继承Thread public class TestThread extends Thread{ public void run(){ System.out.println(Thread.currentThread().getName()); } public static void main(String[] args) { Thread t = new TestThread(); t.start(); } } 2.实现Runnable public class TestRunnable

多线程学习笔记四--------------线程间通信问题

线程间通信问题: 多个线程在处理同一资源,但是任务却不同: java中将资源共享的方法(思路): 1.方法或者变量静态化---->静态化后,在类加载的时候,会将其加载到内存的方法区进行共享 2.单例设计模式---->保证只对一个实例进行操作. 3.将资源作为操作该资源的类的构造函数的参数,这样可以保证此类的多个对象在使用该资源的时候使用该资源的同一个实例. 现在我们要用第三种方法来进行线程间的通信. 情景:两个线程 ,一个负责输入,一个负责输出:共同处理一个资源. public class T

实验十四 线程设计

1.源程序: package shiyan14; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.util.Timer; import java.util.TimerTask; import javax.swing.JButton; impor