并发与线程安全
串行运行时正确的程序在并发运行时可能会出错,这是由于并发运行的多个任务(进程或线程)之间共享了变量。在Java EE环境下很多的组件最终都是在多线程环境下并发运行的,应该牢记住这一点,避免发生诡异的错误。在上个项目中,我们的程序就出现过诡异的错误,在并发量小的测试环境下,它很少出现,但是随着并发量的增大,它出现的次数增多。经查,发现如下类似代码:
public class SomeServlet extends HttpServlet {
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 class SomeServlet extends HttpServlet {
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 interface SomeInterface {
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);
}
}
如果实例化组件的代价是昂贵的,用一个对象池缓存组件实例,并在每次从池中取对象时清空对象的状态可能是个更好的办法。其实只要遵循简单的原则,就很容易实现无状态,就是不使用实例变量或类变量,或者只使用只读的实例变量或只读类变量。这样的组件用单例就可以实先无状态。
由于实现上的差别,有些框架根本就不会做过多的努力来保证组件的无状态性,相反他们把责任交给了程序员。作为组件的创作者,如果你确定你要的是无状态的组件,那么只使用只读的实例变量或类变量,是个明智的选择。
Java EE的发布,尤其是ejb3,让我高兴了一把。改进是显而易见的,但是还不够,经过一年的多的使用,我今天忍不住要发发牢骚。
混乱的jndi
把对象绑定到树形结构的目录上,易于查找和使用,这本来是一个非常好的想法,但是偏偏被搞的非常复杂。在同一个服务器内,你能看到的jndi的形式可能有 /a/b, java:a/b,java:com/env/a/b, 甚至a/b。为什么JCP那帮人就不能把这个名称统一一下呢?部署在应用服务器上的组件,可以有一个jndi,但是Java EE偏偏没有定义将组件映射到一个什么样的jndi名字上,于是在glassfish下,默认的数据源的jndi名是java:/jdbc/db,在 jboss下则是java:/db,就算是ejb,在不同的应用服务器上被部署后都会被分配给不同的名字。@Resource可以把jndi资源注入到组件中,但是在引用组件时却可以通过两个名字,如:@Resource(name=”a/b”) @Resource(mappedName=”a/b”),好讨厌!什么时候jndi能够被统一起来,java EE在易用性上一定会进一大步!
半吊子的依赖注入
当我听到可以通过@Resource @EJB注入资源时,我非常高兴,那岂不是意味着Java 应用服务器,一下子就变成了个spring bean容器了?但是最终的结果很令人失望,因为你只能注入有jndi名字的东西,而且只能将它注入到指定的几种组件中,这样的限制一下子把好好的依赖注入给废了。通过jndi去找组件,比通过spring bean的名字找bean要难多了,而且我们确实需要将组件注入到某个普通的类里面。这样的限制最终导致了在实际项目中,很少会用到Java EE本身的依赖注入功能。
classloader的噩梦
相同的一个类,被不同的classloader载入,那么在jvm中这两个类就不是同一个类。classloader的这一机制有它的道理,比如提高安全性,但是会产生一些很奇怪的问题。假设有两个classloader, cl1和cl2,这两个classloader都载入了同一个class,比如 A.class,其中cl1的A.class实例化了一个对象a1, 那么,当你试图把a1 cast成 cl2的A.class的类型的时候,就会发生ClassCastException,尽管cl1的A.class和cl2的A.class是同一个 class。J2ee规定了每个ear内的library必须能被ear内的每个成员共享,容器生成的ejb的stub也必须能在ear内被共享。通常要实现这个功能,只要给这个ear分配一个classloader,并由这个classloader载入需要被整个ear内共享的class,而ear内的 war,和ejb-jar则由这个ear的classloader的子classloader载入。有了这一机制,在ear内部,ejb的local接口可以被正常的使用而不会出现问题。但是如果跨越ear调用ejb的local接口,就会出现诸如ClassloaderException的异常。跨越 ear使用jpa也会遇到类似的问题。Java EE没有给出统一的解决方案,不同的服务器给出了不同的方案,但还没有一个解决方案算是真正的优美。
MDB不能消费远程的消息
ejb2的时代,同样作为ejb,message driven bean 远不像会话bean和实体bean那样有那么多负面评论。MDB确实很好用,唯一的不足就是没有统一和简单的方法可以让mdb监听远程的jms消息。我对此感到非常纳闷,既然Java EE的标准已经统一了那么多,为何不多进一步,也统一一下如何让MDB访问远程JMS?
或许Java EE标准的制定有太多的政治因素了,那些专家们总是再制造一些半成品。
php比java的开发效率高几乎成了大家的共识了。为什么php开发起来会比java快很多呢?频繁的服务重启是众多原因之一。php开发者们编辑完了 php代码,只要刷新一下浏览器,就可以看到运行的效果了,从来没有编译和重启apache的问题。尽管现在的IDE能够自动编译部署和打包应用程序,但 是还是没有php便捷。这一年来一直在使用jboss 4.2 做开发,jboss号称有很强的热部署能力,但是更新一个应用后还是要等上一两分钟的时间让jboss重新部署应用,而且这种热部署时间一长就会发生内存 耗尽的错误。如果是用tomcat+spring这种轻量级解决方案,重启一次十几秒钟,还可以接受,但是jboss+seam要一两分钟,而且吃掉百兆 内存,实在让人受不了。有的时候感觉一天之中没有在专心写多少代码,都是在等待jboss重启中度过。我现在对这种工作方式越来越不满,我想要更好性能的 开发机器,使用更轻量的框架,更轻量的服务器。现在使用的seam相对spring+hibernate+struts来说,还是重量的多,而jboss 也没有tomcat那样轻巧。如果能够不用等待就可以看到程序运行的效果,java在开发效率上会离php更近一步。
jboss的热部署,是指不重启jboss的情况下部署应用,比如ear, war等。虽然不重启jboss,但是还是要卸载和装载应用,还是慢。
如何热部署jboss应用呢?如果应用是用打包方式部署的,更新一下包的修改时间就可以了。如果是以目录的方式部署的,对于ear,更新一下META-INF/application.xml的修改时间,对于war,则更新WEB-INF/web.xml的修改时间。更新修改时间,最方便的就是用touch命令了,如果你用的是windows,没有touch命令,你可以装一个cygwin,或者用ant的touch任务来代替。
在做项目的时候,为了调试方便,把很多日志信息通过console打印出来。每次进行一个操作,输出的日志非常多,导致程序看起来非常慢。难道向控制台打印信息就这么慢吗?我突然想到以前用linux时好像没有这么慢,于是就做了一个简单的测试。测试程序非常简单,就是用System.out.println()向控制台写一个字符串,写十万次。在我的windows xp和ubuntu 8.04下分别测试了10次,结果发现,在linux下平均用时9.8秒,而windows下要用18.6秒,差不多是linux的两倍。两边用的都是sun的1.6版的虚拟机,没想到会有这么大的差异。
是不是java在linux上就比在windows上要快呢?我想找一些更全面,更权威的测试,但是没有找到。sun的官方论坛上,有人说在两个系统上的表现是差不多的,不知道他是从哪里得到的结论。通过这个简单的测试,至少可以看出,在x86平台上,同是sun 1.6的虚拟机,写控制台的时候,在windows xp上,比在ubunut 8.04上要慢一倍
突然看到消息,SpringSource宣布被微软收购,非常震惊! 这是真的吗?
今天是个西方特殊的日子,目前还不能肯定是不是真的。但是,微软完全有动机这样做。微软提出.net已有数年,但是仍旧不无法动摇java在企业开发界的地位。我对.net没有深入的研究,但是.net界远没有像java界那样百家争鸣,思想活跃是不争的事实。当用java开发有无数的方案可选时,.net世界里基本上只有MS自己的解决方案一家。sping是Java世界里最丰硕的果实,虽然spring也有.net的版本,但是一直没有被足够支持和发扬。微软收购Spring,一下子就把java世界的果实摘了去。而且,微软借此收购,还可以把java界的牛人Rod Johnson收入麾下,这颗脑袋比spring的源代码还要重要。
我很不希望这是真的。我担心johnson被拉入微软后,以后spring在java界的发展变慢,甚至于停滞。但愿这不是真的!