从一道题目验证Servlet单实例

1、题目

在servlet的开发中,人们通常喜欢用StringBuffer对象来append日志,然后将日志打印出来。在走读某同事的代码时,发现其servlet代码是这么写的

public class DoSomethingServlet extends HttpServlet {
	private static Logger logger = org.apache.log4j.Logger.getLogger(DoSomethingServlet.class);
    private StringBuffer sb = new StringBuffer();
    
	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        sb.append("日志信息");  
        //...  
        logger.info(sb.toString());
	}

	protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		doGet(request, response);
	}
}

请问这位同事的代码有什么问题?

2、初探

我想这大概是和线程安全有关吧,多线程访问下日志会互窜?动手做个试验,首先是servlet

	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		int userid = Integer.valueOf(request.getParameter("userid"));  
		sb.append("userid="+userid+" ");  
		logger.info(sb.toString());  
		PrintWriter out = response.getWriter();  
		out.print("process result!"); 
	}

由于用浏览器模拟多个用户访问比较麻烦,下面通过代码发送http请求

import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;


public class UserThread extends Thread{

	private int userid;

	public UserThread(int userid) {
		super();
		this.userid = userid;
	}

	@Override
	public void run() {
		super.run();
		try {
			request();
		} catch (Exception e) {
			e.printStackTrace();
		}
	};
	private String request() throws Exception{
		String url = "http://localhost:8080/ServletSingleThread/DoSomething?userid="+userid;
		System.out.println(url);
		URL u = new URL(url);
	    HttpURLConnection con;
	    InputStream in;
	    OutputStream out;
	    ByteArrayOutputStream baos = new ByteArrayOutputStream();
	    byte[] data =new byte[4096];
	    con = (HttpURLConnection) u.openConnection();     
	    con.setRequestMethod("GET");
	    con.setDoInput(true);
	    con.setDoOutput(true);
		
	    out = con.getOutputStream();
	    out.write(0);
	    out.flush();
	    
	    in = con.getInputStream();
	    int i=0;
	    while((i=in.read(data))!=-1){
	    	baos.write(data, 0, i);
	    }
	    out.close();
	    in.close();
	    baos.close();
		return baos.toString();
	}
	public static void main(String[] args) {
		//模拟多用户请求
		for (int i = 0; i < 10; i++) {
			UserThread ut = new UserThread(i);	
			ut.start();
		}
	}
}

log4j的配置就不贴上来,将日志输出到控制台,启动服务器,运行UserThread,观察日志:

[INFO] 2016-01-16 16:18:26 : userid=6 userid=7 userid=5 
[INFO] 2016-01-16 16:18:26 : userid=6 userid=7 userid=5 userid=8 userid=3 userid=2 userid=9 userid=0 userid=1 userid=4 
[INFO] 2016-01-16 16:18:26 : userid=6 userid=7 userid=5 userid=8 userid=3 userid=2 userid=9 userid=0 userid=1 
[INFO] 2016-01-16 16:18:26 : userid=6 
[INFO] 2016-01-16 16:18:26 : userid=6 userid=7 userid=5 userid=8 userid=3 userid=2 userid=9 userid=0 
[INFO] 2016-01-16 16:18:26 : userid=6 userid=7 userid=5 userid=8 userid=3 userid=2 userid=9 
[INFO] 2016-01-16 16:18:26 : userid=6 userid=7 userid=5 userid=8 userid=3 userid=2 
[INFO] 2016-01-16 16:18:26 : userid=6 userid=7 userid=5 userid=8 userid=3 
[INFO] 2016-01-16 16:18:26 : userid=6 userid=7 userid=5 
[INFO] 2016-01-16 16:18:26 : userid=6 userid=7 userid=5 userid=8 
这并不是我们想要的日志,不同用户的请求日志窜到了一起,但是仔细看会发现日志还是有规律的,userid被按线程执行顺序添加到了同一个StringBuffer中,说明了对于多个用户的请求,共用了同一个Servlet,可见Servlet是单例的。所有对于题目的问题,可以这么解决:将StringBuffer设置成局部变量。

	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		int userid = Integer.valueOf(request.getParameter("userid"));  
		StringBuffer sb = new StringBuffer();
		sb.append("userid="+userid+" ");  
		logger.info(sb.toString());  
		PrintWriter out = response.getWriter();  
		out.print("process result!"); 
	}

再跑一遍,观察日志:

