并发与线程安全
串行运行时正确的程序在并发运行时可能会出错,这是由于并发运行的多个任务(进程或线程)之间共享了变量。在Java EE环境下很多的组件最终都是在多线程环境下并发运行的,应该牢记住这一点,避免发生诡异的错误。在上个项目中,我们的程序就出现过诡异的错误,在并发量小的测试环境下,它很少出现,但是随着并发量的增大,它出现的次数增多。经查,发现如下类似代码:
private SomeType someVariable;
public void doPost(HttpServletRequest req, HttpServletRespons rsp) {
method1();
method2();
}
void method1() {
...
someVariable = someValue;
...
}
void method2() {
...
someVariable = someValue;
...
}
}
method1() 和method2()都访问到了someVariable,而且程序员的本意是想在method2()使用由method1()产生的 someVariable,但是Servlet可能被多个线程共享,上面的代码不能正常工作。当Servlet容器用thread1和thread2两个线程来服务两个请求request1和request2,而thread1和thread2使用了同一个SomeServlet对象,如果JVM在 SomeServlet在执行到method1()后和method2()前时发生线程间的切换,那么就很可能导致出错,比如:thread1.method1()->thread2.method1()->thread1.method2()->thread2.method2()。实例变量和类变量都是在线程间共享的,而方法内的局部变量和参数是不会被共享的,所以要处理这个问题,最简单的方法是通过方法参数来传递 someVariable,而不是通过实例变量。所以这段程序改成这样就可以了:
public void doPost(HttpServletRequest req, HttpServletRespons rsp) {
SomeType someVariable = method1();
method2(someVariable );
}
SomeType someVariablemethod1() {
...
return someValue;
}
void method2(SomeType someVariable) {
...
someVariable = someValue;
...
}
}
通过synchronized等并发访问控制机制也可以解决这个问题,但是我觉得上面的方式是最直观的。也许有人会说后面的代码比前面的看起来要难看,不够面向对象。这里不讨论怎样才是面向对象,或者面向对象好不好。总之后者是正确的,前者是错误的,没有正确性的程序是没有意义的。
被过线程共享而不会出错的对象,被称之为线程安全的。显然Servlet不是线程安全的,而且还有很多组件也不是线程安全的,比如HttpSession。在上个项目中,我们使用JAXB来处理XML,每次使用时都创建一个JAXBContext对象,后来发现创建JAXBContext的代价是相当的高,于是我们想把它缓存起来在整个应用程序范围内使用,幸好我们使用的JAXBContext的实现是线程安全的,可以放心的被多个线程共享。
有状态与无状态
有状态组件是这样一种组件,组件调用者的返回结果会依赖于这个组件之前或正在受到的调用。无状态的组件则相反,调用者的返回结果只和本次调用相关,所有调用者的调用过程不会影响到其他调用者。举例来说,someComponent组件的getLatestCaller()返回上一次的调用者,这个行为的返回值总会和上一次的调用者相关,这样的组件就是有状态的。假如有个组件math有个方法为add(x,y),返回x+y的值,那么无论之前被哪个调用者调用过,math.add()的返回值只和本次调用的参数相关,这样的组件是无状态的。无状态的组件是线程安全的,因为被别的调用者调用并不会影响到本次的调用结果。
除非有必要,否则应当尽量把你的程序组件实现成无状态的。在spring, seam, ejb里都有无状态的组件。不同的框架在如何实现无状态方面各不相同。最简单的实现方法就是为无状态组件做一个包装,每次调用组件的方法时,都由包装类生成一个新的对象来处理,这样实际上就不再任何的调用者之间共享被调用者的实例,实现了无状态。比如:
public SomeType doMyBusinness(SomeArg arg);
}
public class SomeStatlessComponent implements SomeInterface {
public SomeType doMyBusinness(SomeArg arg) {
...
}
}
public class SomeStatlessComponentWrapper implements SomeInterface {
public SomeType doMyBusinness(SomeArg arg) {
return new SomeStatlessComponent().doMyBusinness(arg);
}
}
如果实例化组件的代价是昂贵的,用一个对象池缓存组件实例,并在每次从池中取对象时清空对象的状态可能是个更好的办法。其实只要遵循简单的原则,就很容易实现无状态,就是不使用实例变量或类变量,或者只使用只读的实例变量或只读类变量。这样的组件用单例就可以实先无状态。
由于实现上的差别,有些框架根本就不会做过多的努力来保证组件的无状态性,相反他们把责任交给了程序员。作为组件的创作者,如果你确定你要的是无状态的组件,那么只使用只读的实例变量或类变量,是个明智的选择。
相关文章: