String、StringBuffer、StringBuilder之间的性能和简单性权衡

6
你是否曾考虑过Java编程语言的这种变化所带来的影响?
String类被设计为不可变类(这个决定是经过深思熟虑的)。但是字符串连接非常慢,我自己进行了基准测试。因此,StringBuffer应运而生。这是一个非常好的类,它是同步的并且速度非常快。但是有些人对某些同步块的性能成本不满意,于是StringBuilder应运而生。
但是,当使用String连接不太多的对象时,该类的不可变性使其成为实现线程安全的一种非常自然的方式。我可以理解在我们想要管理多个字符串时使用StringBuffer。但是,这里是我的第一个问题:
1. 如果你有10个或更少的字符串要追加,你会为了执行时间中的几毫秒而牺牲简单性吗?
我也进行了StringBuilder的基准测试。它比StringBuffer更高效(只提高了10%)。但是,如果在单线程程序中使用StringBuilder,如果有时想要更改设计以使用多个线程,会发生什么情况?你必须更改每个StringBuilder实例,如果你忘记了其中一个,你将会产生一些奇怪的效果(由可能出现的竞争条件引起)。
2. 在这种情况下,你会为了调试几个小时而牺牲性能吗?
好的,这就是全部内容。除了简单的问题(StringBuffer比"+"更高效并且线程安全,StringBuilder比StringBuffer更快但不是线程安全),我想知道何时使用它们。
(重要提示:我知道它们之间的区别;这是一个与平台架构和一些设计决策有关的问题。)

2
尽管有加粗的部分,我仍然不完全确定你的问题是什么。实际上,你的文本似乎已经包含了标题中所问的问题的答案,并且加粗的部分直接相互矛盾(即加粗的问题与开头的“重要”提示相矛盾),而且在 Stack Overflow 上似乎已经回答了无数次。 - Konrad Rudolph
Konrad,我想知道有经验的程序员们的意见。我今天突然想到了这个问题,想知道它是否有任何意义。也许我没有正确地提问(我的英语不太好)。对此我感到抱歉。你是对的,顶部的通知不好,我需要改变它。我想知道什么时候使用它们的答案,而不是选择效率(就像我在这里看到的其他问题一样)。 - santiagobasulto
我也对StringBuilder进行了基准测试。它比StringBuffer更高效(仅平均10%)。请展示基准测试结果,你确定你已经正确编写了它吗?到目前为止,我认为在这个网站上还没有看到过一个合适的微基准测试。 - bestsss
@santiagobasulto,很可能是一个糟糕的微基准测试。 - bestsss
@santiagobasulto,如果您希望将测试代码添加到问题中,我会在几个小时内详细回复(现在必须离开)。 - bestsss
显示剩余2条评论
5个回答

9
关于你提到的“StringBuilder和线程”的评论,即使在多线程程序中,想要跨多个线程构建字符串也是非常罕见的。通常,每个线程都会拥有一些数据,并从中创建一个字符串,通常是通过将多个字符串连接在一起来实现的。然后他们会将该StringBuilder转换为字符串,并且该字符串可以安全地在线程之间共享。
我认为我从未看到过由于在线程之间共享StringBuilder而导致的错误。
个人而言,我希望StringBuffer不存在 - 它处于Java的“让我们同步所有东西”的阶段,导致了Vector和Hashtable,这些类已经被Java 2中不同步的ArrayList和HashMap类所淘汰。只是等待了一段时间,才出现了与StringBuffer不同步的等效类。
因此基本上:
- 当您不想执行操作并希望确保没有其他内容时,请使用字符串 - 使用StringBuilder进行操作,通常在短时间内完成 - 避免使用StringBuffer,除非您真的非常需要它 - 正如我所说,我无法记起曾经遇到过需要使用StringBuffer而不是StringBuilder的情况,当两者都可用时。

