原文:http://blog.csdn.net/liuyimu/article/details/5456399
今天的中间件课程上,Dash老师给出了一段代码,用于统计一个Servlet被访问的次数,代码如下:
- public class Counter extends HttpServlet {
- int count = 0; // 记录servlet被访问的次数
- public void doGet(HttpServletRequest request, HttpServletResponse response)
- throws ServletException, IOException {
- response.setContentType("text/html");
- PrintWriter out = response.getWriter();
- count++; // 访问次数加一
- out.println("The page has been accessed:"+count+"<BR>");
- }
- public void doPost(HttpServletRequest request, HttpServletResponse response)
- throws ServletException, IOException {
- this.doGet(request, response);
- }
- }
咋看上去,没有任何问题,好的,我们来实际运行一下,连续访问如下url五次:
连续访问五次,页面显示访问次数正确。但这并不代表程序没有问题,请考虑以下情况,客户端A和客户端B同时对Counter Servlet进行访问,那么必然需要同时对counter进行自增操作,竞争条件出现了!大家想一想,这和操作系统课程中的经典的竞争条件是不是一模一样。
要理解上面的话,首先要先理解Servlet的线程模型:
图一:Servlet线程模型,来自Java Eye
我们知道,当Counter类被初始化之后,Servlet容器中便保存了一个该对象的实例(注意有且仅有一个实例,还有一个条件,Counter类没有实现SingleThreadModel接口),如果存在两个客户端同时访问该Servlet,那么Servlet容器会从该Servlet对象的线程池中分配两个线程分别处理两个客户端的请求。在Dash老师给出的代码中,如果多个客户端同时访问这个Servlet,相应的必然存在多个线程同时访问Counter类的实例变量count,必然会导致数据竞争问题,为了更明显的演示这一问题,我先来给Dash老师的代码加点料,修改后的代码如下:
- public class Counterbeta1 extends HttpServlet {
- int count; //记录页面被访问的次数
- public void doGet(HttpServletRequest request, HttpServletResponse response)
- throws ServletException, IOException {
- response.setContentType("text/html");
- PrintWriter out = response.getWriter();
- count++; // 访问次数加1
- try
- {Thread. sleep (5000); //为了突出并发问题,在这设置一个延时
- }
- catch(Exception e)
- {e.printStackTrace();} //如果你还记得的话,在操作系统的线程同步试验中,
- //我们多次用这种技巧放大问题
- out.println("The page has been accessed:"+count+"<BR>");
- }
- public void doPost(HttpServletRequest request, HttpServletResponse response)
- throws ServletException, IOException {
- this.doGet(request, response);
- }
- }
做如下的实验,打开两个浏览器窗口,输入相同的url访问Counterbeta1,首先在第一个窗口中敲回车确认访问,再迅速的在第二个浏览器中窗口中敲回车,结果如下:
显然,结果出错,两个浏览器窗口中显示的访问次数都是2,显然正确的结果应该是一个为1,另外一个为2。数据竞争出现了,好了,让我再玩的损一点,给Dash老师的代码再加点料:
- public class Counterbeta2 extends HttpServlet {
- int count;
- public void doGet(HttpServletRequest request, HttpServletResponse response)
- throws ServletException, IOException {
- response.setContentType("text/html");
- PrintWriter out = response.getWriter();
- int count_copy = count;
- try
- {
- Thread. sleep (5000); //为了突出并发问题,在这设置一个延时
- }
- catch(Exception e)
- {e.printStackTrace();} //如果你还记得的话,在操作系统的线程同步试验中,
- //我们多次用这种技巧放大问题
- count_copy++;
- count = count_copy;
- out.println("The page has been accessed:"+count+"<BR>");
- }
- public void doPost(HttpServletRequest request, HttpServletResponse response)
- throws ServletException, IOException {
- this.doGet(request, response);
- }
- }
再实验一下,开两个浏览器窗口,方法同上,结果如下:
这下子错的更离谱了,两个页面显示的访问次数都是1。
下面给出线程安全版的计数器:
- public class Countersafe extends HttpServlet {
- int count;
- public void doGet(HttpServletRequest request, HttpServletResponse response)
- throws ServletException, IOException {
- response.setContentType("text/html");
- PrintWriter out = response.getWriter();
- synchronized(this)
- {
- count++; // 访问次数加一
- try
- {Thread. sleep (5000); //为了突出并发问题,在这设置一个延时
- }
- catch(Exception e)
- {e.printStackTrace();} //如果你还记得的话,在操作系统的线程同步试验中,
- //我们多次用这种技巧放大问题
- out.println("The page has been accessed:"+count+"<BR>");
- }
- }
- public void doPost(HttpServletRequest request, HttpServletResponse response)
- throws ServletException, IOException {
- this.doGet(request, response);
- }
- }
注意,上面给出的线程安全版依旧是为了放大问题而加入了线程睡眠的语句,请注意判断。
附录:完整源代码下载地址:http://download.csdn.net/source/2210349