改善Java性能

 转载自 程序员杂志

Java 能够让程序员相对容易地开发出复杂的应用程序,这无疑应该归功于它的普遍性和流行性。然而,易用性就像一把双刃剑,Java批评家经常抱怨“Java不能表现出良好性能”。 就同一个功能而言,用Java编写的程序性能不如用C++编写的程序。但是大部分Java程序的性能问题并不能归罪于Java语言,而只能归罪于程序本身。优秀的设计规则在提高程序性能方面大有作为。例如,给Java应用程序增加线程池不但可以提供性能,而且是应用程序更有活力。无论如何,优秀的编码经验(包括如何更好使用java.lang.String和java.util.Vector类等)必须精通,以此来大幅提高性能。换句话说,设计优秀的程序不会,而且也不可能因为缺乏编程实践而付出高代价。下面我们就来看看,上述观点在编程中是如何发挥作用的。

增强性能的编码

在Java编程中,java.lang.String很可能是使用率最高,而且也最易滥用的类之一。同样,它也是在程序代码中导致效率最底下的类。如下所示:
String s1 = "Testing String";
String s2 = "Concatenation Performance";
String s3 = s1 + " " + s2;
几乎每个Java程序员都可看出该代码的效率很低。应该怎么办?或许可以试试下面的方法:
StringBuffer s = new StringBuffer();
s.append("Testing String");
s.append(" ");
s.append("Concatenation Performance");
String s3 = s.toString();
这段代码的执行性能要比第一段好得多,是吗??错,这正是编译器针对第一段代码所做的编译结果。如果使用StringBuffer并不能使该代码比独立的String objects更具效率,那为什么每一本Java的书籍都谴责第一段代码而推崇第二段代码呢?
   还是来看看第二段代码中所使用的默认StringBuffer构造符(同时借助于第一段的编译器)。下面是其显示结果:
Public StringBuffer(){
    this(16);
}
    默认构造器采用的是16个字符。下面来看看StringBuffer类中的append()方法:
pubic synchronized StringBuffer append(String str){
    if (str == null){
       str = String.valueOf(str);
    }
    int len = str.length();
    int newcount = count + len;
    if (newcount > value.length)
       expandCapacity(newcount);
    str.getChars(0,len,value,count);
    count = newcount;
    return this;
}
    append()方法第一次计算最终附加字符的总体长度,如果它比StringBuffer能够存储的空间大,就调用私有expandCapacity()方法,该方法将StringBuffer每次调用的控件增大了两倍,并且将现有的字符数组复制进新的内存。
    在第二段代码(以及第一段代码里由编译器产生的代码)中,因为最终字符串“Testing String Concatenation Performance”的长度是40个字符,StringBuffer空间将不得不扩充两倍,因此产生两倍的高昂副本,这样,能够跨过编译器的方式之一,就是给StringBuffer分配大于或等于40个字符的初始值,如下所示:
StringBuffer s = new StringBuffer(45);
s.append("Testing String");
s.append(" ");
s.append("concatenation Performance");
String s3 = s.toString();

设想如下:
Strng s = "";
int sum = 0;
for (int i=1; i<10; i++){
    sum += i;
    s = s + "+" + i;
}
s = s + "=" + sum;
想想为什么那段代码会比下列代码效率更低:
StringBuffer sb = new StringBuffer();
int sum = 0;
for(int i=1; i<10; i++){
    sum += i;
    sb.append(i).append("+");
}
String s = sb.append("=").append(sum).toString();
原因就在对于每个s=s+"+"+i,执行导致了StringBuffer和String object的产生和消除。这纯粹是浪费,在第二个例子中我们就可以避免。
下面来看看另一个非常流行的Java类----java.util.Vector。 用最简单的术语,Vector就是java.lang.Object实例中的数组。同其他数组一样,它包含能用整数索引访问的多个组件。无论如何,在创建Vector之后,Vector的大小能够适应条目的需要,随着条目的增加或删除而增加或缩减。如下所示,我们来看看给Vector增加元素的实例:
Object obj = new Object();
Vector v = new Vector(100000);
for(int i=0; i<100000; i++){
    v.add(0,obj);
}
    除非不得已的原因,不要在每个Vector开头处增加新元素,否则这段代码非常耗费性能。在默认的构造函数中,Vector的最初空间设定为10个元素,并且在该空间范围下,每增加一个元素,空间就加大一倍。由于使用StringBuffer类,每次空间扩充,所有的现有元素都被复制进新内存。下面的这段代码将比第一个范例快好几个数量级:
