多线程的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对象只在一个线程中被访问。因为只有一个线程服务请求,所以请求对象的属性访问是线程安全的。
总结
真是大开眼界,从中学到很多,分享给大家。