太好了Jon!谢谢!那就是我一直在寻找的。我一直在编写一个应用程序,它有这个小问题(多个线程共享一个简单的字符串),但这只是一个奇怪且不常见的例子。与集合的比较非常好!我从未想过。你说得对。 - santiagobasulto
我对StringBuffer和StringBuilder最大的抱怨是它们没有通过某个接口或抽象超类进行关联。 - MeBigFatGuy
@MeBigFatGuy,使用Appendable和CharSequence吗? - bestsss
@santiagobasulto:分享一个字符串没问题,但分享一个StringBuilder/StringBuffer是不常见的情况。 - Jon Skeet
@Jon,是的Jon,这就是我所说的。几个线程获得同一个StringBuffer实例并对其进行操作。 - santiagobasulto
@santiagobasulto:只有一次同步操作的工作是否能够正常进行?这就是自我同步类的问题——它们只能同步一个操作,通常并不是您想要的。 - Jon Skeet

8

StringBuffer是Java 1.0中的内容;它并不是对速度缓慢或不可变性的反应。它也不比字符串连接方式更快或更好;事实上,Java编译器会编译

String s1 = s2 + s3;

转化为类似于

String s1 = new StringBuilder(s2).append(s3).toString();

如果你不相信我,请使用反汇编器(例如javap -c)自己试一试。
关于“StringBuffer比连接更快”的事情是指 重复 连接。在这种情况下,显式创建自己的StringBuffer并重复使用它要比让编译器创建多个StringBuffer表现更好。
Java 5中引入了StringBuilder以提高性能,正如你所说的那样。使用它的原因是StringBuffer/Builder几乎从不在创建它们的方法之外共享:它们的99%用法类似上面的例子,即创建它们,用于将几个字符串附加在一起,然后丢弃。

你说得对,Ernest。它们都来自1.0版本。我只是想表达一个观点。我已经对字符串连接(使用+运算符)和StringBuffer.append()进行了基准测试,发现前者更快。 - santiagobasulto
2
引入StringBuilder后,编译器将使用StringBuilder代替StringBuffer来实现+运算符。 - Paŭlo Ebermann
@Paulo:确实,这是主要动力的一部分!已经修复。 - Ernest Friedman-Hill

5
现在StringBuilder和StringBuffer都从性能的角度来看有点无用。我来解释一下原因:
StringBuilder本来应该比StringBuffer快,但任何合理的JVM都可以优化同步操作。所以当它被引入时,这是一个相当大的失误(和小小的打击)。
StringBuffer在创建字符串时(非共享变体),过去不会复制char[],然而这是问题的主要来源,包括为小字符串泄漏巨大的char[]。在1.5版本中,他们决定每次必须复制char[],这使得StringBuffer无用了(同步操作是为了确保没有线程可以欺骗该字符串)。这节约了内存,并最终有助于GC(除了显然减少的占用空间之外),通常char[]是消耗内存最多的三个对象之一。
String.concat仍然是连接两个字符串(可能是三个)的最快方法。记住,它不需要执行额外的char[]复制。
回到无用的部分,现在任何第三方代码都可以达到与StringBuilder相同的性能水平。即使在Java 1.1中,我也使用了一个名为AsyncStringBuffer的类,它完全可以做到StringBuilder现在所做的事情,但它分配的char[]比StringBuilder更大。默认情况下,StringBuffer/StringBuilder都针对小字符串进行了优化,您可以查看构造函数。
  StringBuilder(String str) {
    super(str.length() + 16);
    append(str);
    }

因此,如果第二个字符串超过16个字符,它会得到底层char[]的另一个副本。这样做并不太好。
这可能是试图将StringBuilder / Buffer和char[]都适合同一缓存行(在x86上)的副作用,在32位操作系统上...但我不确定。
至于关于数小时的调试等评论,请自行判断,我个人不记得曾经遇到过任何与字符串操作有关的问题,除了为JDO impl的sql生成器实现绳索类似的结构之外。
编辑: 下面我将说明java设计师没有做的使字符串操作更快的事情。 请注意,该类旨在用于java.lang包,并且只能通过将其添加到引导类路径中来放置在那里。但是,即使不放置在那里(差异仅为一行代码!),它仍然比StringBuilder快,让人震惊?这个类会使string1 + string2 + ...比使用StringBuilder好很多,但是......
package java.lang;

