更明确地说,这一问题是由get()的前置条件是以size()的结果来定义的这一事实所带
来的。
只要看到这种必须使用一种方法的结果作为另一种讲法的输入条件的样式,它就是一
个状态依赖,就必须保证至少在调用这两种方法期间元素的状态没有改变。
一般来说,做到
这一点的唯一方法在调用第一个方法之前是独占性地锁定对象,一直到调用了后一种方法以
后。
在上面的迭代Vector元素的例子中,您需要在迭代过程中同步Vector对象。
如上面的例子所示,线程安全性不是一个非真即假的命题。
Vector的方法都是同步的,
并且Vector明确地设计为在多线程环境中工作。
但是它的线程安全性是有限制的,即在某些
方法之间有状态依赖(类似地,如果在迭代过程中Vector被其他线程修改,那么由
Vector.iterator()返回的iterator会抛出ConcurrentModificationException)。
对于Java类中常见的线程安全性级别,没有一种分类系统可被广泛接受,不过重要的
是在编写类时尽量记录下它们的线程安全行为。
Bloch给出了描述五类线程安全性的分类方法:
不可变、线程安全、有条件线程安全、
线程兼容和线程对立。
只要明确地记录下线程安全特性,那么您是否使用这种系统都没关系。
这种系统有其局限性——各类之间的界线不是百分之百地明确,而且有些情况它没照顾到,
但是这套系统是一个很好的起点。
这种分类系统的核心是调用者是否可以或者必须用外部同
步包围操作(或者一系列操作)。
下面分别描述了线程安全性的这五种类别。
1)不可变
不可变的对象一定是线程安全的,并且永远也不需要额外的同步。
因为一个不可变的对
象只要构建正确,其外部可见状态永远也不会改变,永远也不会看到它处于不一致的状态。
Java类库中大多数基本数值类如Integer、String和BigInteger都是不可变的。
2)线程安全
由类的规格说明所规定的约束在对象被多个线程访问时仍然有效,不管运行时环境如何
排列,线程都不需要任何额外的同步。
这种线程安全性保证是很严格的——许多类,如
Hashtable或者Vector都不能满足这种严格的定义。
3)有条件的线程安全
有条件的线程安全类对于单独的操作可以是线程安全的,但是某些操作序列可能需要外
部同步。
条件线程安全的最常见的例子是遍历由Hashtable或者Vector或者返回的迭代器
——由这些类返回的fail-fast迭代器假定在迭代器进行遍历的时候底层集合不会有变化。
为
了保证其他线程不会在遍历的时候改变集合,进行迭代的线程应该确保它是独占性地访问集
合以实现遍历的完整性。
通常,独占性的访问是由对锁的同步保证的——并且类的文档应该
说明是哪个锁(通常是对象的内部监视器(intrinsicmonitor))。
如果对一个有条件线程安全类进行记录,那么您应该不仅要记录它是有条件线程安全的,
而且还要记录必须防止哪些操作序列的并发访问。
用户可以合理地假设其他操作序列不需要
任何额外的同步。
4)线程兼容
线程兼容类不是线程安全的,但是可以通过正确使用同步而在并发环境中安全地使用。
这可能意味着用一个synchronized块包围每一个方法调用,或者创建一个包装器对象,其中
每一个方法都是同步的(就像Collections.synchronizedList()一样)。
也可能意味着用
synchronized块包围某些操作序列。
为了最大程度地利用线程兼容类,如果所有调用都使用
同一个块,那么就不应该要求调用者对该块同步。
这样做会使线程兼容的对象作为变量实例
包含在其他线程安全的对象中,从而可以利用其所有者对象的同步。
许多常见的类是线程兼容的,如集合类ArrayList和HashMap、
java.text.SimpleDateFormat、或者JDBC类Connection和ResultSet。
5)线程对立
线程对立类是那些不管是否调用了外部同步都不能在并发使用时安全地呈现的类。
线程
对立很少见,当类修改静态数据,而静态数据会影响在其他线程中执行的其他类的行为,这
时通常会出现线程对立。
线程对立类的一个例子是调用System.setOut()的类。
线程安全类(以及线程安全性程度更低的的类)可以允许或者不允许调用者锁定对象以
进行独占性访问。
Hashtable类对所有的同步使用对象的内部监视器,但是
ConcurrentHashMap类不是这样,事实上没有办法锁定一个ConcurrentHashMap对象以进
行独占性访问。
除了记录线程安全程序,还应该记录是否某些锁——如对象的内部锁——对
类的行为有特殊的意义。
通过将类记录为线程安全的(假设它确实是线程安全的),您就提供了两种有价值的服务:
您告知类的维护者不要进行会影响其线程安全性的修改或者扩展,您还告知类的用户使用它
时可以不使用外部同步。
通过将类记录为线程兼容或者有条件线程安全的,您就告知了用户
这个类可以通过正确使用同步而安全地在多线程中使用。
通过将类记录为线程对立的,您就
告知用户即使使用了外部同步,他们也不能在多线程中安全地使用这个类。
不管是哪种情况,
您都在潜在的严重问题出现之前防止了它们,而要查找和修复这些问题是很昂贵的。
一个类的线程安全行为是其规格说明中的固有部分,应该成为其文档的一部分。
因为还
没有描述类的线程安全行为的声明式方式,所以必须用文字描述。
虽然Bloch的描述类的线
程安全程度的五层系统没有涵盖所有可能的情况,但是它是一个很好的起点。
如果每一个类
都将这种线程行为的程度加入到其Javadoc中,那么可以肯定的是我们大家都会受益。
2.2.Servlet的线程安全性
Servlet/JSP默认是以多线程模式执行的,所以,在编写代码时需要非常细致地考虑多线
程的安全性问题。
然而,很多人编写Servlet/JSP程序时并没有注意到多线程安全性的问题,
这往往造成编写的程序在少量用户访问时没有任何问题,而在并发用户上升到一定值时,就
会经常出现一些莫明其妙的问题。
Servlet体系结构是建立在Java多线程机制之上的,它的生命周期是由Web容器负责的。
当客户端第一次请求某个Servlet时,Servlet容器将会根据web.xml配置文件实例化这个
Servlet类。
当有新的客户端请求该Servlet时,一般不会再实例化该Servlet类,也就是有多
个线程在使用这个实例。
Servlet容器会自动使用线程池等技术来支持系统的运行。
这样,当两个或多个线程同时访问同一个Servlet时,可能会发生多个线程同时访问同一
资源的情况,数据可能会变得不一致。
所以在用Servlet构建的Web应用时如果不注意线程
安全的问题,会使所写的Servlet程序有难以发现的错误。
1.无状态Servlet
下面是一个无状态的Servlet,它从Request中解包数据,然后将这两个数据进行相乘,
最后把结果封装在Response中。
importjava.io.IOException;
importjava.io.PrintWriter;
importjavax.servlet.ServletException;
importjavax.servlet.http.HttpServlet;
importjavax.servlet.http.HttpServletRequest;
importjavax.servlet.http.HttpServletResponse;
publicclassConcurrentServletextendsHttpServlet{
privatestaticfinallongserialVersionUID=1L;
publicConcurrentServlet(){
super();
}
protectedvoiddoGet(HttpServletRequestrequest,
HttpServletResponseresponse)throwsServletException,
IOException{
Strings1=request.getParameter("num1");
Strings2=request.getParameter("num2");
intresult=0;
if(s1!
=null&&s1!
=null){
result=Integer.parseInt(s1)*Integer.parseInt(s2);
}
PrintWriterout=response.getWriter();
out.print(result);
out.close();
}
}
这个Servlet是无状态的,它不包含域,也没有引用其它类的域,一次特定计算的瞬时状
态,会唯一的存储在本地变量中,这些本地变量存在线程的栈中,只有执行线程才能访问,
一个执行该Servlet的线程不会影响访问同一个Servlet的其它线程的计算结果,因为两个线
程不共享状态,他们如同在访问不同的实例。
因为线程访问无状态对象的行为,不会影响其它线程访问对象时的正确性,所以无状态
对象是线程安全的。
2有状态Servlet
对上面的Servlet进行修改,把result变量提升为类的实例变量。
那么这个Servlet就有状
态了。
有状态的Servlet在多线程访问时,有可能发生线程不安全性。
请看下面的代码。
importjava.io.IOException;
importjava.io.PrintWriter;
importjavax.servlet.ServletException;
importjavax.servlet.http.HttpServlet;
importjavax.servlet.http.HttpServletRequest;
importjavax.servlet.http.HttpServletResponse;
publicclassStatefulServletextendsHttpServlet{
privatestaticfinallongserialVersionUID=1L;
intresult=0;
publicStatefulServlet(){
super();
}
protectedvoiddoGet(HttpServletRequestrequest,
HttpServletResponseresponse)throwsServletException,
IOException{
Strings1=request.getParameter("num1");
Strings2=request.getParameter("num2");
if(s1!
=null&&s1!
=null){
result=Integer.parseInt(s1)*Integer.parseInt(s2);
}
try{
Thread.sleep(5000);
}catch(InterruptedExceptione){
e.printStackTrace();
}
PrintWriterout=response.getWriter();
out.print(result);
out.close();
}
}
在Servlet中定义了一个实例变量result,Servlet把它的值进行输出。
当只有一个用户访
问该Servlet时,程序会正常的运行,但当多个用户并发访问时,就可能会出现其它用户的信
息显示在另外一些用户的浏览器上的问题。
这是一个严重的问题。
为了突出并发问题,便于测试、观察,我们在回显用户信息时执行了一个延时的操作。
打开两个浏览器窗口,分别输入:
http:
//localhost:
8080/test/StatefulServlet?
num1=5&num2=80
http:
//localhost:
8080/test/StatefulServlet?
num1=5&num2=70。
相隔5000毫秒之内执行这两个请求,产生的结果如下图:
从运行结果可以看出,两个请求显示了相同的计算结果,也就是说,因为两个线程访问
了共同的有状态的Servlet,其中一个线程的计算结果覆盖了另外一个线程的计算结果。
从程
序分析可以看出第一个线程在输出result时,暂停了一段时间,那么它的值就被第二个线程
的计算结果所覆盖,两个请求输出了相同的结果。
这就是潜在的线程不安全性。
要解决线程不安全性,其中一个主要的方法就是取消Servlet的实例变量,变成无状态的
Servlet。
另外一种方法是对共享数据进行同步操作。
使用synchronized关键字能保证一次只
有一个线程可以访问被保护的区段,同步后的Servlet如下:
importjava.io.IOException;
importjava.io.PrintWriter;
importjavax.servlet.ServletException;
importjavax.servlet.http.HttpServlet;
importjavax.servlet.http.HttpServletRequest;
importjavax.servlet.http.HttpServletResponse;
publicclassStatefulServletextendsHttpServlet{
privatestaticfinallongserialVersionUID=1L;
intresult=0;
publicStatefulServlet(){
super();
}
protectedvoiddoGet(HttpServletRequestrequest,
HttpServletResponseresponse)throwsServletException,
IOException{
Strings1=request.getParameter("num1");
Strings2=request.getParameter("num2");
synchronized(this){
if(s1!
=null&&s1!
=null){
result=Integer.parseInt(s1)*Integer.parseInt(s2);
}
try{
Thread.sleep(5000);
}catch(InterruptedExceptione){
e.printStackTrace();
}
PrintWriterout=response.getWriter();
out.print(result);
out.close();
}
}
}
Servlet的线程安全问题只有在大量的并发访问时才会显现出来,并且很难发现,因此在
编写Servlet程序时要特别注意。
线程安全问题主要是由实例变量造成的,因此在Servlet中
应避免使用实例变量。
如果应用程序设计无法避免使用实例变量,那么使用同步来保护要使
用的实例变量,但为保证系统的最佳性能,应该同步可用性最小的代码路径。
2.3.同步与互斥
线程通