Topic: 求多线程的servlet在很多人同时访问的时候会竞争现像的解决办法

  Print this page

1.求多线程的servlet在很多人同时访问的时候会竞争现像的解决办法 Copy to clipboard
Posted by: zenzuguo
Posted on: 2003-10-27 09:08

求多线程的servlet在很多人同时访问的时候会竞争现像的解决办法

在本系统中有个后台管理入口servlet:entry,每一个后台管理者在操作系统后台时系统都要是通过这个servlet根据用户的request来response给用户相应的页面。而且通过entry调用其它的JAVA BEAN来动态生成一个XML树(xmlData)+xsl(xslData,其中存放的是一个模板)形式的页面,用XML+XSL来替代静态的HTML。另外包括对用户的权限,IP,日志操作等都会通过这个servlet来调用相应的JAVA BEAN来完成。
Entry的源代码如下
package com.strong.admin;

import com.strong.data.ConnectionManager;
import com.strong.data.DataOpt;
import com.strong.constant.ManagePanelType;
import com.strong.constant.OpType;
import com.strong.utils.Pagination;
import com.strong.utils.DataEditor;
import com.strong.utils.UploadKit;
import com.strong.utils.ParaHandler;
import com.strong.utils.StringKit;
import com.strong.utils.PagiExtAssistant;
import com.strong.admin.TableLoader;
import com.strong.admin.DataHandler;
import com.strong.admin.XmlHandler;
import com.strong.admin.PagiExtUser;
import com.strong.admin.PagiExtLib;
import com.strong.admin.Locator;
import com.strong.usr.CheckRight;
import com.strong.usr.InsertLog;
import com.strong.sys.CheckIP;
import com.strong.upload.*;

import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
import java.util.*;
import java.sql.*;
import java.net.InetAddress;

import javax.xml.*;
import javax.xml.transform.*;
import javax.xml.transform.stream.*;

import org.jdom.JDOMException;