public class FastConcat {

    public static String concat(String s1, String s2){
        s1=String.valueOf(s1);//null checks
        s2=String.valueOf(s2);

        return s1.concat(s2);
    }

    public static String concat(String s1, String s2, String s3){
        s1=String.valueOf(s1);//null checks
        s2=String.valueOf(s2);
        s3=String.valueOf(s3);
        int len = s1.length()+s2.length()+s3.length();
        char[] c = new char[len];
        int idx=0;
        idx = copy(s1, c, idx);
        idx = copy(s2, c, idx);
        idx = copy(s3, c, idx);
        return newString(c);
    }
    public static String concat(String s1, String s2, String s3, String s4){
        s1=String.valueOf(s1);//null checks
        s2=String.valueOf(s2);
        s3=String.valueOf(s3);
        s4=String.valueOf(s4);

        int len = s1.length()+s2.length()+s3.length()+s4.length();
        char[] c = new char[len];
        int idx=0;
        idx = copy(s1, c, idx);
        idx = copy(s2, c, idx);
        idx = copy(s3, c, idx);
        idx = copy(s4, c, idx);
        return newString(c);

    }
    private static int copy(String s, char[] c, int idx){
        s.getChars(c, idx);
        return idx+s.length();

    }
    private static String newString(char[] c){
        return new String(0, c.length, c);
        //return String.copyValueOf(c);//if not in java.lang
    }
}

2
几点说明:1. JVM(据我所知)只能优化本地变量的同步(逃逸分析),因此在使用非本地变量时,“StringBuilder”仍然比“StringBuffer”更快。2. 使用“StringBuilder”记录了您不关心同步的事实。正确使用时,这使代码更易于理解。3. 您的帖子让人觉得“StringBuilder”永远不会比连接更快。您的“concat”代码都很好,但它解决的问题与“StringBuilder”不同。我怀疑在大量连接的紧密循环中,它不会比“StringBuilder”更快。 - Konrad Rudolph
  1. 不是很确定 - 这取决于JVM内联的程度,即对象可能逃逸的程度,而且现在无争用同步也几乎是免费的(它不会膨胀对象头)。
  2. 这并不是真正的性能问题(我自己倾向于主要查看代码)。
  3. 帖子没有这么说,提供示例代码是为了在您获得>时使用。 String s =“xxx:”+ someInt +“yyy”+ anotherString; 那将打败任何StringBuilder...回到重点。许多Java1.5实现已经放弃了StringBuffer,因为它对于共享(多个)调用没有帮助。
- bestsss
@Konrad,也许有些不清楚的是:目前,无论是StringBuilder/Buffer都没有从放在java.lang包中获得任何好处。因此,一个自定义的java1.1实现与StringBuilder的速度一样快。在java1.5之前,StringBuffer试图聪明地避免复制char[]以避免分配(和一些较小的GC)成本。这没问题,但是不添加一些简单的连接方法(如上面的片段)并不是一个明智的举动。对于2个字符串,String.concat始终比StringBuilder/Buffer更快。你甚至无法接近它(除非JVM可以使用intrisics)。 - bestsss

1

我在一台XP机器上尝试了同样的事情。StringBuilder确实更快,但如果您改变运行顺序或进行多次运行,您会注意到结果中的“几乎两倍”的差距将变成大约10%的优势:

StringBuffer build & output duration= 4282,000000 µs
StringBuilder build & output duration= 4226,000000 µs
StringBuffer build & output duration= 4439,000000 µs
StringBuilder build & output duration= 3961,000000 µs
StringBuffer build & output duration= 4801,000000 µs
StringBuilder build & output duration= 4210,000000 µs

对于您这种类型的测试,JVM将无法提供帮助。我不得不限制运行次数和元素数量,才能从“仅字符串”测试中获得任何结果。


