优化一般意义上说是提高已有系统的性能,减少如内存、数据库、网络带宽等资源的占用,是在系统开发告一段落的前提下进行。一般是通过压力测试或具体使用发现性能方面的问题,然后寻找性能瓶颈,并结合项目进度、人员安排、技术储备等因素,提出相应的优化策略。
下面结合一些案例,进行具体的讨论,总结出两个有代表性的条例:
条例一:尽量重用对象,避免创建过多短时对象
对象在面向对象编程中随处可见,甚至可以毫不夸张的说是:“一切都是对象”。如何更好的创建和使用对象,是优化中要考虑的一个重要方面。笔者将对象按使用分为两大类:独享对象和共享对象。独享对象指由某个线程单独拥有并维护其生命周期的对象,一般是通过new 创建的对象,线程结束且无其它对这个对象的引用,这个对象将由垃圾收集机制自动gc。共享对象指由多个线程共享的对象,各线程保持多个指向同一个对象的引用,任何对这个对象的修改都会在其它引用上得到体现,共享对象一般通过factory工厂的getinstace()方法创建,单例模式就是创建共享对象的标准实现。独享对象由于无其它指向同一对象的引用,不用担心其它引用对对象属性的修改,在多线程环境里,也就不需要对其可能修改属性的方法加以同步,减少了出错的隐患和复杂性,但由于需要为每个线程都创建对象,增加了对内存的需求和jvm gc的负担。共享对象则需要进行适当的同步(避免较大的同步块,同时防止死锁)。
还有几种特殊对象:不变对象和方法对象。不变对象指对象对外不含有修改对象属性的方法(如set方法),外部要修改属性只能通过new新的实例来实现。不变对象最大的好处就是无需担心属性被修改,避免了潜在的bug,并能无需任何额外工作(如同步)就很好的工作在多线程环境下。如jdk的string对象就是典型的不变对象。方法对象简单的说就是仅包含方法,不含有属性的对象。由于没有对象属性,方法中无需进行修改属性的操作,也就能采用static方法或单例模式,避免每次使用都要new对象,减少对象的使用。
那么该如何确定创建何种对象,这就要结合对象的使用方式和生命周期、对象大小、构建花销等方面来综合考虑。如果对象生命周期较长,会存在修改操作,不能容忍其它线程对其的修改,就应该采用独享对象,如常见的bean类。而如果对象生命周期较长,且能为各个线程共享,就可以考虑共享对象。共享有2种常见情况,一种是系统全局对象,如配置属性等,各个线程应该引用同一对象,任何对这个对象的修改都会影响其它线程;另一种是由于对象创建开销较大,各线程对此对象是瞬时访问,且无需再次读取其属性,如常见的date 对象,一般这种对象的使用是瞬时的,比如把它format成string,如果每次创建然后等待gc就会浪费大量内存和cpu时间,较好做法就是做成共享对象,各个线程先set再使用,注意对进行set并访问的方法要同步。不变对象一般使用在对象创建开销较小(属性较少,类层次较少),且需要能自由共享的情形。如一个对象里的常量对象,使用public static final aaa=new aaa(…) 创建。方法对象使用较广,如util类、dao类等,这些对象提供操作其它对象(一般是bean对象)的接口,能对系统在层次和功能上进行解耦合。
条例二:在循环处,多下功夫
循环作为程序编写的基本语法,可以说是随处可见。一些小的细节能带来性能上的提升,而对循环体的一些改写,能带来性能的大幅提升。
比如最简单的list遍历,会有这样的写法:for(int i=0;i
同样是对list的操作,如果要在遍历同时进行增加和删除操作,代码如下:for(int i=0,j=l.size();i=0;i--){l.remove(i);}。经过测试,如果采用arraylist,两种写法在循环次数较少时没有太大的区别,循环次数为1000,均为1ms以内,次数为10000,前一种为60ms左右,后一种为1ms以内,,而次数上到100000,前一种为6000ms左右,后一种为15ms,随着循环次数的增多,后一种较前一种的效率优势明显提高。
这是由collection库arraylist的实现决定的,以下是jdk1.3的arraylist源码:
public object remove(int index) { |
从中我们可以看出,nummoved代表了需要进行arraycopy操作的数量,它是由remove的位置决定的,如果index=0,也就是删除第一个元素,则需要arraycopy后面的所有数据,而如果index=size-1,则只需将最后一个元素设为null即可。所以从后面向前循环remove是比较好的写法。
如果list中的确存在较多的add或remove操作,且容量较大(如存储几万个对象),则应该采用linkedlist作为实现。linkedlist内部采用双向链表作为数据结构,比arraylist占用较多内存空间,且随机访问操作较慢(需要从头或尾循环到相应位置),但插入删除操作很快(仅需进行链表操作,无须大量移动或拷贝)。
对于list操作如果循环规模较小,其实对性能影响非常小(ms级),远远不是性能瓶颈所在。但心中有着优化的意识,并力求写出简洁高效的程序应该是我们每个程序员的追求。而且一旦在循环规模较大时,如果有了这些意识,也就能有效的消除性能隐患。
再举一个与优化无关但确实可能成为性能杀手(可以说是bug)的循环的例子。下面是源代码:
for(; totalread < m_totalbytes; totalread += readbytes) |
这个代码意图很清楚,就是将一个inputstream流读到一个byte数组中去。它使用read方法循环读取inputstream,该方法返回读取的字节数。正常情况下,该循环运行良好,当totalread=m_totalbytes时,结束循环,byte数组被正常填充。但如果仔细看一下inputstream的read方法的说明,了解一下其返回值就会发现,返回值可能为-1,即已读到inputstream末尾再继续读时。如果发生读取异常,可能出现这个问题,而这个循环没有检查readbytes值是否为-1就往totalread上加,这样再次进入循环体继续读取inputstream,又返回-1,继续循环。如此循环直到int溢出才会跳出循环。而这个循环也就成了实实在在的cpu杀手,可以占去大量的cpu时间(取决于操作系统)。其实解决很简单,对readbytes进行判断,如果为-1则跳出循环。
这个例子告诉我们:对循环一定要搞清循环的循环规模、每次循环体执行时间、循环结束条件包括异常情况等,只有这样才能写出高效且没有隐患的代码。
Java Asp PHP .Net XML C/C++ CGI VB Jsp J2ee J2se J2me EJB Servlet Tomcat Resin Struts Weblogic Eclipse ANT GUI JMS Web servise IDEA Webphere Hibernate Spring Jboss Applet Swing Socket Javamail Perl Ajax P2P 安全 模式 框架 测试 开源 游戏
Windows XP Windows 2000 Windows 2003 Windows Me Windows 9.x Linux UNIX 注册表 操作系统 服务器 应用服务器