[INFO] 2016-01-16 16:33:47 : userid=2 
[INFO] 2016-01-16 16:33:47 : userid=0 
[INFO] 2016-01-16 16:33:47 : userid=4 
[INFO] 2016-01-16 16:33:47 : userid=7 
[INFO] 2016-01-16 16:33:47 : userid=6 
[INFO] 2016-01-16 16:33:47 : userid=5 
[INFO] 2016-01-16 16:33:47 : userid=1 
[INFO] 2016-01-16 16:33:47 : userid=9 
[INFO] 2016-01-16 16:33:47 : userid=3 
[INFO] 2016-01-16 16:33:47 : userid=8
这才是我们想要的正常的日志 ,每个线程在自己的堆栈空间执行方法,StringBuffer在该方法体中,所有不会在多线程中共享。

3、深究

在刚刚的测试中,并没有很好的体现多线程并发访问问题,有人可能会想,这样写也可以解决题目的问题

    	synchronized(this){
    		sb.append("userid="+userid+" ");
    	}

测试结果和第一种情况一样,其实append本身就是一个同步方法,在这里加个同步块并没有意义。那如果是StringBuilder呢?

public class DoSomethingServlet extends HttpServlet {
	private static Logger logger = org.apache.log4j.Logger.getLogger(DoSomethingServlet.class);
	private StringBuilder sb = new StringBuilder();
    
	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		int userid = Integer.valueOf(request.getParameter("userid"));  
		sb.append("userid="+userid+" ");  
		logger.info(sb.toString());  
		PrintWriter out = response.getWriter();  
		out.print("process result!"); 
	}

	protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		doGet(request, response);
	}
}
测试结果:
[INFO] 2016-01-16 16:56:43 : userid=7 userid=3 userid=1 userid=5 userid=6 userid=8 userid=2 userid=4 
[INFO] 2016-01-16 16:56:43 : userid=7 userid=3 userid=1 userid=5 userid=6 userid=8 userid=2 userid=4 
[INFO] 2016-01-16 16:56:43 : userid=7 userid=3 
[INFO] 2016-01-16 16:56:43 : userid=7 userid=3 userid=1 userid=5 userid=6 userid=8 userid=2 userid=4 userid=9 
[INFO] 2016-01-16 16:56:43 : userid=7 userid=3 userid=1 userid=5 userid=6 userid=8 userid=2 
[INFO] 2016-01-16 16:56:43 : userid=7 userid=3 userid=1 
[INFO] 2016-01-16 16:56:43 : userid=7 userid=3 userid=1 userid=5 userid=6 userid=8 
[INFO] 2016-01-16 16:56:43 : userid=7 
[INFO] 2016-01-16 16:56:43 : userid=7 userid=3 userid=1 userid=5 
[INFO] 2016-01-16 16:56:43 : userid=7 userid=3 userid=1 userid=5 userid=6
仔细观察会发现和第一种情况是不一样的,1、2行日志相同了。因为StringBuilder是线程不安全的,多个线程并发append时就会出现数据不一致:在1、2行日志输出之前,userid=0被userid=4覆盖了。更直观点可以有String来测试:
public class DoSomethingServlet extends HttpServlet {
       
	private static Logger logger = org.apache.log4j.Logger.getLogger(DoSomethingServlet.class);
    private String str;
    
	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        int userid = Integer.valueOf(request.getParameter("userid"));
    	str = "userid="+userid+" ";
		logger.info(str);
		PrintWriter out = response.getWriter();
		out.print("process result!");
	}

	protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		doGet(request, response);
	}
}
测试结果:
[INFO] 2016-01-16 17:14:15 : userid=1 
[INFO] 2016-01-16 17:14:15 : userid=6 
[INFO] 2016-01-16 17:14:15 : userid=4 
[INFO] 2016-01-16 17:14:15 : userid=9 
[INFO] 2016-01-16 17:14:15 : userid=3 
[INFO] 2016-01-16 17:14:15 : userid=5 
[INFO] 2016-01-16 17:14:15 : userid=9 
[INFO] 2016-01-16 17:14:15 : userid=2 
[INFO] 2016-01-16 17:14:15 : userid=8 
[INFO] 2016-01-16 17:14:15 : userid=0
userid为7的用户的日志被userid为9的用户对应的处理线程给改了

4、结论

这道题目的解释就是,由于Servlet是单例的,所以所有请求共享同一个成员变量StringBuffer,但是由于StringBuffer是线程安全的,所有StringBuffer的值并不会因为并发访问而相互覆盖,只是不断的新增。如果使用StringBuffer来存储日志信息,请将其定义为局部变量。








  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值