Object obj = new Object();
Vector v = new Vector(100000);
for(int i=0; i<100000; i++){
    v.add(obj);
}
    同样的道理也适用于Vector类的remove()方法。把除了最后一个索引之外的任何元素删除掉,都会导致被删除元素一边的所有元素移位,因为在Vector所包含的元素之间不能存在任何“缝隙”。这就意味着从Vector中删除最后一个元素要比删除第一个元素“便宜”好多倍。
    如果想删除Vector范例中的所有元素,你可以试试下面的方法:
for(int i=0; i<100000; i++){
    v.remove(0);
}
    这将比下列方法慢好几个数量级:
for(int i=0; i<100000; i++){
    v.remove(v.size()-1);
}
    从V中删除所有元素的最好方法就是:
v.removeAllElements();
    假定Vector v包含字符串“Hello”,试试下列代码,可以用它来删除Vector中的该字符串:
String s = "Hello";
int i = v.indexOf(s);
if(i != =1)
    v.remove(s);
    该代码看起来似乎没有什么损害,但它会在此降低性能。这里用indexOf()方法可以在v中实现顺序检索,并且找出字符串“Hello”的索引。然后调用remove(s)再次做同样的顺序检索。较好的方案就是:
String s = "Hello";
int i = v.indexOf(s);
if (i != -1)
    v.remove(i);
    这次我们给remove()方法准确的索引,消除了第二次顺序检索,可以得出一个更好的版本:
String s = "Hello";
v.remove(s);
最后,我们来看看有关Vector类的更多代码段:
for(int i=0; i<v.size(); i++){
    System.out.println(v.get(i).getClass().toString());
}
如果v包含100000个元素,这个代码段将调用v.szie() 100000次。尽管该size方法耗时不多,当它却始终需要JVM建立和拆开堆栈。这里,for循环内部的代码无论以何种方式都不能调整Vector v的大小。所以编写这个代码的最好方法就是:
int size = v.size();
for(int i=0; i<size; i++){
    System.out.println(v.get(i).getClass().toString());
}
    虽然是一个简单的更改,但却极大地提高了性能。毕竟,每个CPU的周期都是可计算的数字。
    为了证实上述建议的确能够改进性能,我们决定将其中部分理论用于线程池实现中,以此来比较性能变化前后线程池的性能表现。下面,我们总共列举了线程池实现的四个性能改善方法。需要注明的是,我们并没有对线程池接口做任何更改,所以现有客户程序可以不受任何影响(当然除了性能可能改进之外)。

性能改进方法之一

    使用java.lang.ArrayList代替java.lang.Vector类。同步是防止常见危险因素的关键,尤其是多线程程序的竞争条件下更是如此。但是,太多的同步也能够产生性能的瓶颈,这在线程池的实现中是一个经常遇到的问题。
    池中所以线程的列表用Vector来维护。因为Vector被设计为线程安全类,所以Vector类中的每种方法都被同步。无论如何,我们不必再为同步使用额外的代码,因为线程池里的每种共用的可访问的方法都已经被同步,并且每个有权访问Vector的对象(例如从PoolThread实例中)都由同步的模块产生。不过,JDK给我们提供了选择的机会。ArrayList类实质上是未同步的Vector,而这恰好正是我们说需要的。

性能改进方法之二

    用倒序的方式从挂起的作业列表中删除作业。在当前线程池实现中,从挂起作业列表的开头位置或用_pendingJobs.remove(0)删除挂起的作业。因为该方案降低了执行性能,我将从列表末或用_pendingJobs.remove(_pendingjobs.size()-1)删除挂起的作业。如果增加新作业的速度大于完成作业的速度,先前的作业将不会被执行。这是为了强化性能而不得已的折中做法。作为代替方案,我可以用一种用户自己配置,固定空间的循环队列替代列表来存储挂起作业。用这种方案,可以在队列头删除作业,还能够避免有可能出现的早期作业不被执行的问题。如果该队列满了,线程池将返回溢出错误给调用者。因为在该队列中,肯定不会移动元素,因此省去了内存拷贝的开销,只有将性能考虑进去,我们才算真正OK。

性能改进方法之三
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值