0

决定用一个简单的XML练习来测试选项。对于那些希望复制结果的人,测试是在一台2.7GHz i5处理器和16Gb DDR3内存的计算机上完成的。

代码:

   private int testcount = 1000; 
   private int elementCount = 50000;

   public void testStringBuilder() {

    long total = 0;
    int counter = 0;
    while (counter++ < testcount) {
        total += doStringBuilder();
    }
    float f = (total/testcount)/1000;
    System.out.printf("StringBuilder build & output duration= %f µs%n%n", f); 
}

private long doStringBuilder(){
    long start = System.nanoTime();
    StringBuilder buffer = new StringBuilder("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
    buffer.append("<root>");
      for (int i =0; i < elementCount; i++) {
          buffer.append("<data/>");
      }
      buffer.append("</root>");
     //System.out.println(buffer.toString());
      output = buffer.toString();
      long end = System.nanoTime();
     return end - start;
}


public void testStringBuffer(){
    long total = 0;
    int counter = 0;
    while (counter++ < testcount) {
        total += doStringBuffer();
    }
    float f = (total/testcount)/1000;

    System.out.printf("StringBuffer build & output duration= %f µs%n%n", f); 
}

private long doStringBuffer(){
    long start = System.nanoTime();
    StringBuffer buffer = new StringBuffer("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
    buffer.append("<root>");
      for (int i =0; i < elementCount; i++) {
          buffer.append("<data/>");
      }
      buffer.append("</root>");
     //System.out.println(buffer.toString());
      output = buffer.toString();

      long end = System.nanoTime();
      return end - start;
}

结果:

在OSX机器上:
StringBuilder构建和输出时间= 1047.000000微秒
StringBuffer构建和输出时间= 1844.000000微秒
在Win7机器上: StringBuilder构建和输出时间= 1869.000000微秒
StringBuffer构建和输出时间= 2122.000000微秒

因此,看起来性能增强可能是平台特定的,取决于JVM如何实现同步。

参考资料:

关于System.nanoTime()的使用已经在这里讨论过 -> Is System.nanoTime() completely useless? 和这里 -> How do I time a method's execution in Java?

StringBuilder & StringBuffer的源代码在这里 -> http://www.java2s.com/Open-Source/Java-Document/6.0-JDK-Core/lang/java.lang.htm

这里有一个关于同步的好概述 -> http://www.javaworld.com/javaworld/jw-07-1997/jw-07-hood.html?page=1


1
谢谢你的回答,但我不是在寻找基准测试。我的问题是关于Java平台的架构。无论如何,您应该真正检查一下您的测试/基准测试,因为在单线程环境中StringBuilder应该更快(不想无礼,但这不是一个好的基准测试)。此外,测试内存是否有一些由StringBuffer或StringBuilder使用的辅助结构也是不错的选择。我建议您制作一个单一的方法,因为“concat”方法对所有类都是相同的,即具有相同的API。 - santiagobasulto
我认为 StringBuilder 应该更快,而这里的一个类似测试 -> http://littletutorials.com/2008/07/16/stringbuffer-vs-stringbuilder-performance-comparison/ 也显示了它的速度更快(同样在jre6中完成)。问题是为什么——也许有一个潜在的架构原因?在一台具有1Gb RAM的 Windows 7 机器上进行进一步测试得出了预期结果(即 StringBuilder 更快)(以前的测试是在 OSX 机器上进行的)……也许不同的实现方式会以不同的方式利用可用的内存,或者操作系统可能也起着重要作用? - binarycube
不好意思,我的朋友,原因是你的测试不够好!如果你有一分钟的时间,请阅读《深入 Python》这一章的介绍(它是关于 Python 的,不是关于 Java,抱歉)。http://www.diveintopython.net/performance_tuning/timeit.html - santiagobasulto
感谢@santiagobasulto指出单值测试是没有意义的 - 应该知道得更好...但看起来它工作正常...啊哈... - binarycube

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接