/**
* <p>Title: 后台管理入口类</p>
* <p>Description: 根据参数加载不同模板</p>
* <p>Copyright: Copyright Coffee 2003</p>
* <p>Company: </p>
* @author
* @version 1.0
*/
public class entry
extends HttpServlet{
private static final String CONTENT_TYPE = "text/html; charset=GBK";
private String xmlData;
private String xslData;
private String managePanel; //加载后台面板类别的控制参数
private String OpType; //加载具体面板的控制参数
private HashMap parameters;
private HttpSession session;
private ServletConfig config;
private String channelIdChars;

public void init(ServletConfig config) throws ServletException {
super.init(config);
this.config = config;
ConnectionManager.sysPath = getServletContext().getRealPath("/");
}

//----------------------------------------------------------------------
public synchronized void doGet(HttpServletRequest request, HttpServletResponse response) throws
ServletException, IOException {
response.setContentType(CONTENT_TYPE);
session = request.getSession();
if (session.getAttribute("channelIdChars") != null) {
this.channelIdChars = (String) session.getAttribute("channelIdChars");
}
else {
this.channelIdChars = new String();
}

// 接收控制参数
//
managePanel = (request.getParameter("mp") != null) ?
request.getParameter("mp") : "login";
managePanel = (request.getParameter("OpType") != null) ?
request.getParameter("OpType") : managePanel;

ParaHandler paraHandler = new ParaHandler(request);
if (paraHandler.getParams() != null) {
this.parameters = paraHandler.getParams();
}
else {
this.parameters = new HashMap();
this.parameters.put("mp", "login");
}

// 日志插入
//
InsertLog insertLog = new InsertLog(request, session);
insertLog.logInsert();

// 远程IP功能检测
//
CheckIP CheckIP = new CheckIP();
boolean isRight = CheckIP.checkIpAddr(response, request);
if (!isRight){
managePanel="NoRight";
}
//*********************************
// 权限验证
//
if (managePanel.equals("loginout")) {
session.removeAttribute("userName");
session.removeAttribute("roleChars");
session.removeAttribute("channelIdChars");
managePanel="login";
}

if (!managePanel.equals("login")) { //如果不为登录模板
if (session.getAttribute("userName") == null) {
managePanel="login";
}
}
try {
if (request.getParameter("OpType") != null && session.getAttribute("roleChars")!=null) {
CheckRight checkRight = new CheckRight();
String opType = request.getParameter("OpType");
String roleChars = session.getAttribute("roleChars").toString();
String channelChars;
if (session.getAttribute("channelIdChars") == null) {
channelChars = "";
}
else {
channelChars = session.getAttribute("channelIdChars").toString();
}
if (!checkRight.CheckRight(opType, "", roleChars, channelChars)) {
managePanel="NoRight";
}
if (parameters.get("DirAdmin")!=null && parameters.get("DirAdmin").equals("del")) {
SmartUpload mySmartUpload = new SmartUpload();
mySmartUpload.initialize(config);
mySmartUpload.DelDir((String)parameters.get("delPathName"));
}
}
}
catch (Exception ex) {
System.out.println(ex.getMessage());
}

// 加载模板数据
//
LoadTemplate loadTemplate = new LoadTemplate("wsm_ManagerTemplates",
"templateName",
"templateContext");
xmlData = "<?xml version=\"1.0\" encoding=\"GBK\"?><page></page>";
xslData = loadTemplate.getOracleTemplate(managePanel);

// 模板列表显示
//
String curPage = (request.getParameter("curPage") != null) ?
request.getParameter("curPage") : "1";
String pageSize = (request.getParameter("pageSize") != null) ?
request.getParameter("pageSize") : "10";

// 根据功能处理XML
//
TableLoader tableLoader = new TableLoader(managePanel, session);

// 如果是更新...
//
if (managePanel.toLowerCase().indexOf("edit") != -1) {
DataEditor dataEditor = new DataEditor(tableLoader.getTableName(),
tableLoader.getKeyId(),
tableLoader.getFieldString());
xmlData = dataEditor.build(xmlData, parameters);
}

// 如果是列表...
//
PagiExtUser pagiExtUser = new PagiExtUser();
if (!pagiExtUser.isUsePagiExt(managePanel) &&
managePanel.toLowerCase().indexOf("list") != -1) {
Pagination pagi = new Pagination(tableLoader.getTableName(),
tableLoader.getKeyId(),
tableLoader.getFieldString(),
Integer.parseInt(pageSize));
xmlData = pagi.build(xmlData, curPage, managePanel,
tableLoader.getStaticWhere(),
tableLoader.getWhereClause(),
request.getParameter("id"));
}
else if (pagiExtUser.isUsePagiExt(managePanel) &&
managePanel.toLowerCase().indexOf("list") != -1) {
PagiExtLib lib = new PagiExtLib(managePanel, parameters, session);
Pagination pagi = new Pagination(lib.getDataSource(), lib.getKeyID(),
Integer.parseInt(pageSize));
pagi.setCountSql(lib.getCountSQL());
pagi.setIndexSql(lib.getIndexSQL());
xmlData = pagi.build(xmlData, curPage, managePanel, lib.getCountSQL(),
lib.getOtherSQLs());
}
// XML数据处理
//
try {
XmlHandler xmlHandler = new XmlHandler(xmlData, request, config,
channelIdChars);
xmlData = xmlHandler.build(managePanel, parameters);
if (pagiExtUser.isUsePagiExt(managePanel)) {
PagiExtAssistant ass = new PagiExtAssistant(xmlData, parameters);
xmlData = ass.build();
}
}
catch (Exception ex) {
System.out.println(ex.getMessage());
}

//模板转换开始
//
Convert convert = new Convert(xmlData, xslData);
convert.start(response);
}

public synchronized void doPost(HttpServletRequest request, HttpServletResponse response) throws
ServletException, IOException {
response.setContentType(CONTENT_TYPE);
// 如果是添加...
//
String contentType = request.getContentType(); //表单编码类型
String opType = request.getParameter("OpType");
String DirAdmin = request.getParameter("DirAdmin"); //进行文件管理内的目录操作
HashMap formData = new HashMap();
session = request.getSession();
// 日志插入
//
InsertLog insertLog = new InsertLog(request, session);
insertLog.logInsert();

// 远程IP功能检测
//
CheckIP CheckIP = new CheckIP();
boolean isRight = CheckIP.checkIpAddr(response, request);
if (!isRight){
managePanel="NoRight";
}

/**********************************/

// 权限验证
//
if (!managePanel.equals("login")) { //如果不为登录模板
if (session.getAttribute("userName") == null) {
managePanel="login";
}
}
try {
if (request.getParameter("OpType") != null) {
if (session.getAttribute("userName") != null) {
CheckRight checkRight = new CheckRight();
String roleChars = session.getAttribute("roleChars").toString();
String channelChars;
if (session.getAttribute("channelIdChars") == null) {
channelChars = "";
}
else {
channelChars = session.getAttribute("channelIdChars").toString();
}
if (!checkRight.CheckRight(opType, "", roleChars, channelChars)) {
managePanel="NoRight";
}
}
}
}
catch (Exception ex) {
System.out.println(ex.getMessage());
}
if (contentType.indexOf("multipart/form-data") == -1) { //普通表单数据处理
Enumeration emumNames = request.getParameterNames();
String myKeys, myVals;

while (emumNames.hasMoreElements()) {
myKeys = (String) emumNames.nextElement();
myVals = "";
for (int i = 0; i < request.getParameterValues(myKeys).length; i++) {
myVals += StringKit.getGBString(request.getParameterValues(myKeys)[i]) +
",";
}
myVals = myVals.substring(0, myVals.length() - 1);
formData.put(myKeys, myVals);
}
}
//--------------------------------------------------------------------------
else if (contentType.indexOf("multipart/form-data") != -1) { //二进制上传表单处理
if (request.getParameter("FileAdmin") != null &&
request.getParameter("FileAdmin").equals("Add")) {
UploadKit upload = new UploadKit(opType,
getServletContext().
getRealPath(
request.getParameter("PathName")));
upload.setPath(getServletContext().getRealPath(request.getParameter(
"PathName")));
String url = upload.startUp(request);
}
else {
UploadKit upload = new UploadKit(opType,
getServletContext().getRealPath("/"));
String url = upload.start(request);
formData = upload.getFormData();
formData.put("postfix", "image");
formData.put("url", url);
}
}
//--------------------------------------------------------------------------
try {
DataHandler handler = null;
handler = new DataHandler(formData, opType, response, session, config,
request);
handler.process();
}
catch (Exception ex) {
System.out.println(ex.getMessage());// null Exception here
}
//--------------------------------------------------------------------------
Locator locator = new Locator(); //
String url = locator.getURL(opType);
if (url.equalsIgnoreCase("not_supplied")) {
response.sendRedirect(request.getHeader("Referer"));
return;
}
else {
url = "entry" + url;
response.sendRedirect(url);
return;
}
}

//----------------------------------------------------------------------
public void destroy() {
super.destroy();
}
}
由于这是多线程的servlet,而我们在起初设计时将与用户相关的数据设计成了实例变量,同步问题也没有处理好。因此在多人并发访问时,出现多个人共享一个service时出现了竞争现象,结果前一个用户的数据被后一个访问用户的数据给覆盖了结果用户得到错误的页面。实例变量如下
private String xmlData;//存放XML树(与用户相关的)
private String xslData;//存放XSL数据(与用户相关的,而且是个大数据)
//xmlData和xslData两个加起来大概有50K左右

