StringBuffer is Dead, Long Live StringBuffer

When Java 5.0 was released on 30th September 2004, it introduced StringBuilder as a replacement for StringBuffer in cases where thread safety isn't required. The idea was simple: if you're manipulating strings within a single thread, StringBuilder offers a faster, unsynchronized alternative to StringBuffer.

This is an updated article from 2011

From the Javadoc for StringBuilder:

This class provides an API compatible with StringBuffer, but with no guarantee of synchronization. This class is designed for use as a drop-in replacement for StringBuffer in places where the string buffer was being used by a single thread (as is generally the case). Where possible, it is recommended that this class be used in preference to StringBuffer as it will be faster under most implementations.

Is StringBuffer Really Dead?

You might think that StringBuffer has become redundant, given that most single-threaded scenarios can use StringBuilder, and thread safety often requires explicit synchronization beyond what StringBuffer provides. Yet, if we look at the latest OpenJDK (Java 21), it appears that StringBuffer is still very much alive.

Consider the usage statistics in OpenJDK Java 21 compared to Java 6 Update 25:

Class Uses in Java 21.0.5   Uses in Java 6u25
StringBuffer 601 1,409
StringBuilder 3,262 311

While StringBuilder usage has significantly increased, StringBuffer still has a substantial presence. This raises the question: why hasn't StringBuffer been phased out in favour of its unsynchronized counterpart, especially in the JDK's own codebase?

Why Does StringBuffer Persist?

Migration is hard. Much harder than you might expect. Especially when you have a code base that works. Oracle can't force developers to migrate their code, but one might expect that the JDK's internal code would adopt StringBuilder where appropriate. Yet, even after nearly two decades, StringBuffer remains in use in places where thread safety isn't a concern.

Let's look at some examples from Java 21 where StringBuffer is used as a local variable in methods that are not thread-safe:

  • javax.swing.text.html.StyleSheet#getRule
  • javax.swing.text.html.parser.Parser#parseTag
  • jdk.internal.icu.text.BidiWriter#doWriteForward
  • jdk.internal.org.jline.reader.PrintAboveWriter#flush
  • sun.jvm.hotspot.oops.Method#externalNameAndSignature
  • sun.security.mscapi.CPublicKey.CECPublicKey#toString

Interestingly, some methods even mix StringBuilder and StringBuffer:

  • sun.jvm.hotspot.interpreter.BytecodeGetPut#toString

And in an ironic twist, non-thread-safe classes like SimpleDateFormat and other Format classes use StringBuffer internally.

Performance Implications

Using StringBuffer when thread safety isn't needed introduces unnecessary overhead due to its synchronized methods. To illustrate the performance difference, consider the following benchmark:

public static void main(String... args) {
    String text = "A short piece of text for copying";
    int runs = 1_000_000;

    for (int i = 0; i < 5; i++) {
        {
            long start = System.nanoTime();
            StringBuffer sb = new StringBuffer(text);
            for (int r = 0; r < runs; r++)
                copyStringBuffer(sb);
            long time = System.nanoTime() - start;
            System.out.printf("StringBuffer took an average of %,d ns%n", time / runs);
        }
        {
            long start = System.nanoTime();
            StringBuilder sb = new StringBuilder(text);
            for (int r = 0; r < runs; r++)
                copyStringBuilder(sb);
            long time = System.nanoTime() - start;
            System.out.printf("StringBuilder took an average of %,d ns%n", time / runs);
        }
    }
}

public static String copyStringBuffer(StringBuffer text) {
    StringBuffer sb = new StringBuffer();
    for (int i = 0; i < text.length(); i++)
        sb.append(text.charAt(i));
    return sb.toString();
}

public static String copyStringBuilder(StringBuilder text) {
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < text.length(); i++)
        sb.append(text.charAt(i));
    return sb.toString();
}

Running this benchmark on a Ryzen 5950X with Ubuntu and Azul JDK 21.0.5 yields:

StringBuffer took an average of 456 ns
StringBuilder took an average of 125 ns
StringBuffer took an average of 461 ns
StringBuilder took an average of 162 ns
StringBuffer took an average of 400 ns
StringBuilder took an average of 97 ns
StringBuffer took an average of 387 ns
StringBuilder took an average of 98 ns
StringBuffer took an average of 388 ns
StringBuilder took an average of 97 ns

The results show that StringBuffer is approximately four times slower than StringBuilder in this scenario. While the absolute difference in nanoseconds might seem trivial, in performance-critical applications or loops, this overhead can accumulate.

Why Doesn't Escape Analysis Help?

One might hope that the JVM's Escape Analysis could optimise away the synchronization overhead of StringBuffer when used locally. However, the JVM doesn't eliminate this overhead in such cases.

Conclusion

Despite the introduction of StringBuilder nearly two decades ago, StringBuffer persists in both third-party and JDK internal code. This suggests that relying on passive adoption of improvements isn't sufficient. A deliberate effort is needed to refactor existing codebases.

For developers, it's crucial to:

  • Audit your code: Identify where StringBuffer is used and assess if StringBuilder can replace it.
  • Understand your thread model: Ensure that you're not inadvertently introducing thread safety issues when switching to StringBuilder.
  • Benchmark critical sections: Measure the performance impact of such changes in your specific context.

Key Takeaways

  • StringBuilder offers a faster alternative to StringBuffer for single-threaded scenarios.
  • Even in the latest JDK, StringBuffer remains widely used, often unnecessarily.
  • Developers should actively refactor code to replace StringBuffer with StringBuilder where appropriate.
  • Performance gains from such refactoring can be significant, especially in tight loops or performance-critical code.

Questions for the Reader

  • Have you audited your codebase for unnecessary use of StringBuffer?
  • Are there areas where replacing StringBuffer with StringBuilder could yield performance improvements?
  • What strategies do you use to keep your code up-to-date with language improvements?

Feel free to share your thoughts or experiences on this topic. Let's discuss how we can collectively improve our Java applications by adopting best practices and optimisations.

About the author

As the CEO of Chronicle Software, Peter Lawrey leads the development of cutting-edge, low-latency solutions trusted by 8 out of the top 11 global investment banks. With decades of experience in the financial technology sector, he specialises in delivering ultra-efficient enabling technology which empowers businesses to handle massive volumes of data with unparalleled speed and reliability. Peter's deep technical expertise and passion for sharing knowledge have established him as a thought leader and mentor in the Java and FinTech communities.

Comments

Popular posts from this blog

Java is Very Fast, If You Don’t Create Many Objects

System wide unique nanosecond timestamps

Comparing Approaches to Durability in Low Latency Messaging Queues