When to Use StringBuilder vs StringBuffer?
Both classes are designed for modifying strings. Unlike regular String, they can change their content without creating new objects.
π’ Junior Level
Both classes are designed for modifying strings. Unlike regular String, they can change their content without creating new objects.
Main difference: StringBuffer is thread-safe (synchronized), StringBuilder is not.
Example:
// StringBuilder β use in 99% of cases
StringBuilder sb = new StringBuilder();
sb.append("Hello");
sb.append(" ");
sb.append("World");
String result = sb.toString(); // "Hello World"
// StringBuffer β only if string is used from multiple threads
StringBuffer sb2 = new StringBuffer();
sb2.append("Hello");
Rule: Use StringBuilder by default. StringBuffer only makes sense when one buffer is shared between threads. In legacy code
it appears more often, but this is a historical artifact, not a recommendation.
π‘ Middle Level
How it works
Both classes inherit from AbstractStringBuilder and work with an internal byte[] array (Java 9+) or char[] (before Java 9):
// Internal structure
AbstractStringBuilder {
byte[] value; // Data array
int count; // Current character count
}
Initial capacity: 16 characters by default. On overflow, the array expands: newCapacity = (oldCapacity << 1) + 2 (doubling + 2).
Practical application
// StringBuilder β building a string in a single thread (typical case)
StringBuilder sb = new StringBuilder();
for (User user : users) {
sb.append(user.getName()).append(", ");
}
Typical mistakes
-
Mistake: Using
StringBufferβjust in caseβ Solution: Synchronization adds overhead even in single-threaded code -
Mistake: Not specifying capacity when size is known Solution:
new StringBuilder(1024)avoids unnecessary reallocations
Comparison
| Criterion | StringBuilder | StringBuffer | | ββββββ | βββββ | βββββββββββββββββββββββββββββββββ | | Thread safety | No | Yes (synchronized) | | Speed (1 thread) | Faster | 1.5-2.5x slower. Each synchronized method requires monitorenter/monitorexit + memory barriers. | In single-threaded mode, JVM can partially elide locks, but not completely. | | Speed (N threads) | Race condition | Correct, but contention | | When introduced | Java 5 | Java 1.0 | | Recommendation | By default | Only for real shared access |
π΄ Senior Level
Internal Implementation
// StringBuffer β all methods are synchronized
@Override
public synchronized StringBuffer append(Object obj) {
toStringCache = null;
super.append(String.valueOf(obj));
return this;
}
// StringBuilder β without synchronization
@Override
public StringBuilder append(Object obj) {
return append(String.valueOf(obj));
}
Key difference: every StringBuffer method has synchronized, meaning object monitor acquisition on each call.
Architectural Trade-offs
StringBuffer:
- Pros: Thread-safe, legacy compatibility
- Cons: Monitor on every method, memory barriers, false sharing under contention
StringBuilder:
- Pros: No synchronization overhead, optimal performance
- Cons: Not thread-safe β race condition on parallel access
JVM optimizations: Lock Elision
Modern JVM (HotSpot) uses Escape Analysis + Lock Elision:
void method() {
StringBuffer sb = new StringBuffer(); // Escape: doesn't escape method scope
sb.append("a"); // JIT may remove synchronized
sb.append("b");
}
If JIT proved the object doesnβt βescapeβ the thread β synchronization is removed. But:
- This is not guaranteed (depends on
-XX:+EliminateLocks, compilation budget) - The analysis itself costs CPU cycles
StringBuilderis fast βout of the boxβ without JIT magic
Edge Cases
-
Biased Locking (removed in Java 15): Previously JVM βbiasedβ the monitor to the first thread, making subsequent captures free. This means in Java 15+ the StringBuffer overhead in single-threaded mode is even higher than before.
-
Resize formula:
(oldCapacity << 1) + 2. For capacity=16 β 34 β 70 β 142. For known sizes, always set capacity in constructor. -
toStringCache in StringBuffer:
StringBuffercaches the lasttoString()result. If buffer hasnβt changed, repeatedtoString()returns cache β but this is a micro-optimization. -
Java 9+ Compact Strings: Both classes use
byte[]withcoderflag. Latin characters = 1 byte, others = 2 bytes.
Performance (JMH benchmarks)
| Operation | StringBuilder | StringBuffer (no contention) | StringBuffer (4 threads) | | ββββ- | ββββ- | βββββββββ- | ββββββββ | | append 1M | ~15ms | ~25ms | ~120ms | | toString | ~2ms | ~2ms (cache) | ~2ms | | Memory | ~2MB | ~2MB + monitor | ~2MB + monitor overhead |
Production Experience
Scenario: Rendering HTML report (50KB total) from 10,000 records:
- Without capacity: 12 realloc + copy operations β 15ms
- With
new StringBuilder(51200): 0 realloc β 3ms StringBufferin same scenario: 20ms (lock elision helped, but not fully)
Best Practices for Highload
- Always specify
initialCapacityif size is predictable - In loops:
StringBuilderoutside the loop,appendinside StringBufferβ only if buffer is shared between threads (extremely rare case)- For concatenation outside loops: Java 9+
invokedynamicis often more efficient than manual StringBuilder
π― Interview Cheat Sheet
Must know:
StringBuilderβ mutable, NOT thread-safe, use in 99% of casesStringBufferβ mutable, thread-safe (synchronized), legacy from Java 1.0- Both work with internal
byte[](Java 9+) /char[](before Java 9), expand on overflow:newCapacity = (old << 1) + 2 - Default initial capacity β 16 characters. For known size β set in constructor
StringBuilderis 1.5-2.5x faster thanStringBufferin single-threaded mode- In loops: create
StringBuilderBEFORE the loop,appendINSIDE
Frequent follow-up questions:
- Why is
StringBufferslower? β Every method issynchronized: monitor acquisition, memory barriers, release. Even in a single thread. - When does expansion happen? β On overflow: 16 β 34 β 70 β 142. Each expansion = new array allocation +
System.arraycopy. - Can you use
StringBufferfor safety? β If each thread uses its own buffer βStringBuilderis safe.StringBufferis only needed for real shared access. - What does JVM do to optimize
StringBuffer? β Lock Elision via Escape Analysis: if object doesnβt βescapeβ the thread β JIT may removesynchronized. But not guaranteed.
Red flags (DONβT say):
- β β
StringBufferβ the recommended choiceβ β outdated,StringBuilderis the default - β βStringBuilder is thread-safeβ β no, race condition on parallel access
- β βNo need to set capacityβ β without capacity: 12+ realloc for a 50KB string
- β β
StringBufferis needed for every multithreaded appβ β only if ONE buffer is shared between threads
Related topics:
- [[6. Why StringBuffer is Slower than StringBuilder]]
- [[4. Why String is Immutable]]
- [[7. What Happens When Concatenating Strings with + Operator]]