private String managePanel; //加载后台面板类别的控制参数(与用户相关的)
private String OpType; //加载具体面板的控制参数(与用户相关的)
private HashMap parameters;// 与用户相关的参数(与用户相关的)
private HttpSession session;//存放用户session值(与用户相关的)
private ServletConfig config;//ServletConfig参数
private String channelIdChars;//用户频道权限(与用户相关的)

经过一些研究后我们准备这样修改我们的程序:
首先将session放到程序方法中去,在用它的时候再声明它。这样它就不是一个实例变量了。每个用户都可以有一个自己单独的session。我们就可以将一些和用户相关的资料存放在其中。如managePanel、OpType、parameters、channelIdChars。在这里,ServletConfig这个变量是每个servlet的都相同,因此可以是个实例变量。
但是还有如下这个问题:
1、  xmlData和xslData这两个数据是比较大的,存放着将要response给用户的页面。它们加起来大概50K左右。由于这是一个超大访问量的系统。如果存放到session中的话可能过大。如果放到数据库中去的话意味着,用户的每次点击都将引起一次数据库的操作。如此频繁地访问数据库,也同样是不可取的。如果考虑将它们封装到一个JAVA BEAN中去,再让那个JAVA BEAN同步,不知道是否可行,而性能上也同样会降下来。不知道这里该如何处理这两个大数据????

