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 forStringBuffer
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 toStringBuffer
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 is StringBuffer
Only Kind of Thread-Safe
For StringBuffer
, individual methods are thread safe, however you usually need to make more than one call, and you can get multiple outcomes
Consider the following scenario involving three threads in Java:
- Thread 1 executes:
stringBuffer.append("1").append("2");
- Thread 2 executes:
stringBuffer.append("A").append("B");
- Thread 3 executes:
System.out.println(stringBuffer);
Question: What are all the possible outputs that could be printed by the System.out.println
statement in Thread 3?
Keep in mind that StringBuffer
is thread-safe and its methods are synchronized. The order of operations within each thread is fixed, but the execution order across threads can vary.
Answer:
The possible outputs that might be printed are:
""
(empty string)"1"
"1A"
"1AB"
"1A2"
"1A2B"
"1AB2"
"12"
"12A"
"12AB"
"A"
"A1"
"A1B"
"A1B2"
"A12"
"A12B"
"AB"
"AB1"
"AB12"
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 ifStringBuilder
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 toStringBuffer
for single-threaded scenarios.- Even in the latest JDK,
StringBuffer
remains widely used, often unnecessarily. - Developers should actively refactor code to replace
StringBuffer
withStringBuilder
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
withStringBuilder
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
Post a Comment