Why StringBuffer is Slower than StringBuilder?
StringBuffer is slower because every one of its methods is marked with the synchronized keyword. This means before executing any operation (even a simple append), Java must acqu...
🟢 Junior Level
StringBuffer is slower because every one of its methods is marked with the synchronized keyword. This means before executing any operation (even a simple append), Java must acquire an object “monitor” (lock).
Simple analogy: Imagine a bathroom door:
StringBuilder— a door without a lock. Walk in and do your thing.StringBuffer— a door with a lock. You need to unlock it first, walk in, do your thing, lock it back. Even if you’re alone in the building.
Example:
StringBuilder sb = new StringBuilder(); // No lock — fast
StringBuffer sb2 = new StringBuffer(); // With lock — slower
for (int i = 0; i < 1_000_000; i++) {
sb.append("x"); // ~15ms
sb2.append("x"); // ~25ms (60% slower)
}
// Numbers are approximate (JMH, single-thread, JDK 17, x86_64). // May differ on your hardware, but the ratio remains.
When this matters: Only when you call millions of operations. For normal tasks the difference is unnoticeable.
🟡 Middle Level
Why synchronized slows things down
Each synchronized method of StringBuffer requires:
- Monitor acquisition — checking if the object is locked by another thread
- Memory Barriers — processor synchronizes cache with main memory
- Monitor release — after method completion
Even if there’s only one thread, JVM still performs these steps.
Where it’s used in practice
StringBuffer is legacy from Java 1.0. In modern code it’s practically not used. The only scenario: one StringBuilder object is actually used from multiple threads (which itself is a rare and suspicious pattern).
Typical mistakes
-
Mistake: Thinking
StringBufferis needed for “safety” Solution: If each thread uses its own buffer —StringBuilderis safe -
Mistake: Using
StringBufferfor loop concatenation “just in case” Solution: Loop concatenation is almost always single-threaded —StringBuilder
Performance comparison (single-threaded mode)
| Operation | StringBuilder | StringBuffer | Difference | | ———- | ————– | ————– | ———- | | 1M append | ~15ms | ~25ms | +66% | | 1M insert | ~20ms | ~35ms | +75% | | 1M delete | ~10ms | ~18ms | +80% |
🔴 Senior Level
Internal Implementation
// StringBuffer — EVERY public method is synchronized
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
// StringBuilder — NOT A SINGLE synchronized method
public StringBuilder append(String str) {
super.append(str);
return this;
}
Both delegate to AbstractStringBuilder, but the StringBuffer wrapper adds synchronized on every call.
What’s behind synchronized
- Monitor Enter/Exit: JVM instructions
monitorenterandmonitorexit - Memory Barriers: LoadLoad, StoreStore, LoadStore, StoreLoad — processor flushes and invalidates cache lines
- Object Header: Lock owner information is recorded in the object’s mark word
JVM optimizations: Lock Elision and Lock Coarsening
Lock Elision (lock removal):
void foo() {
StringBuffer sb = new StringBuffer(); // Escape analysis: object doesn't "escape"
sb.append("a"); // JIT: synchronized can be removed!
sb.append("b");
}
HotSpot via Escape Analysis can prove the object is not visible to other threads and remove synchronized.
Lock Coarsening (enlarging locks):
StringBuffer sb = ...;
sb.append("a"); // monitor acquisition
sb.append("b"); // monitor acquisition
sb.append("c"); // monitor acquisition
// JIT may combine into one lock:
// synchronized(sb) { append("a"); append("b"); append("c"); }
// JIT applies coarsening only if it sees sequential calls // within one compilable method. If calls are spread across different // methods — coarsening is impossible.
But: These optimizations are not guaranteed. They depend on:
-XX:+EliminateLocks(enabled by default)- C2 compilation budget
- Method size (inlining budget)
Edge Cases
-
Biased Locking (removed in Java 15): Previously JVM “biased” the monitor to the first thread, making subsequent captures free. Removed due to overhead in containers.
- Contended access: If 2+ threads actually compete for
StringBuffer:- Thread A: BLOCKED → context switch → OS scheduling → resume
- Context switch: ~1-10μs (much more expensive than monitor enter without contention)
- False Sharing: The
StringBufferobject monitor may cause false sharing with neighboring objects in the cache line.
Performance (detailed benchmarks)
| Scenario | StringBuilder | StringBuffer | Delta | | ———————- | ————- | ————– | ———– | | 1 thread, no escape | 15ms | 18ms (elision) | +20% | | 1 thread, escapes | 15ms | 25ms | +66% | | 4 threads, no contention | 15ms x4 | 25ms x4 | +66% | | 4 threads, contention | 15ms x4 | 200ms | +1233% |
Production Experience
Scenario: Logging in a web service (50K RPS):
StringBufferfor formatting each log: p99 latency = 8msStringBuilder: p99 latency = 5ms- Difference of 3ms × 50K = 150 CPU-seconds/sec → 150 cores wasted
Best Practices for Highload
StringBuilder— default choiceStringBuffer— only if one buffer is actually shared between threads- If you need a thread-safe buffer, better to use
StringBuilder+ external synchronization (control over granularity) - For maximum performance:
StringBuilderwithinitialCapacity+ avoid reallocations
🎯 Interview Cheat Sheet
Must know:
StringBufferis slower due tosynchronizedon every method- synchronized requires: monitorenter/monitorexit, memory barriers, monitor release
- Even in single-threaded mode, JVM performs all synchronization steps
- JVM may optimize via Lock Elision (Escape Analysis), but this is not guaranteed
- Biased Locking removed in Java 15 —
StringBufferoverhead in single-threaded mode is even higher - Contention (2+ threads): context switch ~1-10μs, much more expensive than monitor enter
Frequent follow-up questions:
- How much slower is
StringBuffer? — ~66% slower in single-threaded mode, ~1200%+ with 4-thread contention. - What is Lock Elision? — JIT via Escape Analysis proves the object is not visible to other threads and removes
synchronized. Not guaranteed. - What is Lock Coarsening? — JIT combines sequential synchronized calls into one lock. Works only within one compilable method.
- Why was Biased Locking removed? — overhead in containers. In Java 15+
StringBufferoverhead is even higher.
Red flags (DON’T say):
- ❌ “
StringBufferis faster in single-threaded mode” — slower due to synchronized - ❌ “JVM always optimizes synchronized” — Lock Elision is not guaranteed
- ❌ “
StringBufferis needed for every multithreaded program” — only if ONE buffer is shared - ❌ “The difference is unnoticeable in practice” — at 50K RPS: 3ms × 50K = 150 CPU-seconds/sec wasted
Related topics:
- [[5. When to Use StringBuilder vs StringBuffer]]
- [[4. Why String is Immutable]]