2.Re:求多线程的servlet在很多人同时访问的时候会竞争现像的解决办法 [Re: zenzuguo] Copy to clipboard
Posted by: yiqiang
Posted on: 2003-10-28 11:00

用sington模式不就行了吗?

3.Re:求多线程的servlet在很多人同时访问的时候会竞争现像的解决办法 [Re: zenzuguo] Copy to clipboard
Posted by: zenzuguo
Posted on: 2003-10-28 11:12

首先谢谢yiqiang的回复.

singleton模式能解决的是保证在全局只有一个实例.这里要解决的是如果消除由于存在共享数据而引发的竞争现象.同时满足超大访问量.而且这存在两个大数据.问题就出在如何处理这两大数据.

4.Re:求多线程的servlet在很多人同时访问的时候会竞争现像的解决办法 [Re: zenzuguo] Copy to clipboard
Posted by: jfml
Posted on: 2003-10-28 11:20

yiqiang同志,不了解就不要不负责任地发言
1.Servlet的实例本来就是由服务器容器控制的,生命周期是从第一次请求或服务器启动(可通过设置web.xml修改)开始,到服务器关闭结束,该Servlet一直是“单实例”状态
2.Servlet的实例是非线程安全的,而加上SingleThreadModel或者Synchonized方法或代码块会使性能急剧下降,因此公认的方法是不要在Servlet中使用成员变量,只能如此而已

5.Re:求多线程的servlet在很多人同时访问的时候会竞争现像的解决办法 [Re: zenzuguo] Copy to clipboard
Posted by: yiqiang
Posted on: 2003-10-28 13:32

看来我真是答非所问了,非常抱歉,阅题不慎。
楼主的问题看来主要是在多个并发请求的时候出现数据交叉混乱,就相抢购货品一样出现了无秩序状态。如果楼主的问题是出在这里的话,我觉得用户在访问这个servlet的时候应该有队列控制器来,同步(synchonized)协调所有的访问请求,当然这可能会带来一定的性能损失。我这里有个例子不知道是不是合适:

