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来存储日志信息,请将其定义为局部变量。