在作者所从事的项目中,要开发一个应用服务器,实现如下所述的功能:能够高效的处理来自多个客户端的并发请求。为了简化同步控制,分离并发逻辑和业务逻辑,我们采用了Active Object模式,具体的实现可以参见作者的另外一篇文章:《构建Java并发模型框架》(http://www-900.ibm.com/developerWorks/cn/java/l-multithreading/index.shtml)。该设计中有一个核心部件ActiveQueue用于存放客户的请求。为了能够做到应用服务器的负载控制,我们对于ActiveQueue的大小进行了限制,如果当前的客户请求数量已经达到这个限制,就让后继的请求等待,具体的代码实现片断如下(为了简洁起见,省略了其他无关的代码):

    class ActiveQueue {
      ...
public synchronized void enqueue(ClientRequest cr) throws InterruptedException
{
while(isFull( ) ){ // ActiveQueue的大小达到上限
wait();
}
    // 把用户请求添加到处理队列中
notifyAll();
}
  ...
}



该方法刚开始工作的很好,但是随着项目开发的进展,在随后的测试中我们发现了两个较为严重的问题:1、当并发请求的客户端很多时,会造成某些客户端等待的时间过长,对于客户端的使用者来说非常不友好;2、由于系统中应用服务器的其他方面的异常同样会造成客户端请求的永久等待比如:应用服务器在处理完客户端请求后,由于异常没有正确的调用相应notify方法。所以为了改善程序的用户友好性以及健壮性,我们决定采用带有超时语意的wait方法。该方法的原型声明如下:

  public final void wait (long millisecTimeout) throws InterruptedException;

  使用该方法改进后的代码实现片断如下:

    class ActiveQueue {
      ...
public synchronized void enqueue(ClientRequest cr, long timeout)
            throws InterruptedException
{
while(isFull( ) ){ // ActiveQueue的大小达到上限
   wait(timeout);
      // 语意模糊性体现于此,当wait返回时
// 我们无法区分是由于notify的通知还是超时触发的
// 因此我们无法做出适当的处理
}
    // 把用户请求添加到处理队列中
notifyAll();
}
  ...
  }



可以看出,简单的使用一个具有超时语意的wait方法是不可行的,原因就在于wait方法超时语意的模糊性。在下面的小节会先给出一个初步的解决方案,随后我们将使用模式对于该方案进行重构从而构造出一个比较通用的方案。

初步解决方案

我们的初步解决方案采用了Doug Lea(Doug Lea为对象并发领域世界级的专家)给出的一个显式的判断算法(关于该算法更加详细、深入的论述请参考参考文献〔1〕),通过该算法来辨别是否已经超时。采用该算法的解决方案的代码实现片断如下:

  class ActiveQueue {
      ...
public synchronized void enqueue(ClientRequest cr, long timeout)
throws InterruptedException, TimeoutException
{
if (isFull ()) { // 判断队列是否为满
long start = System.currentTimeMillis ();
long waitTime = timeout;
for (;Wink { // 一直等待到队列不满被notify通知或者超时
wait (waitTime);
if (isFull ()) { //重新判断队列是否为满
long now = System.currentTimeMillis ();
long timeSoFar = now - start; // 队列仍然为满,计算已经等待的时间
if (timeSoFar >= msecTimeout) // 如果超时,抛出TimeoutException异常
       throw new TimeoutException ();
else // 没有超时,计算还要等待的时间
waitTime = timeout - timeSoFar;
}
else // 被notify唤醒,并且队列不为满
break;
}
}

    // 把用户请求添加到处理队列中
notifyAll();
}
  ...
  }



可以看出,这个算法非常的简单,核心思路就是在每次wait返回时,计算wait等待的时间,并比较该时间和设定的要等待的时间,如果大于设定的要等待的时间,即确定为超时,否则确定为被notify唤醒。

使解决方案一般化

上述的解决方案针对我们目前的要求已经可以很好的工作了,但是细心的读者一定会发现在上面给出的解决方案中我们把两个无关的概念揉合在了一起:队列是否为满的判断逻辑和是否超时的计算判断逻辑。为什么说这是两个无关的概念呢?因为队列是否为满是与我们开发的具体应用相关的,不同的应用会有不同类型的判断逻辑(比如:不使用队列的应用可能会有其他在概念上类似的判断逻辑),而计算判断超时的逻辑是和具体应用无关的。如果我们能够把这两个概念剥离开来,那么这二者就可以独立变化,我们的解决方案的可重用性就会增强。

我们使用Template Method模式(参见参考文献〔2〕)来指导我们的重构。首先,我们会根据和具体应用无关的超时计算判断的算法定义一个通用的算法框架,把和具体应用逻辑相关的条件判断作为一个抽象的hook方法,延迟到具体的应用中去实现,从而实现了和应用无关的超时计算判断逻辑的复用。实现该算法的抽象基类的关键实现代码如下:

    public abstract class WaitWithTiming
{
// wait方法要作用的对象,对于上述例子就是ActiveQueue
protected Object object_;
public WaitWithTiming (Object obj)
{
object_ = obj;
}
// 这是一个抽象的hook方法,由具体的应用实现,该方法由本算法框架调用
public abstract boolean condition ();

// 计算判断超时的算法框架实现
public final void timedWait (long timeout)
throws InterruptedException, TimeoutException
{
if (condition ()) { //调用具体应用实现的hook方法
long start = System.currentTimeMillis ();
long waitTime = msecTimeout;

for (;Wink {
object_.wait (waitTime);

if (condition ()) {
long now = System.currentTimeMillis ();
long timeSoFar = now - start;

if (timeSoFar >= msecTimeout)
throw new TimeoutException ();
else
waitTime = timeout - timeSoFar;
}
else
break;
}
}
}
public final void announce() {
object_.notifyAll ();
}
}



使用方法介绍

本小节我们将对上一节给出的抽象基类WaitWithTiming的使用方法进行详细的介绍。我们当然可以直接使得ActiveQueue继承自WaitWithTiming,并实现相应的抽象hook方法condition,但是这样做有一个弊端,就是对于ActiveQueue我们只能够实现仅仅一个condition,如果我们要添加针对dequeue时队列为空的条件判断逻辑就无能为力了,因为WaitWithWaiting仅仅只有一个condition方法(其实,即使有多个也没有办法做到通用,因为不能对具体的应用的需求进行假设)。

我们推荐的使用方法是,根据具体应用的需求,整理出需要的判断条件,创建相应的类来表示这些判断条件,使这些用来表示具体判断条件的类继承自WaitWithTiming,这些类中具体的条件判断逻辑的实现可以使用相应的具体的应用实体。比如:对于本文开始所列举的应用,我们需要的判断条件为队列为满,所以我们可以定义一个QueueFullCondition类继承自WaitWithTiming,在QueueFullCondition中实现抽象的hook方法condition的逻辑,在该逻辑中在使用ActiveQueue的isFull方法。使用这种委托的方法,我们就可以比较有效的解决一个对象同时需要多个判断条件的问题(不同的判断条件只需定义不同的子类即可)。相应的UML结构图和关键代码实现如下:

UML结构图:



关键代码片断:

class QueueFullCondition extends WaitWithTiming
{
public QueueFullCondition (ActiveQueue aq)
{ super (aq); } // 为WaitWithTiming中的object_赋值

public boolean condition () {
ActiveQueue aq = (ActiveQueue) object_; //使用ActiveQueue来实现具体的判断逻辑
return aq.isFull ();
}
}

    class ActiveQueue {
      ...
public synchronized void enqueue(ClientRequest cr, long timeout)
          throws InterruptedException, TimeoutException
{
   //具有时限控制的等待  
queueFullCondition_.timedWait (timeout);

// 把用户请求添加进队列

//唤醒等待在ActiveQueue上的线程
queueFullCondition_.announce ();
}
  ...
private QueueFullCondition queueFullCondition_ = new QueueFullCondition (this);

  }



要注意的问题

如果读者朋友仔细观察的话,就会觉察到在WaitWithTiming类中的timedWait方法的定义中没有添加synchronized关键字,这一点是非常关键的,因为是为了避免在编写并发的Java应用时一个常见的死锁问题:嵌套的monitor。下面对于这个问题进行简单的介绍,关于这一问题更为详细的论述请参见参考文献〔1〕。

什么是嵌套的monitor问题呢?嵌套的monitor是指:当一个线程获得了对象A的monitor锁,接着又获得了对象B的monitor锁,在还没有释放对象B的monitor锁时,调用了对象B的wait方法,此时,该线程释放对象B的monitor锁并等待在对象B的线程等待队列上,但是此时该线程还拥有对象A的monitor锁。如果该线程的唤起条件依赖于另一个线程首先要获得对象A的monitor锁的话,就会引起死锁,因为此时别的线程无法获得上述线程还没有释放的对象A的monitor锁,结果就出现了死锁情况。一般的解决方案是:在设计时线程不要获取对象B的monitor锁,而仅仅使用对象A的monitor锁。

针对我们前面列举的例子,ActiveQueu可以类比为对象A,queueFullContion_可以类比为对象B,如果我们在timedWait方法前面添加上synchronized关键字,就有可能会发生上述的死锁情况,因为当我们在调用ActiveQueu的enqueue方法中调用了queueFullContion_的timedWait方法后,如果队列为满,虽然我们释放了queueFullContion_的monitor锁,但是我们还持有ActiveQueue的monitor锁,并且我们的唤醒条件依赖于另外一个线程调用ActivcQueue的dequeue方法,但是因为此时我们没有释放ActiveQueue的monitor锁,所以另外的线程就无法调用ActiveQueu的dequeue方法,那么结果就是这两个线程就都只能够等待。

总结

本文对于Java中wait方法超时语意的模糊性进行了分析,并给出了一个比较通用的解决方案,本解决方案对于需要精确的超时语意的应用还是无法很好的适用的,因为方案中所给出的关于超时计算的算法是不精确的。还有一点就是有关嵌套monitor的问题,在编写多线程的Java程序时一定要特别注意,否则非常容易引起死锁。其实,本文所讲述的所有问题的根源都是由于Java对于wait方法超时语意实现的模糊性造成的,如果在后续的Java版本中对此进行了修正,那么本文给出的解决方案就是多余的了。


   Powered by Jute Powerful Forum® Version Jute 1.5.6 Ent
Copyright © 2002-2021 Cjsdn Team. All Righits Reserved. 闽ICP备05005120号-1
客服电话 18559299278    客服信箱 714923@qq.com    客服QQ 714923